feat(pc-audit): report-viewer を Posimai ダークテーマに全面改修
- 左ボーダーラインを廃止、badge + カード枠色でレベルを表現 - 非エンジニア向け説明・対処方法(actionJa)を各カードに追加 - ポート一覧に IP アドレス・ポート番号の日本語解説を追加 - プライバシー説明をヘッダーに常時表示 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
4360f9090f
commit
a17084dfd8
|
|
@ -1,351 +1,448 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ja">
|
<html lang="ja" data-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>PC audit viewer(ローカル専用)</title>
|
<title>PC Audit — Posimai</title>
|
||||||
<style>
|
<style>
|
||||||
:root { font-family: system-ui, sans-serif; line-height: 1.5; color: #1a1a1a; background: #f5f5f5; }
|
:root {
|
||||||
body { max-width: 52rem; margin: 0 auto; padding: 1rem 1.25rem 3rem; }
|
--bg: #0D0D0D;
|
||||||
h1 { font-size: 1.25rem; }
|
--surface: #1A1A1A;
|
||||||
h2 { font-size: 1.05rem; margin: 0 0 0.35rem; }
|
--surface2: #252525;
|
||||||
.box { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }
|
--border: #2D2D2D;
|
||||||
.warn { border-left: 4px solid #c45c00; }
|
--text: #F3F4F6;
|
||||||
.ok { border-left: 4px solid #2d6a4f; }
|
--text2: #9CA3AF;
|
||||||
.info { border-left: 4px solid #1d4ed8; }
|
--text3: #6B7280;
|
||||||
.muted { color: #555; font-size: 0.9rem; }
|
--accent: #6EE7B7;
|
||||||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.5rem 1rem; }
|
--accent-dim: rgba(110,231,183,0.08);
|
||||||
.k { font-size: 0.8rem; color: #555; }
|
--accent-border: rgba(110,231,183,0.2);
|
||||||
.v { font-weight: 600; word-break: break-all; }
|
--warn-color: #F59E0B;
|
||||||
pre { overflow: auto; font-size: 0.8rem; background: #fafafa; padding: 0.75rem; border-radius: 6px; border: 1px solid #eee; }
|
--warn-dim: rgba(245,158,11,0.08);
|
||||||
label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
|
--warn-border: rgba(245,158,11,0.2);
|
||||||
input[type="file"] { margin-bottom: 0.5rem; }
|
--ok-color: #6EE7B7;
|
||||||
summary { cursor: pointer; font-weight: 600; }
|
--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;
|
||||||
|
}
|
||||||
|
.page { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.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.8rem;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Upload area */
|
||||||
|
.upload-area {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px dashed var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.75rem 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
.upload-label { font-size: 0.85rem; font-weight: 600; color: var(--text2); margin-bottom: 0.5rem; display: block; }
|
||||||
|
.upload-path {
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text3);
|
||||||
|
background: var(--surface2);
|
||||||
|
padding: 0.3rem 0.6rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-family: monospace;
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
input[type="file"] {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text2);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type="file"]::file-selector-button {
|
||||||
|
background: var(--surface2);
|
||||||
|
color: var(--text);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.35rem 0.9rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section label */
|
||||||
|
.section-title {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text3);
|
||||||
|
margin: 2rem 0 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary grid */
|
||||||
|
.summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
.metric {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.85rem 1rem;
|
||||||
|
}
|
||||||
|
.metric-label { font-size: 0.72rem; color: var(--text3); margin-bottom: 0.2rem; }
|
||||||
|
.metric-value { font-size: 0.95rem; 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: 1.25rem 1.5rem;
|
||||||
|
}
|
||||||
|
.diff-card-title { font-size: 0.82rem; font-weight: 600; color: var(--text2); margin-bottom: 0.75rem; }
|
||||||
|
.diff-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr)); gap: 0.5rem; }
|
||||||
|
|
||||||
|
/* Guidance cards — no left border */
|
||||||
|
.card {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
.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.6rem; margin-bottom: 0.6rem; }
|
||||||
|
.badge {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
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.9rem; font-weight: 600; color: var(--text); line-height: 1.4; }
|
||||||
|
.card-body { font-size: 0.84rem; color: var(--text2); line-height: 1.65; }
|
||||||
|
|
||||||
|
.action-box {
|
||||||
|
margin-top: 0.85rem;
|
||||||
|
background: var(--surface2);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text2);
|
||||||
|
}
|
||||||
|
.action-label {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Listener table */
|
||||||
|
.table-wrap { overflow-x: auto; margin-top: 0.75rem; }
|
||||||
|
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
|
||||||
|
thead th {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
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.45rem 0.75rem; color: var(--text2); vertical-align: top; }
|
||||||
|
.addr-cell { font-family: monospace; font-size: 0.78rem; color: var(--text3); }
|
||||||
|
.port-cell { font-family: monospace; font-weight: 600; color: var(--text); }
|
||||||
|
.scope-all { color: var(--warn-color); font-size: 0.8rem; }
|
||||||
|
.scope-localhost { color: var(--ok-color); font-size: 0.8rem; }
|
||||||
|
.scope-specific { color: var(--info-color); font-size: 0.8rem; }
|
||||||
|
.hint-line { font-size: 0.73rem; color: var(--text3); margin-top: 0.1rem; }
|
||||||
|
|
||||||
|
/* Raw */
|
||||||
|
details {
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
summary { cursor: pointer; font-size: 0.8rem; font-weight: 600; color: var(--text3); user-select: none; }
|
||||||
|
pre { margin-top: 0.75rem; font-size: 0.72rem; color: var(--text3); overflow: auto; max-height: 20rem; line-height: 1.5; }
|
||||||
|
|
||||||
|
.disclaimer { font-size: 0.75rem; color: var(--text3); border-top: 1px solid var(--border); padding-top: 1rem; margin-top: 2rem; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>PC audit viewer</h1>
|
<div class="page">
|
||||||
<p class="muted">この HTML はブラウザ内だけで動きます。ファイルを選ぶまで外部にデータは送りません。Web 公開はしていません。</p>
|
<div class="header">
|
||||||
|
<h1>PC Audit <span>/</span> Posimai</h1>
|
||||||
|
<p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作します。ファイルを選択しても外部にデータは一切送信されません。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="box">
|
<div class="upload-area">
|
||||||
<label for="f">JSON レポートを選択</label>
|
<label class="upload-label" for="f">レポートファイルを選択</label>
|
||||||
<p class="muted" style="margin:0 0 0.5rem;font-size:0.85rem;">
|
<div class="upload-path">tools\pc-audit\out\latest.json</div><br/>
|
||||||
場所: <code style="background:#f0f0f0;padding:2px 6px;border-radius:4px;">tools\pc-audit\out\latest.json</code>
|
<input id="f" type="file" accept=".json,application/json" />
|
||||||
(run-audit.bat を実行した後に作られます)
|
</div>
|
||||||
</p>
|
|
||||||
<input id="f" type="file" accept=".json,application/json" />
|
<div id="output" style="display:none">
|
||||||
|
<div id="summary"></div>
|
||||||
|
<div id="diff"></div>
|
||||||
|
<div id="guidance"></div>
|
||||||
|
<div id="listeners"></div>
|
||||||
|
<details><summary>生データ(JSON 全体)</summary><pre id="raw"></pre></details>
|
||||||
|
<p class="disclaimer">このレポートは補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
|
||||||
|
</div>
|
||||||
</div>
|
</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>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
var d = document.createElement("div");
|
var d = document.createElement("div");
|
||||||
d.textContent = s == null ? "" : String(s);
|
d.textContent = s == null ? "" : String(s);
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
}
|
}
|
||||||
|
function len(a) { return Array.isArray(a) ? a.length : 0; }
|
||||||
|
|
||||||
function levelClass(level) {
|
var PORT_NOTES = {
|
||||||
if (level === "warn") return "warn";
|
"135": "Windows 標準(RPC)— 正常",
|
||||||
if (level === "ok") return "ok";
|
"139": "Windows ファイル共有(NetBIOS)— LAN 内なら正常",
|
||||||
return "info";
|
"445": "Windows ファイル共有(SMB)— LAN では一般的",
|
||||||
}
|
"843": "Adobe Flash 関連またはローカルアプリ",
|
||||||
|
"2015": "開発用ローカルサーバー(Caddy 等)",
|
||||||
|
"5040": "Windows 標準サービス",
|
||||||
|
"5354": "mDNS(ローカルデバイス検索)— 正常",
|
||||||
|
"5357": "Windows ネットワーク検出 — 正常",
|
||||||
|
"7680": "Windows Update 配信最適化 — 正常",
|
||||||
|
"17500": "Dropbox — インストール済みなら正常",
|
||||||
|
"17600": "Dropbox — インストール済みなら正常",
|
||||||
|
"27015": "Steam ゲームクライアント — 正常"
|
||||||
|
};
|
||||||
|
|
||||||
function labelJa(level) {
|
function addressNote(addr) {
|
||||||
if (level === "warn") return "注意";
|
if (addr === "::" || addr === "0.0.0.0") return "全インターフェース(Windows 標準)";
|
||||||
if (level === "ok") return "問題なさそう";
|
if (addr === "::1" || addr === "127.0.0.1") return "localhost — 自分の PC 内だけ、安全";
|
||||||
return "情報";
|
if (/^100\./.test(addr)) return "Tailscale VPN — インストール済みなら正常";
|
||||||
}
|
if (/^192\.168\./.test(addr)) return "自宅 / 社内 LAN — 正常";
|
||||||
|
if (/^fd7a:115c:/.test(addr)) return "Tailscale VPN(IPv6)— 正常";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function len(a) {
|
function renderSummary(data) {
|
||||||
return Array.isArray(a) ? a.length : 0;
|
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;
|
||||||
|
|
||||||
function renderSummary(data) {
|
var items = [
|
||||||
var uc = data.userContext || {};
|
{ label: "PC 名", value: m.Name || "—" },
|
||||||
var m = data.machine || {};
|
{ label: "ユーザー", value: uc.UserName || "—" },
|
||||||
var uac = data.uac || {};
|
{ label: "管理者権限", value: uc.IsAdmin === true ? "あり" : "なし" },
|
||||||
var net = data.network || {};
|
{ label: "UAC EnableLUA", value: uac.EnableLUA != null ? String(uac.EnableLUA) : "—" },
|
||||||
var listeners = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
|
{ label: "LISTEN ポート数", value: String(listeners) },
|
||||||
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
|
{ label: ".env 系ファイル", value: envs + " 件" },
|
||||||
var hkcu = data.registryRun && data.registryRun.HKCU_Run ? len(data.registryRun.HKCU_Run) : 0;
|
{ label: "自動起動 HKCU / HKLM", value: hkcu + " / " + hklm }
|
||||||
var hklm = data.registryRun && data.registryRun.HKLM_Run ? len(data.registryRun.HKLM_Run) : 0;
|
];
|
||||||
var shareSafe = data.meta && data.meta.shareSafe === true;
|
if (shareSafe) items.unshift({ label: "モード", value: "ShareSafe" });
|
||||||
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 html = '<div class="section-title">概要</div><div class="summary-grid">';
|
||||||
var el = document.getElementById("insights");
|
items.forEach(function (it) {
|
||||||
var parts = [];
|
html += '<div class="metric"><div class="metric-label">' + esc(it.label) + '</div><div class="metric-value">' + esc(it.value) + '</div></div>';
|
||||||
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: "UAC(EnableLUA)が 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");
|
|
||||||
});
|
});
|
||||||
})();
|
html += '</div>';
|
||||||
|
document.getElementById("summary").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDiff(data) {
|
||||||
|
var df = data.diffFromPrevious;
|
||||||
|
if (!df) return;
|
||||||
|
var html = '<div class="section-title">前回との差分</div><div class="diff-card">';
|
||||||
|
if (df.HasPrevious === false) {
|
||||||
|
html += '<div class="diff-card-title">前回データなし</div><p style="font-size:0.82rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>';
|
||||||
|
} else {
|
||||||
|
html += '<div class="diff-card-title">前回(' + esc(df.PreviousGeneratedAtUtc || "") + ')との比較</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 style="margin-top:0.75rem;font-size:0.82rem;color:var(--warn-color)">管理者権限の状態が前回と変わりました。</p>';
|
||||||
|
if (df.UacEnableLuaChanged) html += '<p style="margin-top:0.5rem;font-size:0.82rem;color:var(--warn-color)">UAC 設定が前回と変わりました。</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
document.getElementById("diff").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 を有効にしてほしい」と伝える。設定変更は IT に依頼するのが安全です。" });
|
||||||
|
}
|
||||||
|
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:"PC 上で多くのサービスが通信を受け付けています。使っていないアプリが起動したままになっている可能性があります。", ActionJa:"エンジニアに一覧を見てもらい、不要なアプリがないか確認してもらう。" });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGuidance(data) {
|
||||||
|
var items = buildGuidanceItems(data);
|
||||||
|
var html = '<div class="section-title">チェック項目</div>';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = '<div class="section-title">待ち受けポート一覧</div><div class="card">';
|
||||||
|
html += '<p class="card-body" style="margin-bottom:0.85rem">';
|
||||||
|
html += '「待ち受けポート」は PC 上で外部からの通信を受け付けているサービスの一覧です。<br>';
|
||||||
|
html += '<span style="color:var(--ok-color)">localhost</span> のものは自分の PC 内だけで通信するため安全です。';
|
||||||
|
html += '<span style="color:var(--warn-color);margin-left:0.5rem">::(コロン2つ) / 0.0.0.0</span> は Windows 標準で出ることが多く、通常は問題ありません。';
|
||||||
|
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.8rem;color:var(--text3)">全インターフェース: <strong style="color:var(--text)">' + s.AllInterfacesCount + '</strong></span>';
|
||||||
|
html += '<span style="font-size:0.8rem;color:var(--text3)">localhost のみ: <strong style="color:var(--text)">' + s.LocalhostOnlyCount + '</strong></span>';
|
||||||
|
html += '<span style="font-size:0.8rem;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, 50);
|
||||||
|
for (var i = 0; i < shown; i++) {
|
||||||
|
var r = rows[i];
|
||||||
|
var scopeClass = r.BindScope === "allInterfaces" ? "scope-all" : (r.BindScope === "localhost" ? "scope-localhost" : "scope-specific");
|
||||||
|
var addrNote = addressNote(r.Address || "");
|
||||||
|
var portNote = PORT_NOTES[String(r.Port)] || (r.WellKnownHint || "");
|
||||||
|
html += '<tr>';
|
||||||
|
html += '<td class="addr-cell">' + esc(r.Address || "") + (addrNote ? '<div class="hint-line">' + esc(addrNote) + '</div>' : '') + '</td>';
|
||||||
|
html += '<td class="port-cell">' + esc(r.Port || "") + '</td>';
|
||||||
|
html += '<td class="' + scopeClass + '">' + esc(r.BindScope || "") + '</td>';
|
||||||
|
html += '<td class="hint-line" style="font-size:0.8rem;color:var(--text3)">' + esc(portNote) + '</td>';
|
||||||
|
html += '</tr>';
|
||||||
|
}
|
||||||
|
html += '</tbody></table></div>';
|
||||||
|
if (rows.length > 50) {
|
||||||
|
html += '<p style="margin-top:0.5rem;font-size:0.75rem;color:var(--text3)">先頭 50 行のみ表示。全データは下の生データを参照。</p>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
document.getElementById("listeners").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue