feat(pc-audit): ビューポートフィット・全BAT自動読み込み対応

- report-viewer: html/body overflow:hidden で 100vh 固定、3カラムがビューポートを埋める
- col 1(概要)・col 2(チェック)・col 3(ポート)が独立してスクロール
- 生データ・免責は col 1 のスクロール枠内に移動(グリッド外に出ない)
- ファイル読込後は upload-wrap を非表示にして「別ファイルを読む」に切替
- run-audit.bat / run-audit-share.bat も JSON インジェクション方式に統一

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-04-21 15:59:07 +09:00
parent 6ce5e0be7d
commit 04483c8744
5 changed files with 309 additions and 203 deletions

View File

@ -1,4 +1,4 @@
#Requires -Version 5.1 #Requires -Version 5.1
<# <#
.SYNOPSIS .SYNOPSIS
Read-only local PC audit (developer / AI-tooling focused). No writes except report output. Read-only local PC audit (developer / AI-tooling focused). No writes except report output.

View File

@ -0,0 +1,33 @@
# Posimai PC Auditポータブル
Windows PC 向けの読み取り専用監査ツールです。Node.js やリポジトリのクローンは不要ですPowerShell 5.1 が標準搭載の環境を想定)。
## 含まれるファイル
| ファイル | 説明 |
|----------|------|
| `Invoke-PcAudit.ps1` | 監査本体 |
| `report-viewer.html` | 生成 JSON をブラウザで確認するビューア |
| `Run-Portable.bat` | 通常モードで実行し、終了後にビューアを開く |
| `Run-Portable-Share.bat` | 共有向け(機微を抑えた)レポートで同様に実行 |
## 手順
1. ZIP を任意のフォルダに展開する(例: `C:\Tools\PosimaiPcAudit`)。
2. `Run-Portable.bat` または `Run-Portable-Share.bat` をダブルクリックする。
3. 同じフォルダに `out` が作成され、`latest.json` などが出力される。
4. 開いた `report-viewer.html``out\latest.json` を選択する。
## スキャン対象について
ポータブル配置時は、プロジェクト用のファイル走査の既定ルートが **ユーザープロファイル**`%USERPROFILE%`になります。リポジトリ直下を走査したい場合は、PowerShell から次のように指定できます。
```powershell
cd C:\展開先
powershell -NoProfile -ExecutionPolicy Bypass -File .\Invoke-PcAudit.ps1 -ProjectRoot "D:\your\repo\root"
```
## 注意
- 管理者権限なしでも多くの情報は取得できますが、一部項目は権限により空になることがあります。
- 本 ZIP は PWA ではなく、ローカルで動かすオフライン寄りのツールです。

View File

