fix(pc-audit): 管理者判定を3段階に強化、viewer を3カラムレイアウトに変更

PS1: IsInRole -> SID string 比較 -> net localgroup の3段階で判定。
UAC トークンフィルタリング環境でも管理者ユーザーを正しく検出する。

viewer: 左寄せ3カラム(概要/差分 | チェック項目 | ポート一覧)。
スマホは1カラムに自動切替。一時ポート(49152-65535)に「正常」説明を追加。

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-04-21 08:24:33 +09:00
parent 2c0f4da03c
commit cf10f757a8
2 changed files with 201 additions and 178 deletions

View File

@ -48,16 +48,23 @@ if (Test-Path -LiteralPath $latestPathForPrev) {
} }
function Test-IsAdmin { function Test-IsAdmin {
# IsInRole checks the *current token* (elevated or not under UAC). # Check 1: current process token is elevated (Run as Administrator)
# We also check group membership via SID so non-elevated admin sessions are detected.
$id = [Security.Principal.WindowsIdentity]::GetCurrent() $id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id) $p = New-Object Security.Principal.WindowsPrincipal($id)
if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { return $true } if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { return $true }
# Fallback: check if the user's identity is a member of BUILTIN\Administrators (S-1-5-32-544) # Check 2: user is a member of BUILTIN\Administrators group (SID S-1-5-32-544).
$adminSid = New-Object Security.Principal.SecurityIdentifier("S-1-5-32-544") # Under UAC the token is filtered so IsInRole returns false even for admin group members.
foreach ($g in $id.Groups) { # Comparing .Value (string form of SID) is reliable regardless of token filtering.
if ($g.Equals($adminSid)) { return $true } $adminSidValue = "S-1-5-32-544"
} if ($id.Groups.Value -contains $adminSidValue) { return $true }
# Check 3: fallback via net localgroup (catches edge cases where token groups differ)
try {
$currentUser = $env:USERNAME
$members = & net localgroup Administrators 2>$null
foreach ($line in $members) {
if ($line.Trim() -eq $currentUser) { return $true }
}
} catch {}
return $false return $false
} }

View File

