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

352 lines
15 KiB
HTML
Raw Normal View History

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>PC audit viewerローカル専用</title>
<style>
:root { font-family: system-ui, sans-serif; line-height: 1.5; color: #1a1a1a; background: #f5f5f5; }
body { max-width: 52rem; margin: 0 auto; padding: 1rem 1.25rem 3rem; }
h1 { font-size: 1.25rem; }
h2 { font-size: 1.05rem; margin: 0 0 0.35rem; }
.box { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }
.warn { border-left: 4px solid #c45c00; }
.ok { border-left: 4px solid #2d6a4f; }
.info { border-left: 4px solid #1d4ed8; }
.muted { color: #555; font-size: 0.9rem; }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.5rem 1rem; }
.k { font-size: 0.8rem; color: #555; }
.v { font-weight: 600; word-break: break-all; }
pre { overflow: auto; font-size: 0.8rem; background: #fafafa; padding: 0.75rem; border-radius: 6px; border: 1px solid #eee; }
label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
input[type="file"] { margin-bottom: 0.5rem; }
summary { cursor: pointer; font-weight: 600; }
</style>
</head>
<body>
<h1>PC audit viewer</h1>
<p class="muted">この HTML はブラウザ内だけで動きます。ファイルを選ぶまで外部にデータは送りません。Web 公開はしていません。</p>
<div class="box">
<label for="f">JSON レポートを選択</label>
<p class="muted" style="margin:0 0 0.5rem;font-size:0.85rem;">
場所: <code style="background:#f0f0f0;padding:2px 6px;border-radius:4px;">tools\pc-audit\out\latest.json</code>
&nbsp;run-audit.bat を実行した後に作られます)
</p>
<input id="f" type="file" accept=".json,application/json" />
</div>
<div id="disclaimer" class="box muted" hidden></div>
<div id="summary"></div>
<div id="insights"></div>
<div id="guidance"></div>
<details class="box">
<summary>生データJSON 全体)</summary>
<pre id="raw"></pre>
</details>
<script>
(function () {
function esc(s) {
var d = document.createElement("div");
d.textContent = s == null ? "" : String(s);
return d.innerHTML;
}
function levelClass(level) {
if (level === "warn") return "warn";
if (level === "ok") return "ok";
return "info";
}
function labelJa(level) {
if (level === "warn") return "注意";
if (level === "ok") return "問題なさそう";
return "情報";
}
function len(a) {
return Array.isArray(a) ? a.length : 0;
}
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 el = document.getElementById("summary");
el.innerHTML =
'<div class="box"><h2>取得結果のあらまし</h2><p class="muted">数値は「このレポートに載った件数」です。良い/悪いの判定そのものではありません。</p>' +
'<div class="grid">' +
(shareSafe
? '<div><div class="k">共有モード</div><div class="v">ShareSafe.claude のパス・キーワード省略)</div></div>'
: "") +
'<div><div class="k">PC 名</div><div class="v">' +
esc(m.Name) +
"</div></div>" +
'<div><div class="k">ユーザー</div><div class="v">' +
esc(uc.UserName) +
"</div></div>" +
'<div><div class="k">管理者ロール</div><div class="v">' +
(uc.IsAdmin === true ? "検出あり(影響範囲が広がりやすい)" : "検出なし") +
"</div></div>" +
'<div><div class="k">UAC EnableLUA</div><div class="v">' +
esc(uac.EnableLUA) +
"</div></div>" +
'<div><div class="k">LISTEN ポート行数</div><div class="v">' +
listeners +
"</div></div>" +
'<div><div class="k">env 系ファイル名</div><div class="v">' +
envs +
" 件</div></div>" +
'<div><div class="k">Run 起動項目</div><div class="v">HKCU ' +
hkcu +
" / HKLM " +
hklm +
"</div></div>" +
"</div></div>";
}
function renderInsights(data) {
var el = document.getElementById("insights");
var parts = [];
var df = data.diffFromPrevious;
if (df) {
parts.push("<div class=\"box\"><h2>前回実行からの差分</h2>");
if (df.HasPrevious === false) {
parts.push("<p class=\"muted\">" + esc(df.NoteJa || "") + "</p>");
} else {
parts.push("<p class=\"muted\">" + esc(df.NoteJa || "") + "</p>");
parts.push("<div class=\"grid\">");
parts.push(
"<div><div class=\"k\">比較元(前回 UTC</div><div class=\"v\">" +
esc(df.PreviousGeneratedAtUtc || "") +
"</div></div>"
);
var tcp = df.TcpListeners || {};
parts.push(
"<div><div class=\"k\">TCP 追加/削除</div><div class=\"v\">" +
(tcp.AddedCount || 0) +
" / " +
(tcp.RemovedCount || 0) +
"</div></div>"
);
var envd = df.EnvLikeRelPaths || {};
parts.push(
"<div><div class=\"k\">env パス 追加/削除</div><div class=\"v\">" +
(envd.AddedCount || 0) +
" / " +
(envd.RemovedCount || 0) +
"</div></div>"
);
var hkcu = df.HKCU_Run || {};
var hklm = df.HKLM_Run || {};
parts.push(
"<div><div class=\"k\">Run 名 HKCU / HKLM</div><div class=\"v\">" +
(hkcu.AddedCount || 0) +
"/" +
(hkcu.RemovedCount || 0) +
" ・ " +
(hklm.AddedCount || 0) +
"/" +
(hklm.RemovedCount || 0) +
"</div></div>"
);
parts.push("</div>");
if (df.UserIsAdminChanged) {
parts.push("<p class=\"muted\">管理者ロールの検出が前回と異なります。</p>");
}
if (df.UacEnableLuaChanged) {
parts.push("<p class=\"muted\">UAC EnableLUA が前回と異なります。</p>");
}
}
parts.push("</div>");
}
var li = data.network && data.network.ListenerInsights;
if (li && li.Summary) {
var s = li.Summary;
parts.push("<div class=\"box\"><h2>LISTEN の見方(自動付与)</h2>");
parts.push("<p class=\"muted\">BindScope は待ち受けアドレスの種類の目安です。WellKnownHint はポートの一般的用途の目安です。</p>");
parts.push("<div class=\"grid\">");
parts.push(
"<div><div class=\"k\">全IF / localhost / 合計行</div><div class=\"v\">" +
(s.AllInterfacesCount != null ? s.AllInterfacesCount : "?") +
" / " +
(s.LocalhostOnlyCount != null ? s.LocalhostOnlyCount : "?") +
" / " +
(s.TotalListenRows != null ? s.TotalListenRows : "?") +
"</div></div>"
);
parts.push("</div>");
var rows = li.RowsEnrichedSample || [];
if (rows.length) {
parts.push("<table style=\"width:100%;font-size:0.85rem;border-collapse:collapse;margin-top:0.75rem\">");
parts.push("<thead><tr><th align=\"left\">Address</th><th align=\"left\">Port</th><th align=\"left\">BindScope</th><th align=\"left\">目安</th></tr></thead><tbody>");
for (var i = 0; i < Math.min(rows.length, 40); i++) {
var r = rows[i];
parts.push(
"<tr><td>" +
esc(r.Address) +
"</td><td>" +
esc(r.Port) +
"</td><td>" +
esc(r.BindScope) +
"</td><td>" +
esc(r.WellKnownHint || "") +
"</td></tr>"
);
}
parts.push("</tbody></table>");
if (rows.length > 40) {
parts.push("<p class=\"muted\">先頭 40 行のみ表示。残りは下の生データを参照。</p>");
}
}
parts.push("</div>");
}
el.innerHTML = parts.join("");
}
function buildFallbackGuidance(data) {
var out = [];
out.push({
Level: "info",
Title: "この JSON には日本語解説guidanceJaがありません",
Body:
"古い版の Invoke-PcAudit.ps1 で作られたファイルの可能性があります。リポジトリの最新版で npm run audit:pc を再実行し、新しい latest.json を選び直してください。それまでの間、下記はデータから機械的に作った説明です。",
});
var uc = data.userContext || {};
if (uc.IsAdmin === true) {
out.push({
Level: "warn",
Title: "管理者ロールが検出されています",
Body: "自動ツールや操作ミスで PC 全体に影響が及びやすい状態です。意図した使い方か、エンジニアに確認してください。",
});
} else {
out.push({
Level: "ok",
Title: "管理者ロールは検出されませんでした",
Body: "この監査時点での検出結果です。常に正しいとは限りません。",
});
}
var uac = data.uac || {};
if (uac.EnableLUA === 0) {
out.push({
Level: "warn",
Title: "UACEnableLUAが 0 です",
Body: "ユーザーアカウント制御が弱い可能性があります。社内ポリシーと照らしてください。",
});
}
var net = data.network || {};
var lc = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
if (lc > 45) {
out.push({
Level: "info",
Title: "待ち受けポートが多めです",
Body: "件数は " + lc + " 行です。不要なサービスがないか、エンジニアに一覧を見てもらう価値があります。",
});
}
var ssh = data.ssh || {};
if (ssh.DirectoryExists && len(ssh.Files) > 0) {
out.push({
Level: "info",
Title: "SSH 用フォルダにファイルがあります",
Body: "鍵や設定があるようです。中身はこのレポートでは読んでいません。紛失・漏えい対策を社内ルールで確認してください。",
});
}
var git = data.git || {};
if (git.GitAvailable && git.CredentialHelper) {
out.push({
Level: "info",
Title: "Git credential.helper が設定されています",
Body: "認証情報の保存先が OS や別ツールに関わります。会社の開発ポリシーと矛盾がないか必要なら確認してください。",
});
}
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
if (envs > 0) {
out.push({
Level: "info",
Title: "環境変数ファイルらしき名前が " + envs + " 件あります",
Body: "API キー等が入りがちです。Git に含めていないか、エンジニアに確認してください。",
});
}
return out;
}
function appendGuidanceCard(container, item) {
var div = document.createElement("div");
div.className = "box " + levelClass(item.Level);
div.innerHTML =
"<h2>[" +
esc(labelJa(item.Level)) +
"] " +
esc(item.Title) +
"</h2><p>" +
esc(item.Body).replace(/\n/g, "<br/>") +
"</p>";
container.appendChild(div);
}
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 text = reader.result;
var data;
try {
data = JSON.parse(text);
} catch (e) {
document.getElementById("summary").innerHTML = "";
document.getElementById("insights").innerHTML = "";
document.getElementById("guidance").innerHTML =
'<div class="box warn">JSON の読み取りに失敗しました。pc-audit の出力ファイルか確認してください。</div>';
document.getElementById("raw").textContent = "";
return;
}
var disc = document.getElementById("disclaimer");
if (data.meta && data.meta.guidanceDisclaimerJa) {
disc.hidden = false;
disc.textContent = data.meta.guidanceDisclaimerJa;
} else {
disc.hidden = false;
disc.textContent =
"このレポートは補助的な説明であり、セキュリティ診断やコンプライアンス判定に代わるものではありません。";
}
renderSummary(data);
renderInsights(data);
var g = document.getElementById("guidance");
g.innerHTML = "";
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
var usedFallback = false;
if (!list.length) {
usedFallback = true;
list = buildFallbackGuidance(data);
}
if (usedFallback) {
var note = document.createElement("div");
note.className = "box warn";
note.innerHTML =
"<h2>補足</h2><p>上の「解説がありません」は、<strong>古い JSON</strong>を開いているときに出ます。<strong>Invoke-PcAudit.ps1 を最新にして監査をやり直す</strong>と、PowerShell 側で書いた詳しい日本語解説が JSON に入ります。</p>";
g.appendChild(note);
}
list.forEach(function (item) {
appendGuidanceCard(g, item);
});
document.getElementById("raw").textContent = JSON.stringify(data, null, 2);
};
reader.readAsText(file, "UTF-8");
});
})();
</script>
</body>
</html>