feat(pc-audit): 自動読み込み・フィルタ・スクロール固定枠・差分2カラム

- BAT: JSON をレポートHTMLにインジェクトして一時ファイルで開く(file選択不要)
- report-viewer: AUDIT_PRELOAD 対応・「別ファイルを読む」リンクでフォールバック
- チェック項目:固定高さスクロール枠 + 全て/注意のみ/情報のみフィルタ
- ポート一覧:固定高さスクロール枠 + 全て/全IF/localhost/個別アドレスフィルタ
- thead sticky で列ヘッダが常時表示
- 差分カード:4メトリクスを2×2グリッドに変更

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-04-21 14:39:48 +09:00
parent 7210c8301c
commit 6ce5e0be7d
3 changed files with 355 additions and 136 deletions

View File

@ -0,0 +1,25 @@
@echo off
setlocal
cd /d "%~dp0"
echo.
echo === Posimai PC Audit (portable, share-safe) ===
echo.
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Invoke-PcAudit.ps1" -ShareSafe
if errorlevel 1 (
echo.
echo ERROR: Audit failed. PowerShell 5.1+ is required.
pause
exit /b 1
)
echo.
echo Opening report viewer (share-safe)...
powershell -NoProfile -Command ^
"$j=[System.IO.File]::ReadAllText('%~dp0out\latest.json',[System.Text.Encoding]::UTF8);" ^
"$t=[System.IO.File]::ReadAllText('%~dp0report-viewer.html',[System.Text.Encoding]::UTF8);" ^
"$s='<script>window.__AUDIT_PRELOAD__='+$j+';</script>';" ^
"$o=$t.Replace('<!-- AUDIT_PRELOAD -->',$s);" ^
"$p=[System.IO.Path]::Combine([System.IO.Path]::GetTempPath(),'pc-audit-report.html');" ^
"[System.IO.File]::WriteAllText($p,$o,[System.Text.Encoding]::UTF8);" ^
"Start-Process $p"
echo.
pause

View File

@ -0,0 +1,25 @@
@echo off
setlocal
cd /d "%~dp0"
echo.
echo === Posimai PC Audit (portable) ===
echo.
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Invoke-PcAudit.ps1"
if errorlevel 1 (
echo.
echo ERROR: Audit failed. PowerShell 5.1+ is required.
pause
exit /b 1
)
echo.
echo Opening report viewer...
powershell -NoProfile -Command ^
"$j=[System.IO.File]::ReadAllText('%~dp0out\latest.json',[System.Text.Encoding]::UTF8);" ^
"$t=[System.IO.File]::ReadAllText('%~dp0report-viewer.html',[System.Text.Encoding]::UTF8);" ^
"$s='<script>window.__AUDIT_PRELOAD__='+$j+';</script>';" ^
"$o=$t.Replace('<!-- AUDIT_PRELOAD -->',$s);" ^
"$p=[System.IO.Path]::Combine([System.IO.Path]::GetTempPath(),'pc-audit-report.html');" ^
"[System.IO.File]::WriteAllText($p,$o,[System.Text.Encoding]::UTF8);" ^
"Start-Process $p"
echo.
pause

View File

