posimai-root/tools/pc-audit/report-viewer.html

801 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ja" data-theme="dark">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PC Audit — Posimai</title>
<!-- AUDIT_PRELOAD -->
<style>
:root {
--bg: #0D0D0D;
--surface: #1A1A1A;
--surface2: #252525;
--border: #2D2D2D;
--text: #F3F4F6;
--text2: #9CA3AF;
--text3: #6B7280;
--accent: #6EE7B7;
--warn-color: #F59E0B;
--warn-dim: rgba(245,158,11,0.08);
--warn-border: rgba(245,158,11,0.2);
--ok-color: #6EE7B7;
--ok-dim: rgba(110,231,183,0.06);
--ok-border: rgba(110,231,183,0.2);
--info-color: #60A5FA;
--info-dim: rgba(96,165,250,0.06);
--info-border: rgba(96,165,250,0.2);
--radius: 12px;
--radius-sm: 8px;
--header-h: 44px;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
/* ── Page shell ── */
.page {
height: 100vh;
display: flex;
flex-direction: column;
padding: 0.9rem 1.5rem 0.75rem;
gap: 0.75rem;
}
/* ── Header bar ── */
.header {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 1rem;
flex-wrap: wrap;
min-height: var(--header-h);
}
.header h1 {
font-size: 1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2);
}
.header h1 span { color: var(--accent); }
.header-meta {
display: flex; align-items: center; gap: 0.75rem; flex: 1; flex-wrap: wrap;
}
.privacy-note {
font-size: 0.72rem; color: var(--text3);
display: flex; align-items: center; gap: 0.35rem;
}
.privacy-dot {
display: inline-block; width: 5px; height: 5px;
border-radius: 50%; background: var(--accent); flex-shrink: 0;
}
.alt-file-btn {
background: none; border: none; padding: 0; margin-left: auto;
font-size: 0.72rem; color: var(--text3); cursor: pointer;
text-decoration: underline; text-underline-offset: 2px;
}
.alt-file-btn:hover { color: var(--accent); }
/* ── Upload state (no data) ── */
.upload-wrap {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
}
.upload-area {
background: var(--surface); border: 1px dashed var(--border);
border-radius: var(--radius); padding: 2rem 2.5rem;
width: 100%; max-width: 440px;
}
.upload-label {
font-size: 0.85rem; font-weight: 600; color: var(--text2);
margin-bottom: 0.5rem; display: block;
}
.upload-path {
font-size: 0.73rem; color: var(--text3); background: var(--surface2);
padding: 0.2rem 0.5rem; border-radius: var(--radius-sm);
font-family: monospace; display: inline-block; margin-bottom: 0.75rem;
}
input[type="file"] { font-size: 0.82rem; color: var(--text2); cursor: pointer; max-width: 100%; }
input[type="file"]::file-selector-button {
background: var(--surface2); color: var(--text); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 0.3rem 0.75rem; font-size: 0.78rem;
cursor: pointer; margin-right: 0.5rem;
}
/* ── 3-column grid (data state) ── */
#output {
flex: 1;
min-height: 0;
display: none;
flex-direction: column;
}
.grid3 {
flex: 1;
min-height: 0;
display: grid;
grid-template-columns: 264px 1fr 1fr;
gap: 1rem;
}
@media (max-width: 900px) {
html, body { overflow: auto; height: auto; }
.page { height: auto; overflow: visible; padding: 1rem; }
#output { display: flex !important; flex-direction: column; }
.grid3 { grid-template-columns: 1fr; }
.grid-col { max-height: 70vh; }
}
/* ── Grid column (flex column, child scroll-pane fills it) ── */
.grid-col {
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.col-label {
flex-shrink: 0;
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--text3);
margin-bottom: 0.6rem; padding-bottom: 0.45rem;
border-bottom: 1px solid var(--border);
}
.filter-row {
flex-shrink: 0;
display: flex; gap: 0.4rem; margin-bottom: 0.6rem; flex-wrap: wrap;
align-items: center;
}
/* ── AI prompt copy button ── */
.copy-prompt-btn {
margin-left: auto;
font-size: 0.68rem; font-weight: 600;
padding: 0.2rem 0.65rem;
border-radius: 99px;
border: 1px solid var(--border);
background: transparent; color: var(--text3);
cursor: pointer;
transition: border-color 0.12s, color 0.12s, background 0.12s;
white-space: nowrap;
}
.copy-prompt-btn:hover { border-color: var(--accent); color: var(--accent); }
.copy-prompt-btn.copied { border-color: var(--ok-color); color: var(--ok-color); }
/* ── Scrollable area inside each column ── */
.scroll-pane {
flex: 1;
min-height: 0;
overflow-y: auto;
padding-right: 3px;
scrollbar-width: thin;
scrollbar-color: var(--border) transparent;
}
.scroll-pane::-webkit-scrollbar { width: 4px; }
.scroll-pane::-webkit-scrollbar-track { background: transparent; }
.scroll-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* ── Filter chips ── */
.chip {
font-size: 0.68rem; font-weight: 600; padding: 0.18rem 0.55rem;
border-radius: 99px; border: 1px solid var(--border);
cursor: pointer; color: var(--text3); background: transparent;
transition: border-color 0.12s, color 0.12s, background 0.12s;
user-select: none;
}
.chip:hover { border-color: var(--text2); color: var(--text2); }
.chip.active { background: var(--surface2); color: var(--text); border-color: var(--accent); }
.chip.warn-chip.active { border-color: var(--warn-color); color: var(--warn-color); }
.chip.info-chip.active { border-color: var(--info-color); color: var(--info-color); }
.chip.scope-all-chip.active { border-color: var(--warn-color); color: var(--warn-color); }
.chip.scope-local-chip.active { border-color: var(--ok-color); color: var(--ok-color); }
/* ── Summary metrics ── */
.summary-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: 0.45rem; margin-bottom: 0.65rem;
}
.metric {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 0.6rem 0.8rem;
}
.metric.full { grid-column: 1 / -1; }
.metric-label { font-size: 0.67rem; color: var(--text3); margin-bottom: 0.1rem; }
.metric-value { font-size: 0.88rem; font-weight: 600; color: var(--text); word-break: break-all; }
/* ── Diff card ── */
.diff-card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.8rem 0.9rem; margin-bottom: 0.65rem;
}
.diff-card-title { font-size: 0.77rem; font-weight: 600; color: var(--text2); margin-bottom: 0.55rem; }
.diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; }
.diff-alert { margin-top: 0.45rem; font-size: 0.77rem; color: var(--warn-color); }
/* ── Raw data (lives in col 1 scroll pane) ── */
.raw-details {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.8rem 1rem;
margin-bottom: 0.65rem;
}
.raw-details summary {
cursor: pointer; font-size: 0.75rem; font-weight: 600;
color: var(--text3); user-select: none;
}
.raw-details pre {
margin-top: 0.5rem; font-size: 0.68rem; color: var(--text3);
overflow: auto; max-height: 14rem; line-height: 1.5;
}
.disclaimer {
font-size: 0.7rem; color: var(--text3);
border-top: 1px solid var(--border); padding-top: 0.75rem; margin-bottom: 0.5rem;
}
/* ── Guidance cards ── */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.85rem 0.95rem; margin-bottom: 0.45rem;
}
.card.warn { background: var(--warn-dim); border-color: var(--warn-border); }
.card.ok { background: var(--ok-dim); border-color: var(--ok-border); }
.card.info { background: var(--info-dim); border-color: var(--info-border); }
.card-header { display: flex; align-items: flex-start; gap: 0.4rem; margin-bottom: 0.4rem; }
.badge {
font-size: 0.61rem; font-weight: 700; letter-spacing: 0.06em;
padding: 0.14rem 0.42rem; border-radius: 99px; white-space: nowrap;
flex-shrink: 0; margin-top: 0.22rem;
}
.badge-warn { background: rgba(245,158,11,0.15); color: var(--warn-color); }
.badge-ok { background: rgba(110,231,183,0.15); color: var(--ok-color); }
.badge-info { background: rgba(96,165,250,0.15); color: var(--info-color); }
.card-title { font-size: 0.85rem; font-weight: 600; color: var(--text); line-height: 1.4; }
.card-body { font-size: 0.79rem; color: var(--text2); line-height: 1.65; }
.action-box {
margin-top: 0.6rem; background: var(--surface2);
border-radius: var(--radius-sm); padding: 0.55rem 0.75rem;
font-size: 0.77rem; color: var(--text2);
}
.action-label {
font-size: 0.61rem; font-weight: 700; letter-spacing: 0.1em;
color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.18rem;
}
/* ── Port list ── */
.port-legend {
flex-shrink: 0;
font-size: 0.72rem; color: var(--text2);
margin-bottom: 0.55rem; line-height: 1.7;
}
.port-summary-row {
display: flex; gap: 0.9rem; flex-wrap: wrap; margin-bottom: 0.55rem;
}
.port-summary-item { font-size: 0.72rem; color: var(--text3); }
.port-summary-item strong { color: var(--text); }
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
thead th {
position: sticky; top: 0; z-index: 1;
background: var(--bg);
text-align: left; padding: 0.42rem 0.55rem;
font-size: 0.62rem; font-weight: 700; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--text3);
border-bottom: 1px solid var(--border);
}
tbody tr { border-bottom: 1px solid var(--border); }
tbody tr:last-child { border-bottom: none; }
tbody td { padding: 0.36rem 0.55rem; color: var(--text2); vertical-align: top; }
.addr-cell { font-family: monospace; font-size: 0.71rem; color: var(--text3); }
.port-cell { font-family: monospace; font-weight: 600; color: var(--text); }
.scope-all { color: var(--warn-color); font-size: 0.73rem; }
.scope-localhost { color: var(--ok-color); font-size: 0.73rem; }
.scope-specific { color: var(--info-color); font-size: 0.73rem; }
.hint-line { font-size: 0.66rem; color: var(--text3); margin-top: 0.08rem; }
.port-empty { font-size: 0.78rem; color: var(--text3); padding: 0.6rem 0; }
</style>
</head>
<body>
<div class="page">
<!-- ── Header ── -->
<div class="header">
<h1>PC Audit <span>/</span> Posimai</h1>
<div class="header-meta">
<p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作。データは外部に送信されません。</p>
<button id="alt-file-btn" class="alt-file-btn" style="display:none">別のファイルを読む</button>
</div>
</div>
<!-- ── Upload state ── -->
<div id="upload-wrap" class="upload-wrap">
<div class="upload-area">
<label class="upload-label" for="f">レポートファイルを選択</label>
<div class="upload-path">tools\pc-audit\out\latest.json</div><br/>
<input id="f" type="file" accept=".json,application/json" />
</div>
</div>
<!-- ── Data state: 3-column grid ── -->
<div id="output">
<div class="grid3">
<!-- Col 1: Summary + Diff + Raw -->
<div class="grid-col">
<div class="col-label">概要</div>
<div class="scroll-pane">
<div id="summary"></div>
<div id="diff"></div>
<details class="raw-details">
<summary>生データJSON</summary>
<pre id="raw"></pre>
</details>
<p class="disclaimer">補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
</div>
</div>
<!-- Col 2: Guidance -->
<div class="grid-col">
<div class="col-label" style="display:flex;align-items:center;justify-content:space-between">
<span>チェック項目</span>
<button id="copy-prompt-btn" class="copy-prompt-btn">AI 相談用にコピー</button>
</div>
<div class="filter-row" id="guidance-filter-row"></div>
<div class="scroll-pane" id="guidance"></div>
</div>
<!-- Col 3: Ports -->
<div class="grid-col">
<div class="col-label">待ち受けポート一覧</div>
<div class="filter-row" id="port-filter-row"></div>
<div id="port-legend" class="port-legend"></div>
<div class="scroll-pane">
<div class="table-wrap"><div id="listeners"></div></div>
</div>
</div>
</div>
</div>
</div>
<script>
(function () {
/* ------------------------------------------------------------------ */
/* State */
/* ------------------------------------------------------------------ */
var allGuidanceItems = [];
var allPortRows = [];
var guidanceFilter = "all";
var portFilter = "all";
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
function esc(s) {
var d = document.createElement("div");
d.textContent = s == null ? "" : String(s);
return d.innerHTML;
}
function len(a) { return Array.isArray(a) ? a.length : 0; }
/* ------------------------------------------------------------------ */
/* Port knowledge */
/* ------------------------------------------------------------------ */
var PORT_NOTES = {
"80": "HTTPWeb サーバー)",
"135": "Windows RPC — 正常",
"139": "NetBIOS ファイル共有 — LAN 内なら正常",
"443": "HTTPSWeb サーバー)",
"445": "SMB ファイル共有 — LAN 内なら正常",
"843": "Adobe Flash 関連またはローカルアプリ",
"902": "VMware 管理",
"912": "VMware 管理",
"1080": "SOCKS プロキシ",
"2015": "Caddy 開発サーバー",
"3000": "開発用ローカルサーバーNode 等)— 正常",
"3306": "MySQL",
"3389": "リモートデスクトップRDP",
"5040": "Windows 標準サービス — 正常",
"5354": "mDNSローカルデバイス検索— 正常",
"5357": "Windows ネットワーク検出 — 正常",
"5432": "PostgreSQL",
"5900": "VNC リモートデスクトップ",
"6379": "Redis",
"7680": "Windows Update 配信最適化 — 正常",
"8080": "開発用 HTTP サーバー — 正常",
"8443": "開発用 HTTPS サーバー",
"17500": "Dropbox — インストール済みなら正常",
"17600": "Dropbox — インストール済みなら正常",
"27015": "Steam — インストール済みなら正常",
"27017": "MongoDB",
"33060": "MySQL X Protocol"
};
function isEphemeral(port) {
return parseInt(port, 10) >= 49152;
}
function portNote(port, hint) {
if (PORT_NOTES[String(port)]) return PORT_NOTES[String(port)];
if (hint) return hint;
if (isEphemeral(port)) return "一時ポートWindows 自動割当)— 正常";
return "";
}
function addressNote(addr) {
if (addr === "::" || addr === "0.0.0.0") return "全インターフェースWindows 標準)";
if (addr === "::1" || addr === "127.0.0.1") return "localhost — PC 内だけ、安全";
if (/^100\./.test(addr)) return "Tailscale VPN — 正常";
if (/^192\.168\./.test(addr)) return "LAN — 正常";
if (/^fd7a:115c:/.test(addr)) return "Tailscale VPNIPv6— 正常";
return "";
}
/* ------------------------------------------------------------------ */
/* Render: Summary */
/* ------------------------------------------------------------------ */
function renderSummary(data) {
var uc = data.userContext || {};
var m = data.machine || {};
var uac = data.uac || {};
var net = data.network || {};
var listeners = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
var hkcu = data.registryRun && data.registryRun.HKCU_Run ? len(data.registryRun.HKCU_Run) : 0;
var hklm = data.registryRun && data.registryRun.HKLM_Run ? len(data.registryRun.HKLM_Run) : 0;
var shareSafe = data.meta && data.meta.shareSafe === true;
var items = [
{ label: "PC 名", value: m.Name || "—" },
{ label: "ユーザー", value: uc.UserName || "—" },
{ label: "管理者権限", value: uc.IsAdmin === true ? "あり" : "なし" },
{ label: "UAC EnableLUA", value: uac.EnableLUA != null ? String(uac.EnableLUA) : "—" },
{ label: "LISTEN ポート数", value: String(listeners) },
{ label: ".env 系ファイル", value: envs + " 件" },
{ label: "自動起動 HKCU / HKLM", value: hkcu + " / " + hklm }
];
if (shareSafe) items.unshift({ label: "モード", value: "ShareSafe" });
var html = '<div class="summary-grid">';
items.forEach(function (it) {
var full = (it.label === "PC 名" || it.label === "モード") ? ' full' : '';
html += '<div class="metric' + full + '"><div class="metric-label">' + esc(it.label) + '</div><div class="metric-value">' + esc(it.value) + '</div></div>';
});
html += '</div>';
document.getElementById("summary").innerHTML = html;
}
/* ------------------------------------------------------------------ */
/* Render: Diff (2×2 grid) */
/* ------------------------------------------------------------------ */
function renderDiff(data) {
var df = data.diffFromPrevious;
if (!df) return;
var html = '<div class="diff-card">';
if (df.HasPrevious === false) {
html += '<div class="diff-card-title">前回データなし</div>';
html += '<p style="font-size:0.77rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>';
} else {
html += '<div class="diff-card-title">前回との差分</div>';
var tcp = df.TcpListeners || {};
var envd = df.EnvLikeRelPaths || {};
var hkcu = df.HKCU_Run || {};
var hklm = df.HKLM_Run || {};
html += '<div class="diff-grid">';
[
{ label: "TCP 追加 / 削除", value: (tcp.AddedCount || 0) + " / " + (tcp.RemovedCount || 0) },
{ label: ".env 追加 / 削除", value: (envd.AddedCount || 0) + " / " + (envd.RemovedCount || 0) },
{ label: "Run HKCU +/", value: (hkcu.AddedCount || 0) + " / " + (hkcu.RemovedCount || 0) },
{ label: "Run HKLM +/", value: (hklm.AddedCount || 0) + " / " + (hklm.RemovedCount || 0) }
].forEach(function (r) {
html += '<div class="metric"><div class="metric-label">' + esc(r.label) + '</div><div class="metric-value">' + esc(r.value) + '</div></div>';
});
html += '</div>';
if (df.UserIsAdminChanged) html += '<p class="diff-alert">管理者権限の状態が前回と変わりました。</p>';
if (df.UacEnableLuaChanged) html += '<p class="diff-alert">UAC 設定が前回と変わりました。</p>';
}
html += '</div>';
document.getElementById("diff").innerHTML = html;
}
/* ------------------------------------------------------------------ */
/* Render: Guidance */
/* ------------------------------------------------------------------ */
function buildGuidanceItems(data) {
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
if (list.length) return list;
var uc = data.userContext || {};
var uac = data.uac || {};
var net = data.network || {};
var ssh = data.ssh || {};
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
var out = [];
if (uc.IsAdmin === true) {
out.push({ Level:"warn", Title:"管理者権限が有効です", Body:"この PC は管理者として動作しています。意図的な設定なら問題ありませんが、誤操作やウイルスが PC 全体に影響しやすくなります。", ActionJa:"社内 IT に「管理者権限が必要か確認したい」と相談する。急ぎでなければ次回 IT 棚卸しのタイミングで OK。" });
} else {
out.push({ Level:"ok", Title:"管理者権限は検出されませんでした", Body:"この監査実行時点では管理者権限は確認されていません。" });
}
if (uac.EnableLUA === 0) {
out.push({ Level:"warn", Title:"UAC確認ダイアログが無効です", Body:"アプリが PC 設定を変更しようとしても確認ダイアログが出ない状態です。マルウェアが気づかず実行されやすくなります。", ActionJa:"社内 IT に「UAC を有効にしてほしい」と伝える。" });
}
if (ssh.DirectoryExists && len(ssh.Files) > 0) {
out.push({ Level:"info", Title:"SSH 鍵ファイルがあります", Body:"サーバーへの接続に使う鍵ファイルが ~/.ssh フォルダに存在します。鍵が漏えいするとサーバーに不正アクセスされる可能性があります。", ActionJa:"鍵ファイルのバックアップがあるか確認する。パスフレーズを設定済みか、エンジニアに確認する。" });
}
if (envs > 0) {
out.push({ Level:"info", Title:".env ファイルが " + envs + " 件見つかりました", Body:"API キーやパスワードが書かれることが多いファイル名です。中身はこのツールでは読んでいません。Git に含まれると外部に漏えいします。", ActionJa:"エンジニアに「.env が Git に含まれていないか確認して」と伝える。" });
}
var lc = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
if (lc > 45) {
out.push({ Level:"info", Title:"待ち受けポートが多めです(" + lc + " 件)", Body:"多くのサービスが通信を受け付けています。使っていないアプリが起動したままの可能性があります。", ActionJa:"エンジニアに一覧を見てもらい、不要なアプリがないか確認してもらう。" });
}
return out;
}
function buildGuidanceFilterChips(items) {
var hasWarn = items.some(function(i) { return (i.Level||'').toLowerCase() === 'warn'; });
var hasInfo = items.some(function(i) { return (i.Level||'').toLowerCase() === 'info'; });
var row = document.getElementById("guidance-filter-row");
row.innerHTML = "";
function chip(label, value, extra) {
var b = document.createElement("button");
b.className = "chip" + (guidanceFilter === value ? " active" : "") + (extra ? " " + extra : "");
b.textContent = label;
b.addEventListener("click", function() {
guidanceFilter = value;
applyGuidanceFilter();
row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); });
b.classList.add("active");
});
row.appendChild(b);
}
chip("全て", "all");
if (hasWarn) chip("注意のみ", "warn", "warn-chip");
if (hasInfo) chip("情報のみ", "info", "info-chip");
}
function applyGuidanceFilter() {
var items = guidanceFilter === "all"
? allGuidanceItems
: allGuidanceItems.filter(function(i) { return (i.Level||'info').toLowerCase() === guidanceFilter; });
var html = "";
items.forEach(function (item) {
var level = (item.Level || "info").toLowerCase();
var bc = level === "warn" ? "badge-warn" : (level === "ok" ? "badge-ok" : "badge-info");
var bl = level === "warn" ? "注意" : (level === "ok" ? "問題なし" : "情報");
html += '<div class="card ' + level + '">';
html += '<div class="card-header"><span class="badge ' + bc + '">' + bl + '</span><div class="card-title">' + esc(item.Title || "") + '</div></div>';
html += '<div class="card-body">' + esc(item.Body || "").replace(/\n/g, "<br/>") + '</div>';
var action = item.ActionJa || item.actionJa;
if (action) html += '<div class="action-box"><span class="action-label">対処方法</span>' + esc(action) + '</div>';
html += '</div>';
});
if (!html) html = '<p style="font-size:0.78rem;color:var(--text3);padding:0.4rem 0">該当するチェック項目はありません。</p>';
document.getElementById("guidance").innerHTML = html;
}
function renderGuidance(data) {
allGuidanceItems = buildGuidanceItems(data);
buildGuidanceFilterChips(allGuidanceItems);
applyGuidanceFilter();
}
/* ------------------------------------------------------------------ */
/* Render: Port list */
/* ------------------------------------------------------------------ */
function buildPortFilterChips() {
var scopes = {};
allPortRows.forEach(function(r) { if (r.BindScope) scopes[r.BindScope] = true; });
var row = document.getElementById("port-filter-row");
row.innerHTML = "";
function chip(label, value, extra) {
var b = document.createElement("button");
b.className = "chip" + (portFilter === value ? " active" : "") + (extra ? " " + extra : "");
b.textContent = label;
b.addEventListener("click", function() {
portFilter = value;
applyPortFilter();
row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); });
b.classList.add("active");
});
row.appendChild(b);
}
chip("全て", "all");
if (scopes["allInterfaces"]) chip("全IF のみ", "allInterfaces", "scope-all-chip");
if (scopes["localhost"]) chip("localhost のみ", "localhost", "scope-local-chip");
if (scopes["specific"]) chip("個別アドレス", "specific");
}
function applyPortFilter() {
var rows = portFilter === "all"
? allPortRows
: allPortRows.filter(function(r) { return r.BindScope === portFilter; });
var html = "";
var shown = Math.min(rows.length, 80);
for (var i = 0; i < shown; i++) {
var r = rows[i];
var sc = r.BindScope === "allInterfaces" ? "scope-all" : (r.BindScope === "localhost" ? "scope-localhost" : "scope-specific");
var aN = addressNote(r.Address || "");
var pN = portNote(r.Port, r.WellKnownHint);
html += '<tr>';
html += '<td class="addr-cell">' + esc(r.Address || "") + (aN ? '<div class="hint-line">' + esc(aN) + '</div>' : '') + '</td>';
html += '<td class="port-cell">' + esc(r.Port || "") + '</td>';
html += '<td class="' + sc + '">' + esc(r.BindScope || "") + '</td>';
html += '<td style="font-size:0.73rem;color:var(--text3)">' + esc(pN) + '</td>';
html += '</tr>';
}
var tableHtml = rows.length
? '<table><thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>' + html + '</tbody></table>'
: '<p class="port-empty">該当するポートはありません。</p>';
if (rows.length > 80) tableHtml += '<p style="margin-top:0.4rem;font-size:0.68rem;color:var(--text3)">先頭 80 行表示。全データは生データを参照。</p>';
document.getElementById("listeners").innerHTML = tableHtml;
}
function renderListeners(data) {
var net = data.network || {};
var li = net.ListenerInsights;
allPortRows = (li && li.RowsEnrichedSample) ? li.RowsEnrichedSample : (net.TcpListeners || []);
if (!allPortRows.length) return;
var s = (li && li.Summary) || {};
var legend = '<span style="color:var(--ok-color)">localhost</span> = PC 内のみ。'
+ ' <span style="color:var(--warn-color)">:: / 0.0.0.0</span> = Windows 標準、通常は問題なし。';
if (s.AllInterfacesCount != null) {
legend += '<div class="port-summary-row" style="margin-top:0.4rem">'
+ '<span class="port-summary-item">全IF: <strong>' + s.AllInterfacesCount + '</strong></span>'
+ '<span class="port-summary-item">localhost: <strong>' + s.LocalhostOnlyCount + '</strong></span>'
+ '<span class="port-summary-item">合計: <strong>' + s.TotalListenRows + '</strong></span>'
+ '</div>';
}
document.getElementById("port-legend").innerHTML = legend;
buildPortFilterChips();
applyPortFilter();
}
/* ------------------------------------------------------------------ */
/* Load data */
/* ------------------------------------------------------------------ */
function loadData(data) {
currentData = data;
document.getElementById("upload-wrap").style.display = "none";
document.getElementById("alt-file-btn").style.display = "";
document.getElementById("output").style.display = "flex";
renderSummary(data);
renderDiff(data);
renderGuidance(data);
renderListeners(data);
document.getElementById("raw").textContent = JSON.stringify(data, null, 2);
}
/* ------------------------------------------------------------------ */
/* AI prompt generation */
/* ------------------------------------------------------------------ */
var currentData = null;
function buildAiPrompt(data) {
var uc = data.userContext || {};
var m = data.machine || {};
var uac = data.uac || {};
var shareSafe = data.meta && data.meta.shareSafe;
var items = allGuidanceItems.filter(function(i) {
return (i.Level||'').toLowerCase() !== 'ok';
});
var lines = [];
lines.push("以下は PC 監査ツールPosimai PC Auditの実行結果です。");
lines.push("各チェック項目への対応をお願いします。");
lines.push("");
lines.push("## 対応方針");
lines.push("- PowerShell コマンドや設定変更など、AI が自律的に実行・提案できる対応はそのまま進めてください。");
lines.push("- 以下に該当する場合は、実行前に必ず私に確認してください:");
lines.push(" - システム全体に影響する変更UAC 設定変更・ユーザー権限変更など)");
lines.push(" - 元に戻しにくい操作(ファイル削除・レジストリ変更など)");
lines.push(" - 私の環境や業務の判断が必要なもの(「このソフトは必要か?」など)");
lines.push(" - リスクや副作用が不明確なもの");
lines.push("");
lines.push("## PC 情報");
lines.push("- PC 名: " + (m.Name || "不明"));
lines.push("- ユーザー: " + (uc.UserName || "不明"));
lines.push("- 管理者権限: " + (uc.IsAdmin === true ? "あり" : "なし"));
lines.push("- UAC (EnableLUA): " + (uac.EnableLUA != null ? String(uac.EnableLUA) : "不明"));
if (shareSafe) lines.push("- モード: ShareSafe機微情報は省略済み");
lines.push("");
if (items.length === 0) {
lines.push("## チェック結果");
lines.push("注意・情報レベルの指摘はありませんでした。");
} else {
lines.push("## チェック結果(" + items.length + " 件)");
items.forEach(function(item, idx) {
var level = (item.Level||'info').toLowerCase();
var tag = level === 'warn' ? '[注意]' : '[情報]';
lines.push("");
lines.push("### " + (idx + 1) + ". " + tag + " " + (item.Title || ""));
if (item.Body) lines.push((item.Body || "").replace(/\n/g, " "));
if (item.ActionJa) lines.push("推奨対処のヒント: " + item.ActionJa);
});
}
lines.push("");
lines.push("---");
lines.push("各項目について対応方針を示した上で、自律実行できるものはそのまま進め、判断が必要なものは確認を入れてください。");
return lines.join("\n");
}
document.addEventListener("DOMContentLoaded", function () {
var btn = document.getElementById("copy-prompt-btn");
btn.addEventListener("click", function () {
if (!currentData) return;
var text = buildAiPrompt(currentData);
navigator.clipboard.writeText(text).then(function () {
btn.textContent = "コピーしました";
btn.classList.add("copied");
setTimeout(function () {
btn.textContent = "AI 相談用にコピー";
btn.classList.remove("copied");
}, 2000);
}).catch(function () {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select(); document.execCommand("copy");
document.body.removeChild(ta);
btn.textContent = "コピーしました";
btn.classList.add("copied");
setTimeout(function () {
btn.textContent = "AI 相談用にコピー";
btn.classList.remove("copied");
}, 2000);
});
});
});
/* ------------------------------------------------------------------ */
/* Auto-load */
/* ------------------------------------------------------------------ */
if (window.__AUDIT_PRELOAD__) {
document.addEventListener("DOMContentLoaded", function () {
loadData(window.__AUDIT_PRELOAD__);
});
}
/* ------------------------------------------------------------------ */
/* "別のファイルを読む" */
/* ------------------------------------------------------------------ */
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("alt-file-btn").addEventListener("click", function () {
document.getElementById("upload-wrap").style.display = "flex";
document.getElementById("alt-file-btn").style.display = "none";
document.getElementById("output").style.display = "none";
});
});
/* ------------------------------------------------------------------ */
/* File input */
/* ------------------------------------------------------------------ */
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("f").addEventListener("change", function (ev) {
var file = ev.target.files && ev.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function () {
var data;
try { data = JSON.parse(reader.result); }
catch (e) {
document.getElementById("output").style.display = "flex";
document.getElementById("guidance").innerHTML =
'<div class="card warn"><div class="card-header"><span class="badge badge-warn">エラー</span><div class="card-title">JSON の読み込みに失敗しました</div></div><div class="card-body">pc-audit が出力したファイルか確認してください。</div></div>';
return;
}
loadData(data);
};
reader.readAsText(file, "UTF-8");
});
});
})();
</script>
</body>
</html>