@ -14,8 +14,6 @@
--text2: #9CA3AF; --text2: #9CA3AF;
--text3: #6B7280; --text3: #6B7280;
--accent: #6EE7B7; --accent: #6EE7B7;
--accent-dim: rgba(110,231,183,0.08);
--accent-border: rgba(110,231,183,0.2);
--warn-color: #F59E0B; --warn-color: #F59E0B;
--warn-dim: rgba(245,158,11,0.08); --warn-dim: rgba(245,158,11,0.08);
--warn-border: rgba(245,158,11,0.2); --warn-border: rgba(245,158,11,0.2);
@ -36,26 +34,33 @@
line-height: 1.6; line-height: 1.6;
min-height: 100vh; min-height: 100vh;
} }
.page { max-width: 52rem; margin: 0 auto; padding: 2rem 1.25rem 4rem; }
/* Header */ /* Layout */
.page { max-width: 1280px; padding: 2rem 1.5rem 4rem; }
.header { margin-bottom: 2rem; } .header { margin-bottom: 2rem; }
.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; margin-top: 0.5rem;
font-size: 0.8rem; 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.5rem;
} }
.privacy-dot { .privacy-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
display: inline-block;
width: 6px; height: 6px; /* 3-column grid — collapses to 1 col on mobile */
border-radius: 50%; .grid3 {
background: var(--accent); display: grid;
flex-shrink: 0; 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 */
@ -63,155 +68,99 @@
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.75rem 1.5rem; padding: 1.25rem;
margin-bottom: 2rem; margin-bottom: 1rem;
} }
.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.78rem; font-size: 0.75rem; color: var(--text3); background: var(--surface2);
color: var(--text3); padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-family: monospace;
background: var(--surface2); display: inline-block; margin-bottom: 0.6rem;
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"] { 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); background: var(--surface2); color: var(--text); border: 1px solid var(--border);
color: var(--text); border-radius: var(--radius-sm); padding: 0.3rem 0.75rem; font-size: 0.78rem;
border: 1px solid var(--border); cursor: pointer; margin-right: 0.5rem; margin-bottom: 0.25rem;
border-radius: var(--radius-sm);
padding: 0.35rem 0.9rem;
font-size: 0.8rem;
cursor: pointer;
margin-right: 0.75rem;
} }
/* Section label */ /* Section label */
.section-title { .section-title {
font-size: 0.68rem; font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em;
font-weight: 700; text-transform: uppercase; color: var(--text3); margin: 0 0 0.6rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--text3);
margin: 2rem 0 0.75rem;
} }
/* Summary grid */ /* Metric grid */
.summary-grid { .summary-grid { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.75rem; }
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.metric { .metric {
background: var(--surface); background: var(--surface); border: 1px solid var(--border);
border: 1px solid var(--border); border-radius: var(--radius-sm); padding: 0.7rem 0.9rem;
border-radius: var(--radius-sm);
padding: 0.85rem 1rem;
} }
.metric-label { font-size: 0.72rem; color: var(--text3); margin-bottom: 0.2rem; } .metric-label { font-size: 0.7rem; color: var(--text3); margin-bottom: 0.15rem; }
.metric-value { font-size: 0.95rem; 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 { .diff-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; }
background: var(--surface); .diff-card-title { font-size: 0.8rem; font-weight: 600; color: var(--text2); margin-bottom: 0.6rem; }
border: 1px solid var(--border); .diff-col { display: flex; flex-direction: column; gap: 0.4rem; }
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 */ /* Cards */
.card { .card {
background: var(--surface); background: var(--surface); border: 1px solid var(--border);
border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 0.6rem;
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.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.6rem; margin-bottom: 0.6rem; }
.badge { .badge {
font-size: 0.65rem; font-size: 0.63rem; font-weight: 700; letter-spacing: 0.06em;
font-weight: 700; padding: 0.18rem 0.5rem; border-radius: 99px; white-space: nowrap;
letter-spacing: 0.06em; flex-shrink: 0; margin-top: 0.15rem;
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-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.9rem; 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.84rem; color: var(--text2); line-height: 1.65; }
.action-box { .action-box {
margin-top: 0.85rem; margin-top: 0.75rem; background: var(--surface2);
background: var(--surface2); border-radius: var(--radius-sm); padding: 0.65rem 0.85rem;
border-radius: var(--radius-sm); font-size: 0.8rem; color: var(--text2);
padding: 0.75rem 1rem;
font-size: 0.82rem;
color: var(--text2);
} }
.action-label { .action-label {
font-size: 0.65rem; font-size: 0.63rem; font-weight: 700; letter-spacing: 0.1em;
font-weight: 700; color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.25rem;
letter-spacing: 0.1em;
color: var(--accent);
text-transform: uppercase;
display: block;
margin-bottom: 0.3rem;
} }
/* Listener table */ /* Port table */
.table-wrap { overflow-x: auto; margin-top: 0.75rem; } .table-wrap { overflow-x: auto; }
table { width: 100%; border-collapse: collapse; font-size: 0.82rem; } table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
thead th { thead th {
text-align: left; text-align: left; padding: 0.45rem 0.6rem;
padding: 0.5rem 0.75rem; font-size: 0.65rem; font-weight: 700; letter-spacing: 0.08em;
font-size: 0.68rem; text-transform: uppercase; color: var(--text3); border-bottom: 1px solid var(--border);
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 { border-bottom: 1px solid var(--border); }
tbody tr:last-child { border-bottom: none; } tbody tr:last-child { border-bottom: none; }
tbody td { padding: 0.45rem 0.75rem; color: var(--text2); vertical-align: top; } tbody td { padding: 0.4rem 0.6rem; color: var(--text2); vertical-align: top; }
.addr-cell { font-family: monospace; font-size: 0.78rem; color: var(--text3); } .addr-cell { font-family: monospace; font-size: 0.75rem; 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.8rem; } .scope-all { color: var(--warn-color); font-size: 0.78rem; }
.scope-localhost { color: var(--ok-color); font-size: 0.8rem; } .scope-localhost { color: var(--ok-color); font-size: 0.78rem; }
.scope-specific { color: var(--info-color); font-size: 0.8rem; } .scope-specific { color: var(--info-color); font-size: 0.78rem; }
.hint-line { font-size: 0.73rem; color: var(--text3); margin-top: 0.1rem; } .hint-line { font-size: 0.7rem; color: var(--text3); margin-top: 0.1rem; }
/* Raw */ /* Raw */
details { details {
background: var(--surface); background: var(--surface); border: 1px solid var(--border);
border: 1px solid var(--border); border-radius: var(--radius); padding: 0.9rem 1.1rem; margin-top: 1.5rem;
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; } summary { cursor: pointer; font-size: 0.78rem; 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; } 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.75rem; color: var(--text3); border-top: 1px solid var(--border); padding-top: 1rem; margin-top: 2rem; } .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>
@ -221,17 +170,33 @@
<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 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="output" style="display:none"> <div id="output" style="display:none">
<div id="summary"></div> <div class="grid3">
<div id="diff"></div> <!-- Col 1: summary + diff -->
<div id="guidance"></div> <div>
<div id="listeners"></div> <div class="col-label">概要 / 差分</div>
<div id="summary"></div>
<div id="diff"></div>
</div>
<!-- Col 2: check items -->
<div>
<div class="col-label">チェック項目</div>
<div id="guidance"></div>
</div>
<!-- Col 3: port list -->
<div>
<div class="col-label">待ち受けポート一覧</div>
<div id="listeners"></div>
</div>
</div>
<details><summary>生データJSON 全体)</summary><pre id="raw"></pre></details> <details><summary>生データJSON 全体)</summary><pre id="raw"></pre></details>
<p class="disclaimer">このレポートは補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p> <p class="disclaimer">このレポートは補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
</div> </div>
@ -246,33 +211,71 @@
} }
function len(a) { return Array.isArray(a) ? a.length : 0; } function len(a) { return Array.isArray(a) ? a.length : 0; }
/* ------------------------------------------------------------------ */
/* Port descriptions */
/* ------------------------------------------------------------------ */
var PORT_NOTES = { var PORT_NOTES = {
"135": "Windows 標準RPC— 正常", "80": "HTTPWeb サーバー)",
"139": "Windows ファイル共有NetBIOS— LAN 内なら正常", "135": "Windows RPC — 正常",
"445": "Windows ファイル共有SMB— LAN では一般的", "139": "NetBIOS ファイル共有 — LAN 内なら正常",
"443": "HTTPSWeb サーバー)",
"445": "SMB ファイル共有 — LAN 内なら正常",
"843": "Adobe Flash 関連またはローカルアプリ", "843": "Adobe Flash 関連またはローカルアプリ",
"2015": "開発用ローカルサーバーCaddy 等)", "902": "VMware 管理",
"5040": "Windows 標準サービス", "912": "VMware 管理",
"1080": "SOCKSプロキシ",
"2015": "Caddy 開発サーバー",
"3000": "開発用ローカルサーバーNode 等)— 正常",
"3306": "MySQL",
"3389": "リモートデスクトップRDP",
"5040": "Windows 標準サービス — 正常",
"5354": "mDNSローカルデバイス検索— 正常", "5354": "mDNSローカルデバイス検索— 正常",
"5357": "Windows ネットワーク検出 — 正常", "5357": "Windows ネットワーク検出 — 正常",
"5432": "PostgreSQL",
"5900": "VNC リモートデスクトップ",
"6379": "Redis",
"7680": "Windows Update 配信最適化 — 正常", "7680": "Windows Update 配信最適化 — 正常",
"8080": "開発用 HTTP サーバー — 正常",
"8443": "開発用 HTTPS サーバー",
"17500": "Dropbox — インストール済みなら正常", "17500": "Dropbox — インストール済みなら正常",
"17600": "Dropbox — インストール済みなら正常", "17600": "Dropbox — インストール済みなら正常",
"27015": "Steam ゲームクライアント — 正常" "27015": "Steam — インストール済みなら正常",
"27017": "MongoDB",
"33060": "MySQL X Protocol"
}; };
function addressNote(addr) { /* Ephemeral port ranges are dynamically assigned by Windows — always normal */
if (addr === "::" || addr === "0.0.0.0") return "全インターフェースWindows 標準)"; function isEphemeral(port) {
if (addr === "::1" || addr === "127.0.0.1") return "localhost — 自分の PC 内だけ、安全"; var p = parseInt(port, 10);
if (/^100\./.test(addr)) return "Tailscale VPN — インストール済みなら正常"; return p >= 49152 && p <= 65535;
if (/^192\.168\./.test(addr)) return "自宅 / 社内 LAN — 正常"; }
if (/^fd7a:115c:/.test(addr)) return "Tailscale VPNIPv6— 正常";
function portNote(port, hint) {
if (PORT_NOTES[String(port)]) return PORT_NOTES[String(port)];
if (hint) return hint;
if (isEphemeral(port)) return "一時ポートWindows が一時的に割り当て)— 正常";
return ""; return "";
} }
/* ------------------------------------------------------------------ */
/* Address notes */
/* ------------------------------------------------------------------ */
function addressNote(addr) {
if (addr === "::" || addr === "0.0.0.0") return "全インターフェースWindows 標準)";
if (addr === "::1" || addr === "127.0.0.1") return "localhost — PC 内だけ、安全";
if (/^100\./.test(addr)) return "Tailscale VPN — 正常";
if (/^192\.168\./.test(addr)) return "LAN — 正常";
if (/^fd7a:115c:/.test(addr)) return "Tailscale VPNIPv6— 正常";
if (/^::1$/.test(addr)) return "localhostIPv6— 安全";
return "";
}
/* ------------------------------------------------------------------ */
/* Render: Summary */
/* ------------------------------------------------------------------ */
function renderSummary(data) { function renderSummary(data) {
var uc = data.userContext || {}; var uc = data.userContext || {};
var m = data.machine || {}; var m = data.machine || {};
var uac = data.uac || {}; var uac = data.uac || {};
var net = data.network || {}; var net = data.network || {};
var listeners = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0; var listeners = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
@ -292,7 +295,7 @@
]; ];
if (shareSafe) items.unshift({ label: "モード", value: "ShareSafe" }); if (shareSafe) items.unshift({ label: "モード", value: "ShareSafe" });
var html = '<div class="section-title">概要</div><div class="summary-grid">'; var html = '<div class="summary-grid">';
items.forEach(function (it) { items.forEach(function (it) {
html += '<div class="metric"><div class="metric-label">' + esc(it.label) + '</div><div class="metric-value">' + esc(it.value) + '</div></div>'; html += '<div class="metric"><div class="metric-label">' + esc(it.label) + '</div><div class="metric-value">' + esc(it.value) + '</div></div>';
}); });
@ -300,35 +303,41 @@
document.getElementById("summary").innerHTML = html; document.getElementById("summary").innerHTML = html;
} }
/* ------------------------------------------------------------------ */
/* Render: Diff */
/* ------------------------------------------------------------------ */
function renderDiff(data) { function renderDiff(data) {
var df = data.diffFromPrevious; var df = data.diffFromPrevious;
if (!df) return; if (!df) return;
var html = '<div class="section-title">前回との差分</div><div class="diff-card">'; var html = '<div class="diff-card" style="margin-top:0.75rem">';
if (df.HasPrevious === false) { if (df.HasPrevious === false) {
html += '<div class="diff-card-title">前回データなし</div><p style="font-size:0.82rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>'; html += '<div class="diff-card-title">前回データなし</div><p style="font-size:0.8rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>';
} else { } else {
html += '<div class="diff-card-title">前回' + esc(df.PreviousGeneratedAtUtc || "") + ')との比較</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-grid">'; html += '<div class="diff-col">';
[ [
{ 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.75rem;font-size:0.82rem;color:var(--warn-color)">管理者権限の状態が前回と変わりました。</p>'; if (df.UserIsAdminChanged) html += '<p style="margin-top:0.6rem;font-size:0.8rem;color:var(--warn-color)">管理者権限の状態が前回と変わりました。</p>';
if (df.UacEnableLuaChanged) html += '<p style="margin-top:0.5rem;font-size:0.82rem;color:var(--warn-color)">UAC 設定が前回と変わりました。</p>'; if (df.UacEnableLuaChanged) html += '<p style="margin-top:0.4rem;font-size:0.8rem;color:var(--warn-color)">UAC 設定が前回と変わりました。</p>';
} }
html += '</div>'; html += '</div>';
document.getElementById("diff").innerHTML = html; document.getElementById("diff").innerHTML = html;
} }
/* ------------------------------------------------------------------ */
/* Render: Guidance cards */
/* ------------------------------------------------------------------ */
function buildGuidanceItems(data) { function buildGuidanceItems(data) {
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : []; var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
if (list.length) return list; if (list.length) return list;
@ -344,24 +353,24 @@
out.push({ Level:"ok", Title:"管理者権限は検出されませんでした", Body:"この監査実行時点では管理者権限は確認されていません。" }); out.push({ Level:"ok", Title:"管理者権限は検出されませんでした", Body:"この監査実行時点では管理者権限は確認されていません。" });
} }
if (uac.EnableLUA === 0) { if (uac.EnableLUA === 0) {
out.push({ Level:"warn", Title:"UAC確認ダイアログが無効になっています", Body:"アプリが PC 設定を変更しようとしても確認ダイアログが出ない状態です。マルウェアが気づかず実行されやすくなります。", ActionJa:"社内 IT に「UAC を有効にしてほしい」と伝える。設定変更は IT に依頼するのが安全です。" }); 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 に含まれていないか確認して」と伝える。" });
} }
var lc = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0; var lc = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
if (lc > 45) { if (lc > 45) {
out.push({ Level:"info", Title:"待ち受けポートが多めです(" + lc + " 件)", Body:"PC 上で多くのサービスが通信を受け付けています。使っていないアプリが起動したままになっている可能性があります。", ActionJa:"エンジニアに一覧を見てもらい、不要なアプリがないか確認してもらう。" }); out.push({ Level:"info", Title:"待ち受けポートが多めです(" + lc + " 件)", Body:"多くのサービスが通信を受け付けています。使っていないアプリが起動したまま可能性があります。", ActionJa:"エンジニアに一覧を見てもらい、不要なアプリがないか確認してもらう。" });
} }
return out; return out;
} }
function renderGuidance(data) { function renderGuidance(data) {
var items = buildGuidanceItems(data); var items = buildGuidanceItems(data);
var html = '<div class="section-title">チェック項目</div>'; 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 badgeClass = level === "warn" ? "badge-warn" : (level === "ok" ? "badge-ok" : "badge-info");
@ -378,6 +387,9 @@
document.getElementById("guidance").innerHTML = html; document.getElementById("guidance").innerHTML = html;
} }
/* ------------------------------------------------------------------ */
/* Render: Port list */
/* ------------------------------------------------------------------ */
function renderListeners(data) { function renderListeners(data) {
var net = data.network || {}; var net = data.network || {};
var li = net.ListenerInsights; var li = net.ListenerInsights;
@ -385,42 +397,46 @@
if (!rows.length) return; if (!rows.length) return;
var s = (li && li.Summary) || {}; var s = (li && li.Summary) || {};
var html = '<div class="section-title">待ち受けポート一覧</div><div class="card">'; var html = '';
html += '<p class="card-body" style="margin-bottom:0.85rem">'; html += '<p style="font-size:0.8rem;color:var(--text2);margin-bottom:0.85rem;line-height:1.6">';
html += '「待ち受けポート」は PC 上で外部からの通信を受け付けているサービスの一覧です。<br>'; html += '<span style="color:var(--ok-color)">localhost</span> は自分の PC 内だけ、安全。';
html += '<span style="color:var(--ok-color)">localhost</span> のものは自分の PC 内だけで通信するため安全です。'; html += '<br><span style="color:var(--warn-color)">:: / 0.0.0.0</span> は Windows 標準で出ることが多く、通常は問題なし。';
html += '<span style="color:var(--warn-color);margin-left:0.5rem">::コロン2つ / 0.0.0.0</span> は Windows 標準で出ることが多く、通常は問題ありません。'; html += '<br>日本語説明がないポートは <strong style="color:var(--text)">一時ポートWindows が自動割当)</strong>で、接続ごとに番号が変わる正常な動作です。';
html += '</p>'; html += '</p>';
if (s.AllInterfacesCount != null) { if (s.AllInterfacesCount != null) {
html += '<div style="display:flex;gap:1rem;margin-bottom:0.85rem;flex-wrap:wrap">'; 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.78rem;color:var(--text3)">IF: <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.78rem;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 += '<span style="font-size:0.78rem;color:var(--text3)">合計: <strong style="color:var(--text)">' + s.TotalListenRows + '</strong></span>';
html += '</div>'; html += '</div>';
} }
html += '<div class="table-wrap"><table>'; html += '<div class="table-wrap"><table>';
html += '<thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途の目安</th></tr></thead><tbody>'; html += '<thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>';
var shown = Math.min(rows.length, 50); var shown = Math.min(rows.length, 60);
for (var i = 0; i < shown; i++) { for (var i = 0; i < shown; i++) {
var r = rows[i]; var r = rows[i];
var scopeClass = 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");
var addrNote = addressNote(r.Address || ""); var aN = addressNote(r.Address || "");
var portNote = PORT_NOTES[String(r.Port)] || (r.WellKnownHint || ""); var pN = portNote(r.Port, r.WellKnownHint);
html += '<tr>'; html += '<tr>';
html += '<td class="addr-cell">' + esc(r.Address || "") + (addrNote ? '<div class="hint-line">' + esc(addrNote) + '</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="' + scopeClass + '">' + esc(r.BindScope || "") + '</td>'; html += '<td class="' + sc + '">' + esc(r.BindScope || "") + '</td>';
html += '<td class="hint-line" style="font-size:0.8rem;color:var(--text3)">' + esc(portNote) + '</td>'; html += '<td class="hint-line" style="font-size:0.78rem;color:var(--text3)">' + esc(pN) + '</td>';
html += '</tr>'; html += '</tr>';
} }
html += '</tbody></table></div>'; html += '</tbody></table></div>';
if (rows.length > 50) { if (rows.length > 60) {
html += '<p style="margin-top:0.5rem;font-size:0.75rem;color:var(--text3)">先頭 50 行のみ表示。全データは下の生データを参照。</p>'; html += '<p style="margin-top:0.5rem;font-size:0.72rem;color:var(--text3)">先頭 60 行のみ表示。全データは下の生データを参照。</p>';
} }
html += '</div>';
document.getElementById("listeners").innerHTML = html; document.getElementById("listeners").innerHTML = html;
} }
/* ------------------------------------------------------------------ */
/* File load */
/* ------------------------------------------------------------------ */
document.getElementById("f").addEventListener("change", function (ev) { document.getElementById("f").addEventListener("change", function (ev) {
var file = ev.target.files && ev.target.files[0]; var file = ev.target.files && ev.target.files[0];
if (!file) return; if (!file) return;