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

467 lines
24 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>
<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;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
}
/* Layout */
.page { padding: 2rem 1.5rem 4rem; }
.header { margin-bottom: 2rem; }
.header h1 { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2); }
.header h1 span { color: var(--accent); }
.privacy-note {
margin-top: 0.5rem;
font-size: 0.78rem;
color: var(--text3);
display: flex;
align-items: center;
gap: 0.5rem;
}
.privacy-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
/* 3-column grid — collapses to 1 col on mobile */
.grid3 {
display: grid;
grid-template-columns: 280px 1fr 1fr;
gap: 1.25rem;
align-items: start;
}
@media (max-width: 860px) {
.grid3 { grid-template-columns: 1fr; }
.page { padding: 1rem 1rem 3rem; }
}
/* Upload area */
.upload-area {
background: var(--surface);
border: 1px dashed var(--border);
border-radius: var(--radius);
padding: 1.25rem;
margin-bottom: 1rem;
}
.upload-label { font-size: 0.82rem; font-weight: 600; color: var(--text2); margin-bottom: 0.4rem; display: block; }
.upload-path {
font-size: 0.75rem; color: var(--text3); background: var(--surface2);
padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-family: monospace;
display: inline-block; margin-bottom: 0.6rem;
}
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; margin-bottom: 0.25rem;
}
/* Section label */
.section-title {
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--text3); margin: 0 0 0.6rem;
}
/* Metric grid — 2-col, PC名 spans full width */
.summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.75rem; }
.metric {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 0.7rem 0.9rem;
}
.metric.full { grid-column: 1 / -1; }
.metric-label { font-size: 0.7rem; color: var(--text3); margin-bottom: 0.15rem; }
.metric-value { font-size: 0.9rem; 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: 1rem 1.1rem; }
.diff-card-title { font-size: 0.8rem; font-weight: 600; color: var(--text2); margin-bottom: 0.6rem; }
.diff-col { display: flex; flex-direction: column; gap: 0.4rem; }
/* Cards */
.card {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 0.6rem;
}
.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.5rem; margin-bottom: 0.5rem; }
.badge {
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.06em;
padding: 0.18rem 0.5rem; border-radius: 99px; white-space: nowrap;
flex-shrink: 0; margin-top: 0.15rem;
}
.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.88rem; font-weight: 600; color: var(--text); line-height: 1.4; }
.card-body { font-size: 0.82rem; color: var(--text2); line-height: 1.65; }
.action-box {
margin-top: 0.75rem; background: var(--surface2);
border-radius: var(--radius-sm); padding: 0.65rem 0.85rem;
font-size: 0.8rem; color: var(--text2);
}
.action-label {
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.1em;
color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.25rem;
}
/* Port table */
.table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
thead th {
text-align: left; padding: 0.45rem 0.6rem;
font-size: 0.65rem; 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.4rem 0.6rem; color: var(--text2); vertical-align: top; }
.addr-cell { font-family: monospace; font-size: 0.75rem; color: var(--text3); }
.port-cell { font-family: monospace; font-weight: 600; color: var(--text); }
.scope-all { color: var(--warn-color); font-size: 0.78rem; }
.scope-localhost { color: var(--ok-color); font-size: 0.78rem; }
.scope-specific { color: var(--info-color); font-size: 0.78rem; }
.hint-line { font-size: 0.7rem; color: var(--text3); margin-top: 0.1rem; }
/* Raw */
details {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.9rem 1.1rem; margin-top: 1.5rem;
}
summary { cursor: pointer; font-size: 0.78rem; font-weight: 600; color: var(--text3); user-select: none; }
pre { margin-top: 0.6rem; font-size: 0.7rem; color: var(--text3); overflow: auto; max-height: 18rem; line-height: 1.5; }
.disclaimer { font-size: 0.73rem; color: var(--text3); border-top: 1px solid var(--border); padding-top: 0.9rem; margin-top: 1.5rem; }
.col-label { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text3); margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
</style>
</head>
<body>
<div class="page">
<div class="header">
<h1>PC Audit <span>/</span> Posimai</h1>
<p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作します。ファイルを選択しても外部にデータは一切送信されません。</p>
</div>
<!-- Upload (always shown) -->
<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>
<!-- 3-column output -->
<div id="output" style="display:none">
<div class="grid3">
<!-- Col 1: summary + diff -->
<div>
<div class="col-label">概要 / 差分</div>
<div id="summary"></div>
<div id="diff"></div>
</div>
<!-- Col 2: check items -->
<div>
<div class="col-label">チェック項目</div>
<div id="guidance"></div>
</div>
<!-- Col 3: port list -->
<div>
<div class="col-label">待ち受けポート一覧</div>
<div id="listeners"></div>
</div>
</div>
<details><summary>生データJSON 全体)</summary><pre id="raw"></pre></details>
<p class="disclaimer">このレポートは補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
</div>
</div>
<script>
(function () {
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 descriptions */
/* ------------------------------------------------------------------ */
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"
};
/* Ephemeral port ranges are dynamically assigned by Windows — always normal */
function isEphemeral(port) {
var p = parseInt(port, 10);
return p >= 49152 && p <= 65535;
}
function portNote(port, hint) {
if (PORT_NOTES[String(port)]) return PORT_NOTES[String(port)];
if (hint) return hint;
if (isEphemeral(port)) return "一時ポートWindows が一時的に割り当て)— 正常";
return "";
}
/* ------------------------------------------------------------------ */
/* Address notes */
/* ------------------------------------------------------------------ */
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— 正常";
if (/^::1$/.test(addr)) return "localhostIPv6— 安全";
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 */
/* ------------------------------------------------------------------ */
function renderDiff(data) {
var df = data.diffFromPrevious;
if (!df) return;
var html = '<div class="diff-card" style="margin-top:0.75rem">';
if (df.HasPrevious === false) {
html += '<div class="diff-card-title">前回データなし</div><p style="font-size:0.8rem;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-col">';
[
{ 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 style="margin-top:0.6rem;font-size:0.8rem;color:var(--warn-color)">管理者権限の状態が前回と変わりました。</p>';
if (df.UacEnableLuaChanged) html += '<p style="margin-top:0.4rem;font-size:0.8rem;color:var(--warn-color)">UAC 設定が前回と変わりました。</p>';
}
html += '</div>';
document.getElementById("diff").innerHTML = html;
}
/* ------------------------------------------------------------------ */
/* Render: Guidance cards */
/* ------------------------------------------------------------------ */
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 renderGuidance(data) {
var items = buildGuidanceItems(data);
var html = "";
items.forEach(function (item) {
var level = (item.Level || "info").toLowerCase();
var badgeClass = level === "warn" ? "badge-warn" : (level === "ok" ? "badge-ok" : "badge-info");
var badgeLabel = level === "warn" ? "注意" : (level === "ok" ? "問題なし" : "情報");
html += '<div class="card ' + level + '">';
html += '<div class="card-header"><span class="badge ' + badgeClass + '">' + badgeLabel + '</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>';
});
document.getElementById("guidance").innerHTML = html;
}
/* ------------------------------------------------------------------ */
/* Render: Port list */
/* ------------------------------------------------------------------ */
function renderListeners(data) {
var net = data.network || {};
var li = net.ListenerInsights;
var rows = (li && li.RowsEnrichedSample) ? li.RowsEnrichedSample : (net.TcpListeners || []);
if (!rows.length) return;
var s = (li && li.Summary) || {};
var html = '';
html += '<p style="font-size:0.8rem;color:var(--text2);margin-bottom:0.85rem;line-height:1.6">';
html += '<span style="color:var(--ok-color)">localhost</span> は自分の PC 内だけ、安全。';
html += '<br><span style="color:var(--warn-color)">:: / 0.0.0.0</span> は Windows 標準で出ることが多く、通常は問題なし。';
html += '<br>日本語説明がないポートは <strong style="color:var(--text)">一時ポートWindows が自動割当)</strong>で、接続ごとに番号が変わる正常な動作です。';
html += '</p>';
if (s.AllInterfacesCount != null) {
html += '<div style="display:flex;gap:1rem;margin-bottom:0.85rem;flex-wrap:wrap">';
html += '<span style="font-size:0.78rem;color:var(--text3)">全IF: <strong style="color:var(--text)">' + s.AllInterfacesCount + '</strong></span>';
html += '<span style="font-size:0.78rem;color:var(--text3)">localhost: <strong style="color:var(--text)">' + s.LocalhostOnlyCount + '</strong></span>';
html += '<span style="font-size:0.78rem;color:var(--text3)">合計: <strong style="color:var(--text)">' + s.TotalListenRows + '</strong></span>';
html += '</div>';
}
html += '<div class="table-wrap"><table>';
html += '<thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>';
var shown = Math.min(rows.length, 60);
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 class="hint-line" style="font-size:0.78rem;color:var(--text3)">' + esc(pN) + '</td>';
html += '</tr>';
}
html += '</tbody></table></div>';
if (rows.length > 60) {
html += '<p style="margin-top:0.5rem;font-size:0.72rem;color:var(--text3)">先頭 60 行のみ表示。全データは下の生データを参照。</p>';
}
document.getElementById("listeners").innerHTML = html;
}
/* ------------------------------------------------------------------ */
/* File load */
/* ------------------------------------------------------------------ */
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("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>';
document.getElementById("output").style.display = "";
return;
}
document.getElementById("output").style.display = "";
renderSummary(data);
renderDiff(data);
renderGuidance(data);
renderListeners(data);
document.getElementById("raw").textContent = JSON.stringify(data, null, 2);
};
reader.readAsText(file, "UTF-8");
});
})();
</script>
</body>
</html>