@ -4,6 +4,7 @@
<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 — Posimai</title> <title>PC Audit — Posimai</title>
<!-- AUDIT_PRELOAD -->
<style> <style>
:root { :root {
--bg: #0D0D0D; --bg: #0D0D0D;
@ -35,124 +36,180 @@
min-height: 100vh; min-height: 100vh;
} }
/* Layout */
.page { padding: 2rem 1.5rem 4rem; } .page { padding: 2rem 1.5rem 4rem; }
.header { margin-bottom: 2rem; } .header { margin-bottom: 1.5rem; display: flex; align-items: baseline; gap: 1.5rem; flex-wrap: wrap; }
.header h1 { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2); } .header h1 { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2); }
.header h1 span { color: var(--accent); } .header h1 span { color: var(--accent); }
.privacy-note { .privacy-note {
margin-top: 0.5rem; font-size: 0.75rem;
font-size: 0.78rem;
color: var(--text3); color: var(--text3);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.4rem;
} }
.privacy-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; } .privacy-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
/* 3-column grid — collapses to 1 col on mobile */ /* Upload — shown when no auto-load, or via "別ファイル" link */
.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 { .upload-area {
background: var(--surface); background: var(--surface);
border: 1px dashed var(--border); border: 1px dashed var(--border);
border-radius: var(--radius); border-radius: var(--radius);
padding: 1.25rem; padding: 1.1rem 1.25rem;
margin-bottom: 1rem; margin-bottom: 1.25rem;
} }
.upload-label { font-size: 0.82rem; font-weight: 600; color: var(--text2); margin-bottom: 0.4rem; display: block; } .upload-label { font-size: 0.82rem; font-weight: 600; color: var(--text2); margin-bottom: 0.4rem; display: block; }
.upload-path { .upload-path {
font-size: 0.75rem; color: var(--text3); background: var(--surface2); font-size: 0.73rem; color: var(--text3); background: var(--surface2);
padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-family: monospace; padding: 0.2rem 0.5rem; border-radius: var(--radius-sm); font-family: monospace;
display: inline-block; margin-bottom: 0.6rem; 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"] { font-size: 0.82rem; color: var(--text2); cursor: pointer; max-width: 100%; }
input[type="file"]::file-selector-button { input[type="file"]::file-selector-button {
background: var(--surface2); color: var(--text); border: 1px solid var(--border); 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; border-radius: var(--radius-sm); padding: 0.3rem 0.75rem; font-size: 0.78rem;
cursor: pointer; margin-right: 0.5rem; margin-bottom: 0.25rem; cursor: pointer; margin-right: 0.5rem;
} }
/* Section label */ /* "別ファイルを読む" secondary link (shown after auto-load) */
.section-title { .alt-file-link {
display: none;
margin-bottom: 1rem;
font-size: 0.75rem;
color: var(--text3);
}
.alt-file-link button {
background: none; border: none; padding: 0;
font-size: 0.75rem; color: var(--accent);
cursor: pointer; text-decoration: underline; text-underline-offset: 2px;
}
/* 3-column grid */
.grid3 {
display: grid;
grid-template-columns: 272px 1fr 1fr;
gap: 1.25rem;
align-items: start;
}
@media (max-width: 900px) {
.grid3 { grid-template-columns: 1fr; }
.page { padding: 1rem 1rem 3rem; }
}
.col-label {
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em; font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--text3); margin: 0 0 0.6rem; text-transform: uppercase; color: var(--text3);
margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border);
} }
/* Metric grid — 2-col, PC名 spans full width */ /* Scroll panes for col 2 and col 3 */
.scroll-pane {
max-height: calc(100vh - 190px);
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 */
.filter-row { display: flex; gap: 0.4rem; margin-bottom: 0.7rem; flex-wrap: wrap; }
.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.5rem; margin-bottom: 0.75rem; } .summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; margin-bottom: 0.75rem; }
.metric { .metric {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius-sm); padding: 0.7rem 0.9rem; border-radius: var(--radius-sm); padding: 0.65rem 0.85rem;
} }
.metric.full { grid-column: 1 / -1; } .metric.full { grid-column: 1 / -1; }
.metric-label { font-size: 0.7rem; color: var(--text3); margin-bottom: 0.15rem; } .metric-label { font-size: 0.68rem; color: var(--text3); margin-bottom: 0.12rem; }
.metric-value { font-size: 0.9rem; font-weight: 600; color: var(--text); word-break: break-all; } .metric-value { font-size: 0.9rem; font-weight: 600; color: var(--text); word-break: break-all; }
/* Diff card */ /* Diff card */
.diff-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; } .diff-card {
.diff-card-title { font-size: 0.8rem; font-weight: 600; color: var(--text2); margin-bottom: 0.6rem; } background: var(--surface); border: 1px solid var(--border);
.diff-col { display: flex; flex-direction: column; gap: 0.4rem; } border-radius: var(--radius); padding: 0.9rem 1rem; margin-top: 0.75rem;
}
.diff-card-title { font-size: 0.78rem; font-weight: 600; color: var(--text2); margin-bottom: 0.6rem; }
.diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; }
.diff-alert { margin-top: 0.5rem; font-size: 0.78rem; color: var(--warn-color); }
/* Cards */ /* Guidance cards */
.card { .card {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 0.6rem; border-radius: var(--radius); padding: 0.9rem 1rem; margin-bottom: 0.5rem;
} }
.card.warn { background: var(--warn-dim); border-color: var(--warn-border); } .card.warn { background: var(--warn-dim); border-color: var(--warn-border); }
.card.ok { background: var(--ok-dim); border-color: var(--ok-border); } .card.ok { background: var(--ok-dim); border-color: var(--ok-border); }
.card.info { background: var(--info-dim); border-color: var(--info-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; } .card-header { display: flex; align-items: flex-start; gap: 0.45rem; margin-bottom: 0.45rem; }
.badge { .badge {
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.06em; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em;
padding: 0.18rem 0.5rem; border-radius: 99px; white-space: nowrap; padding: 0.15rem 0.45rem; border-radius: 99px; white-space: nowrap;
flex-shrink: 0; margin-top: 0.15rem; flex-shrink: 0; margin-top: 0.2rem;
} }
.badge-warn { background: rgba(245,158,11,0.15); color: var(--warn-color); } .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-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); } .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-title { font-size: 0.86rem; font-weight: 600; color: var(--text); line-height: 1.4; }
.card-body { font-size: 0.82rem; color: var(--text2); line-height: 1.65; } .card-body { font-size: 0.8rem; color: var(--text2); line-height: 1.65; }
.action-box { .action-box {
margin-top: 0.75rem; background: var(--surface2); margin-top: 0.65rem; background: var(--surface2);
border-radius: var(--radius-sm); padding: 0.65rem 0.85rem; border-radius: var(--radius-sm); padding: 0.6rem 0.8rem;
font-size: 0.8rem; color: var(--text2); font-size: 0.78rem; color: var(--text2);
} }
.action-label { .action-label {
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.1em; font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em;
color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.25rem; color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.2rem;
} }
/* Port table */ /* Port table */
.table-wrap { overflow-x: auto; } .table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; } table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
thead th { thead th {
position: sticky; top: 0; background: var(--bg);
text-align: left; padding: 0.45rem 0.6rem; text-align: left; padding: 0.45rem 0.6rem;
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.08em; font-size: 0.63rem; font-weight: 700; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--text3); border-bottom: 1px solid var(--border); text-transform: uppercase; color: var(--text3); border-bottom: 1px solid var(--border);
z-index: 1;
} }
tbody tr { border-bottom: 1px solid var(--border); } tbody tr { border-bottom: 1px solid var(--border); }
tbody tr:last-child { border-bottom: none; } tbody tr:last-child { border-bottom: none; }
tbody td { padding: 0.4rem 0.6rem; color: var(--text2); vertical-align: top; } tbody td { padding: 0.38rem 0.6rem; color: var(--text2); vertical-align: top; }
.addr-cell { font-family: monospace; font-size: 0.75rem; color: var(--text3); } .addr-cell { font-family: monospace; font-size: 0.73rem; color: var(--text3); }
.port-cell { font-family: monospace; font-weight: 600; color: var(--text); } .port-cell { font-family: monospace; font-weight: 600; color: var(--text); }
.scope-all { color: var(--warn-color); font-size: 0.78rem; } .scope-all { color: var(--warn-color); font-size: 0.75rem; }
.scope-localhost { color: var(--ok-color); font-size: 0.78rem; } .scope-localhost { color: var(--ok-color); font-size: 0.75rem; }
.scope-specific { color: var(--info-color); font-size: 0.78rem; } .scope-specific { color: var(--info-color); font-size: 0.75rem; }
.hint-line { font-size: 0.7rem; color: var(--text3); margin-top: 0.1rem; } .hint-line { font-size: 0.68rem; color: var(--text3); margin-top: 0.1rem; }
.port-empty { font-size: 0.8rem; color: var(--text3); padding: 0.75rem 0; }
/* Raw */ /* Port legend */
.port-legend {
font-size: 0.75rem; color: var(--text2); margin-bottom: 0.8rem; line-height: 1.7;
}
.port-summary-row {
display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.8rem;
}
.port-summary-item { font-size: 0.75rem; color: var(--text3); }
.port-summary-item strong { color: var(--text); }
/* Raw JSON */
details { details {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.9rem 1.1rem; margin-top: 1.5rem; border-radius: var(--radius); padding: 0.9rem 1.1rem; margin-top: 1.5rem;
@ -160,42 +217,47 @@
summary { cursor: pointer; font-size: 0.78rem; font-weight: 600; color: var(--text3); user-select: none; } 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; } 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; } .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> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<div class="header"> <div class="header">
<h1>PC Audit <span>/</span> Posimai</h1> <h1>PC Audit <span>/</span> Posimai</h1>
<p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作します。ファイルを選択しても外部にデータは一切送信されません。</p> <p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作。データは外部に送信されません。</p>
</div> </div>
<!-- Upload (always shown) --> <div id="upload-area" class="upload-area">
<div class="upload-area">
<label class="upload-label" for="f">レポートファイルを選択</label> <label class="upload-label" for="f">レポートファイルを選択</label>
<div class="upload-path">tools\pc-audit\out\latest.json</div><br/> <div class="upload-path">tools\pc-audit\out\latest.json</div><br/>
<input id="f" type="file" accept=".json,application/json" /> <input id="f" type="file" accept=".json,application/json" />
</div> </div>
<!-- 3-column output --> <div id="alt-file-row" class="alt-file-link">
<button id="alt-file-btn">別のファイルを読む</button>
</div>
<div id="output" style="display:none"> <div id="output" style="display:none">
<div class="grid3"> <div class="grid3">
<!-- Col 1: summary + diff --> <!-- Col 1: summary + diff -->
<div> <div>
<div class="col-label">概要 / 差分</div> <div class="col-label">概要</div>
<div id="summary"></div> <div id="summary"></div>
<div id="diff"></div> <div id="diff"></div>
</div> </div>
<!-- Col 2: check items --> <!-- Col 2: guidance -->
<div> <div>
<div class="col-label">チェック項目</div> <div class="col-label">チェック項目</div>
<div id="guidance"></div> <div class="filter-row" id="guidance-filter-row"></div>
<div class="scroll-pane" id="guidance"></div>
</div> </div>
<!-- Col 3: port list --> <!-- Col 3: ports -->
<div> <div>
<div class="col-label">待ち受けポート一覧</div> <div class="col-label">待ち受けポート一覧</div>
<div id="listeners"></div> <div class="filter-row" id="port-filter-row"></div>
<div class="scroll-pane">
<div id="port-legend"></div>
<div class="table-wrap"><div id="listeners"></div></div>
</div>
</div> </div>
</div> </div>
<details><summary>生データJSON 全体)</summary><pre id="raw"></pre></details> <details><summary>生データJSON 全体)</summary><pre id="raw"></pre></details>
@ -205,6 +267,18 @@
<script> <script>
(function () { (function () {
/* ------------------------------------------------------------------ */
/* State */
/* ------------------------------------------------------------------ */
var allGuidanceItems = [];
var allPortRows = [];
var portSummary = {};
var guidanceFilter = "all"; // "all" | "warn" | "info"
var portFilter = "all"; // "all" | "allInterfaces" | "localhost" | "specific"
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
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);
@ -213,7 +287,7 @@
function len(a) { return Array.isArray(a) ? a.length : 0; } function len(a) { return Array.isArray(a) ? a.length : 0; }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Port descriptions */ /* Port knowledge */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
var PORT_NOTES = { var PORT_NOTES = {
"80": "HTTPWeb サーバー)", "80": "HTTPWeb サーバー)",
@ -224,7 +298,7 @@
"843": "Adobe Flash 関連またはローカルアプリ", "843": "Adobe Flash 関連またはローカルアプリ",
"902": "VMware 管理", "902": "VMware 管理",
"912": "VMware 管理", "912": "VMware 管理",
"1080": "SOCKSプロキシ", "1080": "SOCKS プロキシ",
"2015": "Caddy 開発サーバー", "2015": "Caddy 開発サーバー",
"3000": "開発用ローカルサーバーNode 等)— 正常", "3000": "開発用ローカルサーバーNode 等)— 正常",
"3306": "MySQL", "3306": "MySQL",
@ -245,7 +319,6 @@
"33060": "MySQL X Protocol" "33060": "MySQL X Protocol"
}; };
/* Ephemeral port ranges are dynamically assigned by Windows — always normal */
function isEphemeral(port) { function isEphemeral(port) {
var p = parseInt(port, 10); var p = parseInt(port, 10);
return p >= 49152 && p <= 65535; return p >= 49152 && p <= 65535;
@ -254,13 +327,10 @@
function portNote(port, hint) { function portNote(port, hint) {
if (PORT_NOTES[String(port)]) return PORT_NOTES[String(port)]; if (PORT_NOTES[String(port)]) return PORT_NOTES[String(port)];
if (hint) return hint; if (hint) return hint;
if (isEphemeral(port)) return "一時ポートWindows が一時的に割り当て)— 正常"; if (isEphemeral(port)) return "一時ポートWindows 自動割当)— 正常";
return ""; return "";
} }
/* ------------------------------------------------------------------ */
/* Address notes */
/* ------------------------------------------------------------------ */
function addressNote(addr) { function addressNote(addr) {
if (addr === "::" || addr === "0.0.0.0") return "全インターフェースWindows 標準)"; if (addr === "::" || addr === "0.0.0.0") return "全インターフェースWindows 標準)";
if (addr === "::1" || addr === "127.0.0.1") return "localhost — PC 内だけ、安全"; if (addr === "::1" || addr === "127.0.0.1") return "localhost — PC 内だけ、安全";
@ -306,39 +376,40 @@
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Render: Diff */ /* Render: Diff (2-column grid) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function renderDiff(data) { function renderDiff(data) {
var df = data.diffFromPrevious; var df = data.diffFromPrevious;
if (!df) return; if (!df) return;
var html = '<div class="diff-card" style="margin-top:0.75rem">'; var html = '<div class="diff-card">';
if (df.HasPrevious === false) { if (df.HasPrevious === false) {
html += '<div class="diff-card-title">前回データなし</div><p style="font-size:0.8rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>'; html += '<div class="diff-card-title">前回データなし</div>';
html += '<p style="font-size:0.78rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>';
} else { } else {
html += '<div class="diff-card-title">前回との差分</div>'; html += '<div class="diff-card-title">前回との差分</div>';
var tcp = df.TcpListeners || {}; var tcp = df.TcpListeners || {};
var envd = df.EnvLikeRelPaths || {}; var envd = df.EnvLikeRelPaths || {};
var hkcu = df.HKCU_Run || {}; var hkcu = df.HKCU_Run || {};
var hklm = df.HKLM_Run || {}; var hklm = df.HKLM_Run || {};
html += '<div class="diff-col">'; html += '<div class="diff-grid">';
[ [
{ label: "TCP 追加/削除", value: (tcp.AddedCount || 0) + " / " + (tcp.RemovedCount || 0) }, { label: "TCP 追加 / 削除", value: (tcp.AddedCount || 0) + " / " + (tcp.RemovedCount || 0) },
{ label: ".env 追加/削除", value: (envd.AddedCount || 0) + " / " + (envd.RemovedCount || 0) }, { label: ".env 追加 / 削除", value: (envd.AddedCount || 0) + " / " + (envd.RemovedCount || 0) },
{ label: "Run HKCU +/-", value: (hkcu.AddedCount || 0) + "/" + (hkcu.RemovedCount || 0) }, { label: "Run HKCU +/", value: (hkcu.AddedCount || 0) + " / " + (hkcu.RemovedCount || 0) },
{ label: "Run HKLM +/-", value: (hklm.AddedCount || 0) + "/" + (hklm.RemovedCount || 0) } { label: "Run HKLM +/", value: (hklm.AddedCount || 0) + " / " + (hklm.RemovedCount || 0) }
].forEach(function (r) { ].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 class="metric"><div class="metric-label">' + esc(r.label) + '</div><div class="metric-value">' + esc(r.value) + '</div></div>';
}); });
html += '</div>'; html += '</div>';
if (df.UserIsAdminChanged) html += '<p style="margin-top:0.6rem;font-size:0.8rem;color:var(--warn-color)">管理者権限の状態が前回と変わりました。</p>'; if (df.UserIsAdminChanged) html += '<p class="diff-alert">管理者権限の状態が前回と変わりました。</p>';
if (df.UacEnableLuaChanged) html += '<p style="margin-top:0.4rem;font-size:0.8rem;color:var(--warn-color)">UAC 設定が前回と変わりました。</p>'; if (df.UacEnableLuaChanged) html += '<p class="diff-alert">UAC 設定が前回と変わりました。</p>';
} }
html += '</div>'; html += '</div>';
document.getElementById("diff").innerHTML = html; document.getElementById("diff").innerHTML = html;
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Render: Guidance cards */ /* Render: Guidance (with filter chips) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function buildGuidanceItems(data) { function buildGuidanceItems(data) {
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : []; var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
@ -370,8 +441,33 @@
return out; return out;
} }
function renderGuidance(data) { function buildGuidanceFilter(items) {
var items = buildGuidanceItems(data); 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 makeChip(label, value, extraClass) {
var btn = document.createElement("button");
btn.className = "chip" + (guidanceFilter === value ? " active" : "") + (extraClass ? " " + extraClass : "");
btn.textContent = label;
btn.addEventListener("click", function() {
guidanceFilter = value;
applyGuidanceFilter();
row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); });
btn.classList.add("active");
});
row.appendChild(btn);
}
makeChip("全て", "all");
if (hasWarn) makeChip("注意のみ", "warn", "warn-chip");
if (hasInfo) makeChip("情報のみ", "info", "info-chip");
}
function applyGuidanceFilter() {
var items = guidanceFilter === "all"
? allGuidanceItems
: allGuidanceItems.filter(function(i) { return (i.Level||'info').toLowerCase() === guidanceFilter; });
var html = ""; var html = "";
items.forEach(function (item) { items.forEach(function (item) {
var level = (item.Level || "info").toLowerCase(); var level = (item.Level || "info").toLowerCase();
@ -386,37 +482,49 @@
} }
html += '</div>'; html += '</div>';
}); });
if (!html) html = '<p style="font-size:0.8rem;color:var(--text3);padding:0.5rem 0">該当するチェック項目はありません。</p>';
document.getElementById("guidance").innerHTML = html; document.getElementById("guidance").innerHTML = html;
} }
/* ------------------------------------------------------------------ */ function renderGuidance(data) {
/* Render: Port list */ allGuidanceItems = buildGuidanceItems(data);
/* ------------------------------------------------------------------ */ buildGuidanceFilter(allGuidanceItems);
function renderListeners(data) { applyGuidanceFilter();
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">'; /* Render: Port list (with filter chips) */
html += '<span style="color:var(--ok-color)">localhost</span> は自分の PC 内だけ、安全。'; /* ------------------------------------------------------------------ */
html += '<br><span style="color:var(--warn-color)">:: / 0.0.0.0</span> は Windows 標準で出ることが多く、通常は問題なし。'; function buildPortFilter() {
html += '<br>日本語説明がないポートは <strong style="color:var(--text)">一時ポートWindows が自動割当)</strong>で、接続ごとに番号が変わる正常な動作です。'; var scopes = {};
html += '</p>'; allPortRows.forEach(function(r) { if (r.BindScope) scopes[r.BindScope] = true; });
var row = document.getElementById("port-filter-row");
if (s.AllInterfacesCount != null) { row.innerHTML = "";
html += '<div style="display:flex;gap:1rem;margin-bottom:0.85rem;flex-wrap:wrap">'; function makeChip(label, value, extraClass) {
html += '<span style="font-size:0.78rem;color:var(--text3)">全IF: <strong style="color:var(--text)">' + s.AllInterfacesCount + '</strong></span>'; var btn = document.createElement("button");
html += '<span style="font-size:0.78rem;color:var(--text3)">localhost: <strong style="color:var(--text)">' + s.LocalhostOnlyCount + '</strong></span>'; btn.className = "chip" + (portFilter === value ? " active" : "") + (extraClass ? " " + extraClass : "");
html += '<span style="font-size:0.78rem;color:var(--text3)">合計: <strong style="color:var(--text)">' + s.TotalListenRows + '</strong></span>'; btn.textContent = label;
html += '</div>'; btn.addEventListener("click", function() {
portFilter = value;
applyPortFilter();
row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); });
btn.classList.add("active");
});
row.appendChild(btn);
} }
makeChip("全て", "all");
if (scopes["allInterfaces"]) makeChip("全IF のみ", "allInterfaces", "scope-all-chip");
if (scopes["localhost"]) makeChip("localhost のみ", "localhost", "scope-local-chip");
if (scopes["specific"]) makeChip("個別アドレス", "specific");
}
html += '<div class="table-wrap"><table>'; function applyPortFilter() {
html += '<thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>'; var rows = portFilter === "all"
var shown = Math.min(rows.length, 60); ? 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++) { for (var i = 0; i < shown; i++) {
var r = rows[i]; var r = rows[i];
var sc = r.BindScope === "allInterfaces" ? "scope-all" : (r.BindScope === "localhost" ? "scope-localhost" : "scope-specific"); var sc = r.BindScope === "allInterfaces" ? "scope-all" : (r.BindScope === "localhost" ? "scope-localhost" : "scope-specific");
@ -426,39 +534,100 @@
html += '<td class="addr-cell">' + esc(r.Address || "") + (aN ? '<div class="hint-line">' + esc(aN) + '</div>' : '') + '</td>'; 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="port-cell">' + esc(r.Port || "") + '</td>';
html += '<td class="' + sc + '">' + esc(r.BindScope || "") + '</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 += '<td style="font-size:0.75rem;color:var(--text3)">' + esc(pN) + '</td>';
html += '</tr>'; html += '</tr>';
} }
html += '</tbody></table></div>';
if (rows.length > 60) { var tableHtml = rows.length
html += '<p style="margin-top:0.5rem;font-size:0.72rem;color:var(--text3)">先頭 60 行のみ表示。全データは下の生データを参照。</p>'; ? '<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.5rem;font-size:0.7rem;color:var(--text3)">先頭 80 行表示。全データは下の生データを参照。</p>';
} }
document.getElementById("listeners").innerHTML = html; 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) || {};
portSummary = s;
var legend = '<div class="port-legend">';
legend += '<span style="color:var(--ok-color)">localhost</span> = PC 内のみ、安全。';
legend += ' <span style="color:var(--warn-color)">:: / 0.0.0.0</span> = Windows 標準、通常は問題なし。';
legend += '</div>';
if (s.AllInterfacesCount != null) {
legend += '<div class="port-summary-row">';
legend += '<span class="port-summary-item">全IF: <strong>' + s.AllInterfacesCount + '</strong></span>';
legend += '<span class="port-summary-item">localhost: <strong>' + s.LocalhostOnlyCount + '</strong></span>';
legend += '<span class="port-summary-item">合計: <strong>' + s.TotalListenRows + '</strong></span>';
legend += '</div>';
}
document.getElementById("port-legend").innerHTML = legend;
buildPortFilter();
applyPortFilter();
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* File load */ /* Load data */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
document.getElementById("f").addEventListener("change", function (ev) { function loadData(data) {
var file = ev.target.files && ev.target.files[0]; document.getElementById("output").style.display = "";
if (!file) return; renderSummary(data);
var reader = new FileReader(); renderDiff(data);
reader.onload = function () { renderGuidance(data);
var data; renderListeners(data);
try { data = JSON.parse(reader.result); } document.getElementById("raw").textContent = JSON.stringify(data, null, 2);
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; /* Auto-load (injected via BAT) */
} /* ------------------------------------------------------------------ */
document.getElementById("output").style.display = ""; if (window.__AUDIT_PRELOAD__) {
renderSummary(data); document.addEventListener("DOMContentLoaded", function () {
renderDiff(data); document.getElementById("upload-area").style.display = "none";
renderGuidance(data); document.getElementById("alt-file-row").style.display = "block";
renderListeners(data); loadData(window.__AUDIT_PRELOAD__);
document.getElementById("raw").textContent = JSON.stringify(data, null, 2); });
}; }
reader.readAsText(file, "UTF-8");
/* ------------------------------------------------------------------ */
/* "別のファイルを読む" button */
/* ------------------------------------------------------------------ */
document.addEventListener("DOMContentLoaded", function () {
document.getElementById("alt-file-btn").addEventListener("click", function () {
document.getElementById("upload-area").style.display = "";
document.getElementById("alt-file-row").style.display = "none";
});
});
/* ------------------------------------------------------------------ */
/* File input handler */
/* ------------------------------------------------------------------ */
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("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;
}
loadData(data);
};
reader.readAsText(file, "UTF-8");
});
}); });
})(); })();
</script> </script>