@ -26,43 +26,79 @@
--info-border: rgba(96,165,250,0.2); --info-border: rgba(96,165,250,0.2);
--radius: 12px; --radius: 12px;
--radius-sm: 8px; --radius-sm: 8px;
--header-h: 44px;
} }
* { box-sizing: border-box; margin: 0; padding: 0; } * { box-sizing: border-box; margin: 0; padding: 0; }
body {
html, body {
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--bg); background: var(--bg);
color: var(--text); color: var(--text);
line-height: 1.6; line-height: 1.6;
min-height: 100vh;
} }
.page { padding: 2rem 1.5rem 4rem; } /* ── Page shell ── */
.page {
height: 100vh;
display: flex;
flex-direction: column;
padding: 0.9rem 1.5rem 0.75rem;
gap: 0.75rem;
}
.header { margin-bottom: 1.5rem; display: flex; align-items: baseline; gap: 1.5rem; flex-wrap: wrap; } /* ── Header bar ── */
.header h1 { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2); } .header {
.header h1 span { color: var(--accent); } flex-shrink: 0;
.privacy-note {
font-size: 0.75rem;
color: var(--text3);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.4rem; gap: 1rem;
flex-wrap: wrap;
min-height: var(--header-h);
} }
.privacy-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--accent); flex-shrink: 0; } .header h1 {
font-size: 1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2);
}
.header h1 span { color: var(--accent); }
.header-meta {
display: flex; align-items: center; gap: 0.75rem; flex: 1; flex-wrap: wrap;
}
.privacy-note {
font-size: 0.72rem; color: var(--text3);
display: flex; align-items: center; gap: 0.35rem;
}
.privacy-dot {
display: inline-block; width: 5px; height: 5px;
border-radius: 50%; background: var(--accent); flex-shrink: 0;
}
.alt-file-btn {
background: none; border: none; padding: 0; margin-left: auto;
font-size: 0.72rem; color: var(--text3); cursor: pointer;
text-decoration: underline; text-underline-offset: 2px;
}
.alt-file-btn:hover { color: var(--accent); }
/* Upload — shown when no auto-load, or via "別ファイル" link */ /* ── Upload state (no data) ── */
.upload-area { .upload-wrap {
background: var(--surface); flex: 1;
border: 1px dashed var(--border); display: flex;
border-radius: var(--radius); align-items: center;
padding: 1.1rem 1.25rem; justify-content: center;
margin-bottom: 1.25rem; }
.upload-area {
background: var(--surface); border: 1px dashed var(--border);
border-radius: var(--radius); padding: 2rem 2.5rem;
width: 100%; max-width: 440px;
}
.upload-label {
font-size: 0.85rem; font-weight: 600; color: var(--text2);
margin-bottom: 0.5rem; display: block;
} }
.upload-label { font-size: 0.82rem; font-weight: 600; color: var(--text2); margin-bottom: 0.4rem; display: block; }
.upload-path { .upload-path {
font-size: 0.73rem; color: var(--text3); background: var(--surface2); font-size: 0.73rem; color: var(--text3); background: var(--surface2);
padding: 0.2rem 0.5rem; border-radius: var(--radius-sm); font-family: monospace; padding: 0.2rem 0.5rem; border-radius: var(--radius-sm);
display: inline-block; margin-bottom: 0.6rem; font-family: monospace; display: inline-block; margin-bottom: 0.75rem;
} }
input[type="file"] { font-size: 0.82rem; color: var(--text2); cursor: pointer; max-width: 100%; } input[type="file"] { font-size: 0.82rem; color: var(--text2); cursor: pointer; max-width: 100%; }
input[type="file"]::file-selector-button { input[type="file"]::file-selector-button {
@ -71,40 +107,53 @@
cursor: pointer; margin-right: 0.5rem; cursor: pointer; margin-right: 0.5rem;
} }
/* "別ファイルを読む" secondary link (shown after auto-load) */ /* ── 3-column grid (data state) ── */
.alt-file-link { #output {
flex: 1;
min-height: 0;
display: none; display: none;
margin-bottom: 1rem; flex-direction: column;
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 { .grid3 {
flex: 1;
min-height: 0;
display: grid; display: grid;
grid-template-columns: 272px 1fr 1fr; grid-template-columns: 264px 1fr 1fr;
gap: 1.25rem; gap: 1rem;
align-items: start;
} }
@media (max-width: 900px) { @media (max-width: 900px) {
html, body { overflow: auto; height: auto; }
.page { height: auto; overflow: visible; padding: 1rem; }
#output { display: flex !important; flex-direction: column; }
.grid3 { grid-template-columns: 1fr; } .grid3 { grid-template-columns: 1fr; }
.page { padding: 1rem 1rem 3rem; } .grid-col { max-height: 70vh; }
}
/* ── Grid column (flex column, child scroll-pane fills it) ── */
.grid-col {
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
} }
.col-label { .col-label {
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em; flex-shrink: 0;
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.1em;
text-transform: uppercase; color: var(--text3); text-transform: uppercase; color: var(--text3);
margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); margin-bottom: 0.6rem; padding-bottom: 0.45rem;
border-bottom: 1px solid var(--border);
} }
/* Scroll panes for col 2 and col 3 */ .filter-row {
flex-shrink: 0;
display: flex; gap: 0.4rem; margin-bottom: 0.6rem; flex-wrap: wrap;
}
/* ── Scrollable area inside each column ── */
.scroll-pane { .scroll-pane {
max-height: calc(100vh - 190px); flex: 1;
min-height: 0;
overflow-y: auto; overflow-y: auto;
padding-right: 3px; padding-right: 3px;
scrollbar-width: thin; scrollbar-width: thin;
@ -114,8 +163,7 @@
.scroll-pane::-webkit-scrollbar-track { background: transparent; } .scroll-pane::-webkit-scrollbar-track { background: transparent; }
.scroll-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; } .scroll-pane::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
/* Filter chips */ /* ── Filter chips ── */
.filter-row { display: flex; gap: 0.4rem; margin-bottom: 0.7rem; flex-wrap: wrap; }
.chip { .chip {
font-size: 0.68rem; font-weight: 600; padding: 0.18rem 0.55rem; font-size: 0.68rem; font-weight: 600; padding: 0.18rem 0.55rem;
border-radius: 99px; border: 1px solid var(--border); border-radius: 99px; border: 1px solid var(--border);
@ -125,144 +173,173 @@
} }
.chip:hover { border-color: var(--text2); color: var(--text2); } .chip:hover { border-color: var(--text2); color: var(--text2); }
.chip.active { background: var(--surface2); color: var(--text); border-color: var(--accent); } .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.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.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-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); } .chip.scope-local-chip.active { border-color: var(--ok-color); color: var(--ok-color); }
/* Summary metrics */ /* ── 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.45rem; margin-bottom: 0.65rem;
}
.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.65rem 0.85rem; border-radius: var(--radius-sm); padding: 0.6rem 0.8rem;
} }
.metric.full { grid-column: 1 / -1; } .metric.full { grid-column: 1 / -1; }
.metric-label { font-size: 0.68rem; color: var(--text3); margin-bottom: 0.12rem; } .metric-label { font-size: 0.67rem; color: var(--text3); margin-bottom: 0.1rem; }
.metric-value { font-size: 0.9rem; font-weight: 600; color: var(--text); word-break: break-all; } .metric-value { font-size: 0.88rem; font-weight: 600; color: var(--text); word-break: break-all; }
/* Diff card */ /* ── Diff card ── */
.diff-card { .diff-card {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.9rem 1rem; margin-top: 0.75rem; border-radius: var(--radius); padding: 0.8rem 0.9rem; margin-bottom: 0.65rem;
} }
.diff-card-title { font-size: 0.78rem; font-weight: 600; color: var(--text2); margin-bottom: 0.6rem; } .diff-card-title { font-size: 0.77rem; font-weight: 600; color: var(--text2); margin-bottom: 0.55rem; }
.diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.5rem; } .diff-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.45rem; }
.diff-alert { margin-top: 0.5rem; font-size: 0.78rem; color: var(--warn-color); } .diff-alert { margin-top: 0.45rem; font-size: 0.77rem; color: var(--warn-color); }
/* Guidance cards */ /* ── Raw data (lives in col 1 scroll pane) ── */
.raw-details {
background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.8rem 1rem;
margin-bottom: 0.65rem;
}
.raw-details summary {
cursor: pointer; font-size: 0.75rem; font-weight: 600;
color: var(--text3); user-select: none;
}
.raw-details pre {
margin-top: 0.5rem; font-size: 0.68rem; color: var(--text3);
overflow: auto; max-height: 14rem; line-height: 1.5;
}
.disclaimer {
font-size: 0.7rem; color: var(--text3);
border-top: 1px solid var(--border); padding-top: 0.75rem; margin-bottom: 0.5rem;
}
/* ── Guidance cards ── */
.card { .card {
background: var(--surface); border: 1px solid var(--border); background: var(--surface); border: 1px solid var(--border);
border-radius: var(--radius); padding: 0.9rem 1rem; margin-bottom: 0.5rem; border-radius: var(--radius); padding: 0.85rem 0.95rem; margin-bottom: 0.45rem;
} }
.card.warn { background: var(--warn-dim); border-color: var(--warn-border); } .card.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.45rem; margin-bottom: 0.45rem; } .card-header { display: flex; align-items: flex-start; gap: 0.4rem; margin-bottom: 0.4rem; }
.badge { .badge {
font-size: 0.62rem; font-weight: 700; letter-spacing: 0.06em; font-size: 0.61rem; font-weight: 700; letter-spacing: 0.06em;
padding: 0.15rem 0.45rem; border-radius: 99px; white-space: nowrap; padding: 0.14rem 0.42rem; border-radius: 99px; white-space: nowrap;
flex-shrink: 0; margin-top: 0.2rem; flex-shrink: 0; margin-top: 0.22rem;
} }
.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.86rem; font-weight: 600; color: var(--text); line-height: 1.4; } .card-title { font-size: 0.85rem; font-weight: 600; color: var(--text); line-height: 1.4; }
.card-body { font-size: 0.8rem; color: var(--text2); line-height: 1.65; } .card-body { font-size: 0.79rem; color: var(--text2); line-height: 1.65; }
.action-box { .action-box {
margin-top: 0.65rem; background: var(--surface2); margin-top: 0.6rem; background: var(--surface2);
border-radius: var(--radius-sm); padding: 0.6rem 0.8rem; border-radius: var(--radius-sm); padding: 0.55rem 0.75rem;
font-size: 0.78rem; color: var(--text2); font-size: 0.77rem; color: var(--text2);
} }
.action-label { .action-label {
font-size: 0.62rem; font-weight: 700; letter-spacing: 0.1em; font-size: 0.61rem; font-weight: 700; letter-spacing: 0.1em;
color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.2rem; color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.18rem;
} }
/* Port table */ /* ── Port list ── */
.port-legend {
flex-shrink: 0;
font-size: 0.72rem; color: var(--text2);
margin-bottom: 0.55rem; line-height: 1.7;
}
.port-summary-row {
display: flex; gap: 0.9rem; flex-wrap: wrap; margin-bottom: 0.55rem;
}
.port-summary-item { font-size: 0.72rem; color: var(--text3); }
.port-summary-item strong { color: var(--text); }
.table-wrap { overflow-x: auto; } .table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; } table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
thead th { thead th {
position: sticky; top: 0; background: var(--bg); position: sticky; top: 0; z-index: 1;
text-align: left; padding: 0.45rem 0.6rem; background: var(--bg);
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.08em; text-align: left; padding: 0.42rem 0.55rem;
text-transform: uppercase; color: var(--text3); border-bottom: 1px solid var(--border); font-size: 0.62rem; font-weight: 700; letter-spacing: 0.08em;
z-index: 1; text-transform: uppercase; color: var(--text3);
border-bottom: 1px solid var(--border);
} }
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.38rem 0.6rem; color: var(--text2); vertical-align: top; } tbody td { padding: 0.36rem 0.55rem; color: var(--text2); vertical-align: top; }
.addr-cell { font-family: monospace; font-size: 0.73rem; color: var(--text3); } .addr-cell { font-family: monospace; font-size: 0.71rem; 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.75rem; } .scope-all { color: var(--warn-color); font-size: 0.73rem; }
.scope-localhost { color: var(--ok-color); font-size: 0.75rem; } .scope-localhost { color: var(--ok-color); font-size: 0.73rem; }
.scope-specific { color: var(--info-color); font-size: 0.75rem; } .scope-specific { color: var(--info-color); font-size: 0.73rem; }
.hint-line { font-size: 0.68rem; color: var(--text3); margin-top: 0.1rem; } .hint-line { font-size: 0.66rem; color: var(--text3); margin-top: 0.08rem; }
.port-empty { font-size: 0.8rem; color: var(--text3); padding: 0.75rem 0; } .port-empty { font-size: 0.78rem; color: var(--text3); padding: 0.6rem 0; }
/* 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 {
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; }
</style> </style>
</head> </head>
<body> <body>
<div class="page"> <div class="page">
<!-- ── Header ── -->
<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> <div class="header-meta">
<p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作。データは外部に送信されません。</p>
<button id="alt-file-btn" class="alt-file-btn" style="display:none">別のファイルを読む</button>
</div>
</div> </div>
<div id="upload-area" class="upload-area"> <!-- ── Upload state ── -->
<label class="upload-label" for="f">レポートファイルを選択</label> <div id="upload-wrap" class="upload-wrap">
<div class="upload-path">tools\pc-audit\out\latest.json</div><br/> <div class="upload-area">
<input id="f" type="file" accept=".json,application/json" /> <label class="upload-label" for="f">レポートファイルを選択</label>
<div class="upload-path">tools\pc-audit\out\latest.json</div><br/>
<input id="f" type="file" accept=".json,application/json" />
</div>
</div> </div>
<div id="alt-file-row" class="alt-file-link"> <!-- ── Data state: 3-column grid ── -->
<button id="alt-file-btn">別のファイルを読む</button> <div id="output">
</div>
<div id="output" style="display:none">
<div class="grid3"> <div class="grid3">
<!-- Col 1: summary + diff -->
<div> <!-- Col 1: Summary + Diff + Raw -->
<div class="grid-col">
<div class="col-label">概要</div> <div class="col-label">概要</div>
<div id="summary"></div> <div class="scroll-pane">
<div id="diff"></div> <div id="summary"></div>
<div id="diff"></div>
<details class="raw-details">
<summary>生データJSON</summary>
<pre id="raw"></pre>
</details>
<p class="disclaimer">補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
</div>
</div> </div>
<!-- Col 2: guidance -->
<div> <!-- Col 2: Guidance -->
<div class="grid-col">
<div class="col-label">チェック項目</div> <div class="col-label">チェック項目</div>
<div class="filter-row" id="guidance-filter-row"></div> <div class="filter-row" id="guidance-filter-row"></div>
<div class="scroll-pane" id="guidance"></div> <div class="scroll-pane" id="guidance"></div>
</div> </div>
<!-- Col 3: ports -->
<div> <!-- Col 3: Ports -->
<div class="grid-col">
<div class="col-label">待ち受けポート一覧</div> <div class="col-label">待ち受けポート一覧</div>
<div class="filter-row" id="port-filter-row"></div> <div class="filter-row" id="port-filter-row"></div>
<div id="port-legend" class="port-legend"></div>
<div class="scroll-pane"> <div class="scroll-pane">
<div id="port-legend"></div>
<div class="table-wrap"><div id="listeners"></div></div> <div class="table-wrap"><div id="listeners"></div></div>
</div> </div>
</div> </div>
</div> </div>
<details><summary>生データJSON 全体)</summary><pre id="raw"></pre></details>
<p class="disclaimer">このレポートは補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
</div> </div>
</div> </div>
<script> <script>
@ -272,9 +349,8 @@
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
var allGuidanceItems = []; var allGuidanceItems = [];
var allPortRows = []; var allPortRows = [];
var portSummary = {}; var guidanceFilter = "all";
var guidanceFilter = "all"; // "all" | "warn" | "info" var portFilter = "all";
var portFilter = "all"; // "all" | "allInterfaces" | "localhost" | "specific"
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Helpers */ /* Helpers */
@ -320,8 +396,7 @@
}; };
function isEphemeral(port) { function isEphemeral(port) {
var p = parseInt(port, 10); return parseInt(port, 10) >= 49152;
return p >= 49152 && p <= 65535;
} }
function portNote(port, hint) { function portNote(port, hint) {
@ -334,10 +409,9 @@
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 内だけ、安全";
if (/^100\./.test(addr)) return "Tailscale VPN — 正常"; if (/^100\./.test(addr)) return "Tailscale VPN — 正常";
if (/^192\.168\./.test(addr)) return "LAN — 正常"; if (/^192\.168\./.test(addr)) return "LAN — 正常";
if (/^fd7a:115c:/.test(addr)) return "Tailscale VPNIPv6— 正常"; if (/^fd7a:115c:/.test(addr)) return "Tailscale VPNIPv6— 正常";
if (/^::1$/.test(addr)) return "localhostIPv6— 安全";
return ""; return "";
} }
@ -376,7 +450,7 @@
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Render: Diff (2-column grid) */ /* Render: Diff (2×2 grid) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function renderDiff(data) { function renderDiff(data) {
var df = data.diffFromPrevious; var df = data.diffFromPrevious;
@ -384,7 +458,7 @@
var html = '<div class="diff-card">'; var html = '<div class="diff-card">';
if (df.HasPrevious === false) { if (df.HasPrevious === false) {
html += '<div class="diff-card-title">前回データなし</div>'; html += '<div class="diff-card-title">前回データなし</div>';
html += '<p style="font-size:0.78rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>'; html += '<p style="font-size:0.77rem;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 || {};
@ -409,7 +483,7 @@
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Render: Guidance (with filter chips) */ /* Render: Guidance */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function buildGuidanceItems(data) { function buildGuidanceItems(data) {
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : []; var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
@ -429,7 +503,7 @@
out.push({ Level:"warn", Title:"UAC確認ダイアログが無効です", Body:"アプリが PC 設定を変更しようとしても確認ダイアログが出ない状態です。マルウェアが気づかず実行されやすくなります。", ActionJa:"社内 IT に「UAC を有効にしてほしい」と伝える。" }); out.push({ Level:"warn", Title:"UAC確認ダイアログが無効です", Body:"アプリが PC 設定を変更しようとしても確認ダイアログが出ない状態です。マルウェアが気づかず実行されやすくなります。", ActionJa:"社内 IT に「UAC を有効にしてほしい」と伝える。" });
} }
if (ssh.DirectoryExists && len(ssh.Files) > 0) { if (ssh.DirectoryExists && len(ssh.Files) > 0) {
out.push({ Level:"info", Title:"SSH 鍵ファイルがあります", Body:"サーバーへの接続に使う鍵ファイルが ~/.ssh フォルダに存在します。鍵が漏えいするとサーバーに不正アクセスされる可能性があります。", ActionJa:"鍵ファイルのバックアップがあるか確認する。パスフレーズ(パスワード)を設定済みか、エンジニアに確認する。" }); out.push({ Level:"info", Title:"SSH 鍵ファイルがあります", Body:"サーバーへの接続に使う鍵ファイルが ~/.ssh フォルダに存在します。鍵が漏えいするとサーバーに不正アクセスされる可能性があります。", ActionJa:"鍵ファイルのバックアップがあるか確認する。パスフレーズを設定済みか、エンジニアに確認する。" });
} }
if (envs > 0) { if (envs > 0) {
out.push({ Level:"info", Title:".env ファイルが " + envs + " 件見つかりました", Body:"API キーやパスワードが書かれることが多いファイル名です。中身はこのツールでは読んでいません。Git に含まれると外部に漏えいします。", ActionJa:"エンジニアに「.env が Git に含まれていないか確認して」と伝える。" }); out.push({ Level:"info", Title:".env ファイルが " + envs + " 件見つかりました", Body:"API キーやパスワードが書かれることが多いファイル名です。中身はこのツールでは読んでいません。Git に含まれると外部に漏えいします。", ActionJa:"エンジニアに「.env が Git に含まれていないか確認して」と伝える。" });
@ -441,88 +515,84 @@
return out; return out;
} }
function buildGuidanceFilter(items) { function buildGuidanceFilterChips(items) {
var hasWarn = items.some(function(i) { return (i.Level||'').toLowerCase() === 'warn'; }); var hasWarn = items.some(function(i) { return (i.Level||'').toLowerCase() === 'warn'; });
var hasInfo = items.some(function(i) { return (i.Level||'').toLowerCase() === 'info'; }); var hasInfo = items.some(function(i) { return (i.Level||'').toLowerCase() === 'info'; });
var row = document.getElementById("guidance-filter-row"); var row = document.getElementById("guidance-filter-row");
row.innerHTML = ""; row.innerHTML = "";
function makeChip(label, value, extraClass) { function chip(label, value, extra) {
var btn = document.createElement("button"); var b = document.createElement("button");
btn.className = "chip" + (guidanceFilter === value ? " active" : "") + (extraClass ? " " + extraClass : ""); b.className = "chip" + (guidanceFilter === value ? " active" : "") + (extra ? " " + extra : "");
btn.textContent = label; b.textContent = label;
btn.addEventListener("click", function() { b.addEventListener("click", function() {
guidanceFilter = value; guidanceFilter = value;
applyGuidanceFilter(); applyGuidanceFilter();
row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); }); row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); });
btn.classList.add("active"); b.classList.add("active");
}); });
row.appendChild(btn); row.appendChild(b);
} }
makeChip("全て", "all"); chip("全て", "all");
if (hasWarn) makeChip("注意のみ", "warn", "warn-chip"); if (hasWarn) chip("注意のみ", "warn", "warn-chip");
if (hasInfo) makeChip("情報のみ", "info", "info-chip"); if (hasInfo) chip("情報のみ", "info", "info-chip");
} }
function applyGuidanceFilter() { function applyGuidanceFilter() {
var items = guidanceFilter === "all" var items = guidanceFilter === "all"
? allGuidanceItems ? allGuidanceItems
: allGuidanceItems.filter(function(i) { return (i.Level||'info').toLowerCase() === guidanceFilter; }); : 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();
var badgeClass = level === "warn" ? "badge-warn" : (level === "ok" ? "badge-ok" : "badge-info"); var bc = level === "warn" ? "badge-warn" : (level === "ok" ? "badge-ok" : "badge-info");
var badgeLabel = level === "warn" ? "注意" : (level === "ok" ? "問題なし" : "情報"); var bl = level === "warn" ? "注意" : (level === "ok" ? "問題なし" : "情報");
html += '<div class="card ' + level + '">'; 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-header"><span class="badge ' + bc + '">' + bl + '</span><div class="card-title">' + esc(item.Title || "") + '</div></div>';
html += '<div class="card-body">' + esc(item.Body || "").replace(/\n/g, "<br/>") + '</div>'; html += '<div class="card-body">' + esc(item.Body || "").replace(/\n/g, "<br/>") + '</div>';
var action = item.ActionJa || item.actionJa; var action = item.ActionJa || item.actionJa;
if (action) { if (action) html += '<div class="action-box"><span class="action-label">対処方法</span>' + esc(action) + '</div>';
html += '<div class="action-box"><span class="action-label">対処方法</span>' + esc(action) + '</div>';
}
html += '</div>'; html += '</div>';
}); });
if (!html) html = '<p style="font-size:0.8rem;color:var(--text3);padding:0.5rem 0">該当するチェック項目はありません。</p>'; if (!html) html = '<p style="font-size:0.78rem;color:var(--text3);padding:0.4rem 0">該当するチェック項目はありません。</p>';
document.getElementById("guidance").innerHTML = html; document.getElementById("guidance").innerHTML = html;
} }
function renderGuidance(data) { function renderGuidance(data) {
allGuidanceItems = buildGuidanceItems(data); allGuidanceItems = buildGuidanceItems(data);
buildGuidanceFilter(allGuidanceItems); buildGuidanceFilterChips(allGuidanceItems);
applyGuidanceFilter(); applyGuidanceFilter();
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Render: Port list (with filter chips) */ /* Render: Port list */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function buildPortFilter() { function buildPortFilterChips() {
var scopes = {}; var scopes = {};
allPortRows.forEach(function(r) { if (r.BindScope) scopes[r.BindScope] = true; }); allPortRows.forEach(function(r) { if (r.BindScope) scopes[r.BindScope] = true; });
var row = document.getElementById("port-filter-row"); var row = document.getElementById("port-filter-row");
row.innerHTML = ""; row.innerHTML = "";
function makeChip(label, value, extraClass) { function chip(label, value, extra) {
var btn = document.createElement("button"); var b = document.createElement("button");
btn.className = "chip" + (portFilter === value ? " active" : "") + (extraClass ? " " + extraClass : ""); b.className = "chip" + (portFilter === value ? " active" : "") + (extra ? " " + extra : "");
btn.textContent = label; b.textContent = label;
btn.addEventListener("click", function() { b.addEventListener("click", function() {
portFilter = value; portFilter = value;
applyPortFilter(); applyPortFilter();
row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); }); row.querySelectorAll(".chip").forEach(function(c) { c.classList.remove("active"); });
btn.classList.add("active"); b.classList.add("active");
}); });
row.appendChild(btn); row.appendChild(b);
} }
makeChip("全て", "all"); chip("全て", "all");
if (scopes["allInterfaces"]) makeChip("全IF のみ", "allInterfaces", "scope-all-chip"); if (scopes["allInterfaces"]) chip("全IF のみ", "allInterfaces", "scope-all-chip");
if (scopes["localhost"]) makeChip("localhost のみ", "localhost", "scope-local-chip"); if (scopes["localhost"]) chip("localhost のみ", "localhost", "scope-local-chip");
if (scopes["specific"]) makeChip("個別アドレス", "specific"); if (scopes["specific"]) chip("個別アドレス", "specific");
} }
function applyPortFilter() { function applyPortFilter() {
var rows = portFilter === "all" var rows = portFilter === "all"
? allPortRows ? allPortRows
: allPortRows.filter(function(r) { return r.BindScope === portFilter; }); : allPortRows.filter(function(r) { return r.BindScope === portFilter; });
var html = ""; var html = "";
var shown = Math.min(rows.length, 80); var shown = Math.min(rows.length, 80);
for (var i = 0; i < shown; i++) { for (var i = 0; i < shown; i++) {
@ -534,16 +604,13 @@
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 style="font-size:0.75rem;color:var(--text3)">' + esc(pN) + '</td>'; html += '<td style="font-size:0.73rem;color:var(--text3)">' + esc(pN) + '</td>';
html += '</tr>'; html += '</tr>';
} }
var tableHtml = rows.length var tableHtml = rows.length
? '<table><thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>' + html + '</tbody></table>' ? '<table><thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>' + html + '</tbody></table>'
: '<p class="port-empty">該当するポートはありません。</p>'; : '<p class="port-empty">該当するポートはありません。</p>';
if (rows.length > 80) { if (rows.length > 80) tableHtml += '<p style="margin-top:0.4rem;font-size:0.68rem;color:var(--text3)">先頭 80 行表示。全データは生データを参照。</p>';
tableHtml += '<p style="margin-top:0.5rem;font-size:0.7rem;color:var(--text3)">先頭 80 行表示。全データは下の生データを参照。</p>';
}
document.getElementById("listeners").innerHTML = tableHtml; document.getElementById("listeners").innerHTML = tableHtml;
} }
@ -552,25 +619,18 @@
var li = net.ListenerInsights; var li = net.ListenerInsights;
allPortRows = (li && li.RowsEnrichedSample) ? li.RowsEnrichedSample : (net.TcpListeners || []); allPortRows = (li && li.RowsEnrichedSample) ? li.RowsEnrichedSample : (net.TcpListeners || []);
if (!allPortRows.length) return; if (!allPortRows.length) return;
var s = (li && li.Summary) || {}; var s = (li && li.Summary) || {};
portSummary = s; var legend = '<span style="color:var(--ok-color)">localhost</span> = PC 内のみ。'
+ ' <span style="color:var(--warn-color)">:: / 0.0.0.0</span> = Windows 標準、通常は問題なし。';
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) { if (s.AllInterfacesCount != null) {
legend += '<div class="port-summary-row">'; legend += '<div class="port-summary-row" style="margin-top:0.4rem">'
legend += '<span class="port-summary-item">全IF: <strong>' + s.AllInterfacesCount + '</strong></span>'; + '<span class="port-summary-item">全IF: <strong>' + s.AllInterfacesCount + '</strong></span>'
legend += '<span class="port-summary-item">localhost: <strong>' + s.LocalhostOnlyCount + '</strong></span>'; + '<span class="port-summary-item">localhost: <strong>' + s.LocalhostOnlyCount + '</strong></span>'
legend += '<span class="port-summary-item">合計: <strong>' + s.TotalListenRows + '</strong></span>'; + '<span class="port-summary-item">合計: <strong>' + s.TotalListenRows + '</strong></span>'
legend += '</div>'; + '</div>';
} }
document.getElementById("port-legend").innerHTML = legend; document.getElementById("port-legend").innerHTML = legend;
buildPortFilterChips();
buildPortFilter();
applyPortFilter(); applyPortFilter();
} }
@ -578,7 +638,9 @@
/* Load data */ /* Load data */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
function loadData(data) { function loadData(data) {
document.getElementById("output").style.display = ""; document.getElementById("upload-wrap").style.display = "none";
document.getElementById("alt-file-btn").style.display = "";
document.getElementById("output").style.display = "flex";
renderSummary(data); renderSummary(data);
renderDiff(data); renderDiff(data);
renderGuidance(data); renderGuidance(data);
@ -587,28 +649,27 @@
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Auto-load (injected via BAT) */ /* Auto-load */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
if (window.__AUDIT_PRELOAD__) { if (window.__AUDIT_PRELOAD__) {
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
document.getElementById("upload-area").style.display = "none";
document.getElementById("alt-file-row").style.display = "block";
loadData(window.__AUDIT_PRELOAD__); loadData(window.__AUDIT_PRELOAD__);
}); });
} }
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* "別のファイルを読む" button */ /* "別のファイルを読む" */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
document.getElementById("alt-file-btn").addEventListener("click", function () { document.getElementById("alt-file-btn").addEventListener("click", function () {
document.getElementById("upload-area").style.display = ""; document.getElementById("upload-wrap").style.display = "flex";
document.getElementById("alt-file-row").style.display = "none"; document.getElementById("alt-file-btn").style.display = "none";
document.getElementById("output").style.display = "none";
}); });
}); });
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* File input handler */ /* File input */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
document.addEventListener("DOMContentLoaded", function () { document.addEventListener("DOMContentLoaded", function () {
document.getElementById("f").addEventListener("change", function (ev) { document.getElementById("f").addEventListener("change", function (ev) {
@ -619,9 +680,9 @@
var data; var data;
try { data = JSON.parse(reader.result); } try { data = JSON.parse(reader.result); }
catch (e) { catch (e) {
document.getElementById("output").style.display = "flex";
document.getElementById("guidance").innerHTML = 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>'; '<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; return;
} }
loadData(data); loadData(data);

View File

@ -13,9 +13,15 @@ if errorlevel 1 (
) )
echo. echo.
echo Opening report viewer... echo Opening report viewer...
start "" "%~dp0report-viewer.html" 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. echo.
echo Done. Select out\latest.json in the browser.
echo This report is safe to share with IT / engineers. echo This report is safe to share with IT / engineers.
echo. echo.
pause pause

View File

@ -13,8 +13,14 @@ if errorlevel 1 (
) )
echo. echo.
echo Opening report viewer... echo Opening report viewer...
start "" "%~dp0report-viewer.html" 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. echo.
echo Done. Select out\latest.json in the browser.
echo. echo.
pause pause