Compare commits
No commits in common. "d36b7a8fe4663b1242d40a20f86200e75059dcd7" and "d08a0d6eedc602707b4159df36fed36c7c8ff428" have entirely different histories.
d36b7a8fe4
...
d08a0d6eed
|
|
@ -1 +1 @@
|
||||||
# .gitattributes
|
tools/pc-audit/Invoke-PcAudit.ps1 text working-tree-encoding=UTF-8-BOM
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
| `target="_blank"` に `rel="noopener"` なし | Tabnabbing 対策必須 |
|
| `target="_blank"` に `rel="noopener"` なし | Tabnabbing 対策必須 |
|
||||||
| AI 訓練データのみでバージョン回答 | `WebSearch` で確認してから答える |
|
| AI 訓練データのみでバージョン回答 | `WebSearch` で確認してから答える |
|
||||||
| 新アプリ作成時に Gitea を手動案内 | credential store 認証あり。curl + gh で自律作成すること |
|
| 新アプリ作成時に Gitea を手動案内 | credential store 認証あり。curl + gh で自律作成すること |
|
||||||
| UI に左ボーダーライン(`border-left`)でレベル区別 | AI っぽく見える。badge + カード枠色(`border-color`)で表現すること |
|
|
||||||
|
|
||||||
## 2. デプロイパイプライン
|
## 2. デプロイパイプライン
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
| `target="_blank"` に `rel="noopener"` なし | Tabnabbing 対策必須 |
|
| `target="_blank"` に `rel="noopener"` なし | Tabnabbing 対策必須 |
|
||||||
| AI 訓練データのみでバージョン回答 | `WebSearch` で確認してから答える |
|
| AI 訓練データのみでバージョン回答 | `WebSearch` で確認してから答える |
|
||||||
| 新アプリ作成時に Gitea を手動案内 | credential store 認証あり。curl + gh で自律作成すること |
|
| 新アプリ作成時に Gitea を手動案内 | credential store 認証あり。curl + gh で自律作成すること |
|
||||||
| UI に左ボーダーライン(`border-left`)でレベル区別 | AI っぽく見える。badge + カード枠色(`border-color`)で表現すること |
|
|
||||||
|
|
||||||
## 2. デプロイパイプライン
|
## 2. デプロイパイプライン
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 13c76e3a9f486af19f8469fb12ab447a7757dce0
|
Subproject commit 1337c280b07a76ca54b2f5438f4f4672410378d2
|
||||||
|
|
@ -18,9 +18,6 @@
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
(function(){
|
||||||
var p=new URLSearchParams(location.search);
|
|
||||||
var tk=p.get('token');
|
|
||||||
if(tk){ localStorage.setItem('posimai_token',tk); p.delete('token'); history.replaceState({},'',location.pathname+(p.toString()?'?'+p.toString():'')+location.hash); }
|
|
||||||
var t=localStorage.getItem('posimai-sc-theme')||'system';
|
var t=localStorage.getItem('posimai-sc-theme')||'system';
|
||||||
var dark=t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);
|
var dark=t==='dark'||(t==='system'&&matchMedia('(prefers-color-scheme:dark)').matches);
|
||||||
document.documentElement.setAttribute('data-theme',dark?'dark':'light');
|
document.documentElement.setAttribute('data-theme',dark?'dark':'light');
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "Content-Security-Policy",
|
"key": "Content-Security-Policy",
|
||||||
"value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://cdn.jsdelivr.net https://esm.sh; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com https://posimai-ui.vercel.app; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self' https:; connect-src 'self' https://api.soar-enrich.com wss://api.soar-enrich.com https:; worker-src 'self'; frame-ancestors 'none';"
|
"value": "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://unpkg.com https://cdn.jsdelivr.net https://esm.sh; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://fonts.gstatic.com; font-src 'self' data: https://fonts.gstatic.com; img-src 'self' data: https:; media-src 'self' https:; connect-src 'self' https://api.soar-enrich.com wss://api.soar-enrich.com https:; worker-src 'self'; frame-ancestors 'none';"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "Strict-Transport-Security",
|
"key": "Strict-Transport-Security",
|
||||||
|
|
|
||||||
33
server.js
33
server.js
|
|
@ -169,7 +169,7 @@ app.use(cors({
|
||||||
if (isAllowedOrigin(origin)) cb(null, true);
|
if (isAllowedOrigin(origin)) cb(null, true);
|
||||||
else cb(new Error('CORS not allowed'));
|
else cb(new Error('CORS not allowed'));
|
||||||
},
|
},
|
||||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization']
|
allowedHeaders: ['Content-Type', 'Authorization']
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -2000,12 +2000,6 @@ ${excerpt}
|
||||||
if (JSON.stringify(contents).length > 60000) {
|
if (JSON.stringify(contents).length > 60000) {
|
||||||
return res.status(400).json({ error: 'contents too large' });
|
return res.status(400).json({ error: 'contents too large' });
|
||||||
}
|
}
|
||||||
const VALID_ROLES = new Set(['user', 'model']);
|
|
||||||
for (const item of contents) {
|
|
||||||
if (!VALID_ROLES.has(item?.role)) {
|
|
||||||
return res.status(400).json({ error: 'invalid role in contents' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const modelOpts = { model: 'gemini-2.5-flash' };
|
const modelOpts = { model: 'gemini-2.5-flash' };
|
||||||
if (systemPrompt && typeof systemPrompt === 'string') {
|
if (systemPrompt && typeof systemPrompt === 'string') {
|
||||||
|
|
@ -2014,7 +2008,7 @@ ${excerpt}
|
||||||
const model = genAI.getGenerativeModel(modelOpts);
|
const model = genAI.getGenerativeModel(modelOpts);
|
||||||
const generationConfig = {
|
const generationConfig = {
|
||||||
maxOutputTokens: Math.min(config.maxOutputTokens || 500, 1000),
|
maxOutputTokens: Math.min(config.maxOutputTokens || 500, 1000),
|
||||||
temperature: Math.max(0, Math.min(config.temperature ?? 0.5, 2))
|
temperature: config.temperature ?? 0.5
|
||||||
};
|
};
|
||||||
const timeoutPromise = new Promise((_, reject) =>
|
const timeoutPromise = new Promise((_, reject) =>
|
||||||
setTimeout(() => reject(new Error('timeout')), 15000)
|
setTimeout(() => reject(new Error('timeout')), 15000)
|
||||||
|
|
@ -2788,29 +2782,6 @@ ${excerpt}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// PUT /together/members/rename — 表示名変更
|
|
||||||
r.put('/together/members/rename', async (req, res) => {
|
|
||||||
const { group_id, old_username, new_username } = req.body || {};
|
|
||||||
const trimmed = normalizeTogetherUsername(new_username);
|
|
||||||
if (!group_id || !old_username || !trimmed) return res.status(400).json({ error: 'group_id, old_username, new_username は必須です' });
|
|
||||||
if (trimmed.length > 30) return res.status(400).json({ error: '名前は30文字以内にしてください' });
|
|
||||||
const jwtUserId = getTogetherJwtUserId(req);
|
|
||||||
try {
|
|
||||||
if (!(await togetherEnsureMember(pool, res, group_id, old_username, jwtUserId))) return;
|
|
||||||
const conflict = await pool.query(
|
|
||||||
'SELECT 1 FROM together_members WHERE group_id=$1 AND username=$2',
|
|
||||||
[group_id, trimmed]
|
|
||||||
);
|
|
||||||
if (conflict.rows.length > 0) return res.status(409).json({ error: '同じ名前のメンバーが既にいます' });
|
|
||||||
await pool.query('UPDATE together_members SET username=$1 WHERE group_id=$2 AND username=$3', [trimmed, group_id, old_username]);
|
|
||||||
await pool.query('UPDATE together_shares SET shared_by=$1 WHERE group_id=$2 AND shared_by=$3', [trimmed, group_id, old_username]);
|
|
||||||
res.json({ ok: true });
|
|
||||||
} catch (e) {
|
|
||||||
console.error('[together/rename]', e.message);
|
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// DELETE /together/share/:id — 自分の投稿を削除
|
// DELETE /together/share/:id — 自分の投稿を削除
|
||||||
r.delete('/together/share/:id', async (req, res) => {
|
r.delete('/together/share/:id', async (req, res) => {
|
||||||
const { username } = req.body || {};
|
const { username } = req.body || {};
|
||||||
|
|
|
||||||
|
|
@ -1,868 +0,0 @@
|
||||||
#Requires -Version 5.1
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Read-only local PC audit (developer / AI-tooling focused). No writes except report output.
|
|
||||||
Note: node / npm / git / python are invoked with --version to collect toolchain versions.
|
|
||||||
|
|
||||||
.PARAMETER OutDir
|
|
||||||
Directory for reports (created if missing). Default: tools/pc-audit/out under repo root.
|
|
||||||
|
|
||||||
.PARAMETER ProjectRoot
|
|
||||||
Repo root to scan for env-like files. Default: detected from script location.
|
|
||||||
|
|
||||||
.PARAMETER Format
|
|
||||||
json | md | both
|
|
||||||
|
|
||||||
.PARAMETER ShareSafe
|
|
||||||
社外・社内 IT へ渡すレポート向け。.claude 系のフルパス・キーワードヒットを出さず、deny 構成に触れる日本語ガイダンスも省略する。
|
|
||||||
#>
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[string]$OutDir = "",
|
|
||||||
[string]$ProjectRoot = "",
|
|
||||||
[ValidateSet("json", "md", "both")]
|
|
||||||
[string]$Format = "both",
|
|
||||||
[switch]$ShareSafe
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "SilentlyContinue"
|
|
||||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
||||||
$repoRoot = (Resolve-Path (Join-Path $scriptDir "..\..")).Path
|
|
||||||
if (-not $ProjectRoot) { $ProjectRoot = $repoRoot }
|
|
||||||
if (-not $OutDir) { $OutDir = Join-Path $scriptDir "out" }
|
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath $OutDir)) {
|
|
||||||
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
$latestPathForPrev = Join-Path $OutDir "latest.json"
|
|
||||||
$previousPathForPrev = Join-Path $OutDir "previous.json"
|
|
||||||
$script:prevAuditObj = $null
|
|
||||||
if (Test-Path -LiteralPath $latestPathForPrev) {
|
|
||||||
try {
|
|
||||||
Copy-Item -LiteralPath $latestPathForPrev -Destination $previousPathForPrev -Force
|
|
||||||
$script:prevAuditObj = Get-Content -LiteralPath $previousPathForPrev -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
||||||
} catch {
|
|
||||||
$script:prevAuditObj = $null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-IsAdmin {
|
|
||||||
# Check 1: current process token is elevated (Run as Administrator)
|
|
||||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
||||||
$p = New-Object Security.Principal.WindowsPrincipal($id)
|
|
||||||
if ($p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { return $true }
|
|
||||||
# Check 2: user is a member of BUILTIN\Administrators group (SID S-1-5-32-544).
|
|
||||||
# Under UAC the token is filtered so IsInRole returns false even for admin group members.
|
|
||||||
# Comparing .Value (string form of SID) is reliable regardless of token filtering.
|
|
||||||
$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
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-UacSummary {
|
|
||||||
$base = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System"
|
|
||||||
$get = {
|
|
||||||
param($name)
|
|
||||||
try { (Get-ItemProperty -LiteralPath $base -Name $name -ErrorAction Stop).$name } catch { $null }
|
|
||||||
}
|
|
||||||
[pscustomobject]@{
|
|
||||||
EnableLUA = & $get "EnableLUA"
|
|
||||||
ConsentPromptBehaviorAdmin = & $get "ConsentPromptBehaviorAdmin"
|
|
||||||
PromptOnSecureDesktop = & $get "PromptOnSecureDesktop"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-CommandVersion {
|
|
||||||
param([string]$Name)
|
|
||||||
$cmd = Get-Command $Name -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
||||||
if (-not $cmd) { return $null }
|
|
||||||
$ver = $null
|
|
||||||
try {
|
|
||||||
if ($cmd.CommandType -eq "Application") {
|
|
||||||
$out = & $cmd.Source --version 2>&1
|
|
||||||
if ($out) { $ver = ($out | Out-String).Trim().Split("`n")[0].Trim() }
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
[pscustomobject]@{
|
|
||||||
Path = $cmd.Source
|
|
||||||
Version = $ver
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Search-JsonLikeSettings {
|
|
||||||
param([string]$Path, [string[]]$Keywords)
|
|
||||||
if (-not (Test-Path -LiteralPath $Path)) { return @() }
|
|
||||||
try {
|
|
||||||
$raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
|
|
||||||
$hits = @()
|
|
||||||
foreach ($k in $Keywords) {
|
|
||||||
if ($raw -match [regex]::Escape($k)) { $hits += $k }
|
|
||||||
}
|
|
||||||
return ($hits | Select-Object -Unique)
|
|
||||||
} catch { return @() }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-EnvLikeFileName {
|
|
||||||
param([string]$Name)
|
|
||||||
if ($Name -eq '.env' -or $Name -eq '.env.local') { return $true }
|
|
||||||
if ($Name -like '.env.*') { return $true }
|
|
||||||
if ($Name -like '*.env') { return $true }
|
|
||||||
if ($Name -like '*.env.local') { return $true }
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Find-EnvLikeFiles {
|
|
||||||
param([string]$Root, [int]$MaxFiles = 200, [int]$MaxDepth = 8)
|
|
||||||
$script:envScanSkippedDirs = [System.Collections.Generic.List[string]]::new()
|
|
||||||
if (-not (Test-Path -LiteralPath $Root)) { return @() }
|
|
||||||
$excludeDirs = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
||||||
@(
|
|
||||||
'node_modules', '.git', 'dist', 'build', 'out', '.next', 'coverage',
|
|
||||||
'__pycache__', '.venv', 'venv', '.turbo', '.cache', '.output'
|
|
||||||
) | ForEach-Object { [void]$excludeDirs.Add($_) }
|
|
||||||
|
|
||||||
$found = [System.Collections.Generic.HashSet[string]]::new([StringComparer]::OrdinalIgnoreCase)
|
|
||||||
$rootNorm = (Resolve-Path -LiteralPath $Root).Path.TrimEnd('\')
|
|
||||||
|
|
||||||
function Walk {
|
|
||||||
param([string]$Dir, [int]$Depth)
|
|
||||||
if ($found.Count -ge $MaxFiles) { return }
|
|
||||||
if ($Depth -gt $MaxDepth) { return }
|
|
||||||
try {
|
|
||||||
foreach ($f in @(Get-ChildItem -LiteralPath $Dir -File -Force -ErrorAction Stop)) {
|
|
||||||
if ($found.Count -ge $MaxFiles) { return }
|
|
||||||
if (Test-EnvLikeFileName $f.Name) {
|
|
||||||
$rel = $f.FullName.Substring($rootNorm.Length).TrimStart('\')
|
|
||||||
[void]$found.Add($rel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
foreach ($d in @(Get-ChildItem -LiteralPath $Dir -Directory -Force -ErrorAction Stop)) {
|
|
||||||
if ($excludeDirs.Contains($d.Name)) { continue }
|
|
||||||
Walk $d.FullName ($Depth + 1)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
$relSkip = if ($Dir.Length -gt $rootNorm.Length) { $Dir.Substring($rootNorm.Length).TrimStart('\') } else { '.' }
|
|
||||||
[void]$script:envScanSkippedDirs.Add($relSkip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Walk $rootNorm 0
|
|
||||||
return @($found)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-Listeners {
|
|
||||||
$rows = @()
|
|
||||||
try {
|
|
||||||
$rows = Get-NetTCPConnection -State Listen -ErrorAction Stop |
|
|
||||||
Select-Object @{n = "Address"; e = { $_.LocalAddress } }, @{n = "Port"; e = { $_.LocalPort } }, OwningProcess |
|
|
||||||
Sort-Object Address, Port
|
|
||||||
} catch {
|
|
||||||
# Fallback: netstat -ano. Column 1 is "Address:Port" (one combined field).
|
|
||||||
$net = netstat -ano | Select-String "LISTENING"
|
|
||||||
foreach ($line in $net) {
|
|
||||||
$parts = ($line -split '\s+') | Where-Object { $_ }
|
|
||||||
if ($parts.Length -ge 5) {
|
|
||||||
$localField = $parts[1] # e.g. "0.0.0.0:135" or "[::1]:5432"
|
|
||||||
$lastColon = $localField.LastIndexOf(':')
|
|
||||||
if ($lastColon -ge 0) {
|
|
||||||
$addrOnly = $localField.Substring(0, $lastColon).Trim('[', ']')
|
|
||||||
$portVal = 0
|
|
||||||
if ([int]::TryParse($localField.Substring($lastColon + 1), [ref]$portVal)) {
|
|
||||||
$rows += [pscustomobject]@{ Address = $addrOnly; Port = $portVal; OwningProcess = $parts[-1] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $rows
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-ListenerInsights {
|
|
||||||
param([object[]]$Rows)
|
|
||||||
$Rows = @($Rows)
|
|
||||||
$notablePorts = @{
|
|
||||||
22 = "SSH"
|
|
||||||
80 = "HTTP"
|
|
||||||
443 = "HTTPS"
|
|
||||||
135 = "RPC"
|
|
||||||
139 = "NetBIOS"
|
|
||||||
445 = "SMB"
|
|
||||||
3389 = "RDP"
|
|
||||||
5985 = "WinRM(HTTP)"
|
|
||||||
5986 = "WinRM(HTTPS)"
|
|
||||||
3306 = "MySQL"
|
|
||||||
5432 = "PostgreSQL"
|
|
||||||
6379 = "Redis"
|
|
||||||
27017 = "MongoDB"
|
|
||||||
}
|
|
||||||
$allIf = 0
|
|
||||||
$localOnly = 0
|
|
||||||
$specific = 0
|
|
||||||
$enriched = New-Object System.Collections.Generic.List[object]
|
|
||||||
foreach ($r in $Rows) {
|
|
||||||
if (-not $r) { continue }
|
|
||||||
$addr = [string]$r.Address
|
|
||||||
$port = 0
|
|
||||||
try { $port = [int]$r.Port } catch { }
|
|
||||||
$scope = "specific"
|
|
||||||
if ($addr -eq "0.0.0.0" -or $addr -eq "::" -or $addr -eq "*") {
|
|
||||||
$scope = "allInterfaces"
|
|
||||||
$allIf++
|
|
||||||
}
|
|
||||||
elseif (($addr -match "^127\.") -or $addr -eq "::1" -or $addr -eq "localhost") {
|
|
||||||
$scope = "localhost"
|
|
||||||
$localOnly++
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$specific++
|
|
||||||
}
|
|
||||||
$hint = $null
|
|
||||||
if ($notablePorts.ContainsKey($port)) { $hint = $notablePorts[$port] }
|
|
||||||
$enriched.Add([pscustomobject]@{
|
|
||||||
Address = $r.Address
|
|
||||||
Port = $r.Port
|
|
||||||
OwningProcess = $r.OwningProcess
|
|
||||||
BindScope = $scope
|
|
||||||
WellKnownHint = $hint
|
|
||||||
})
|
|
||||||
}
|
|
||||||
$sample = @($enriched.ToArray() | Select-Object -First 40)
|
|
||||||
return [pscustomobject]@{
|
|
||||||
Summary = [pscustomobject]@{
|
|
||||||
TotalListenRows = @($Rows).Count
|
|
||||||
AllInterfacesCount = $allIf
|
|
||||||
LocalhostOnlyCount = $localOnly
|
|
||||||
OtherAddressCount = $specific
|
|
||||||
}
|
|
||||||
NotesJa = @(
|
|
||||||
"BindScope: allInterfaces は 0.0.0.0 や :: など「全インターフェイス」で待ち受けている行です。LAN 向けに開いている可能性があります(VPN・開発ツールではよくあります)。",
|
|
||||||
"WellKnownHint はポート番号から一般的な用途を示す目安です。別アプリが同じ番号を使うこともあります。",
|
|
||||||
"安全・危険の断定はしません。気になる行があればエンジニアまたは社内 IT に相談してください。"
|
|
||||||
)
|
|
||||||
RowsEnrichedSample = $sample
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-AuditDiff {
|
|
||||||
param(
|
|
||||||
[System.Collections.IDictionary]$Current,
|
|
||||||
$Previous
|
|
||||||
)
|
|
||||||
if (-not $Previous) {
|
|
||||||
return [pscustomobject]@{
|
|
||||||
HasPrevious = $false
|
|
||||||
NoteJa = "前回の latest.json が無かったため、差分はありません。次回実行から、ひとつ前の実行結果との差分が付きます。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-TcpKeys {
|
|
||||||
param($Net)
|
|
||||||
if (-not $Net -or -not $Net.TcpListeners) { return @() }
|
|
||||||
$keys = New-Object System.Collections.Generic.HashSet[string]
|
|
||||||
foreach ($x in @($Net.TcpListeners)) {
|
|
||||||
if (-not $x) { continue }
|
|
||||||
[void]$keys.Add("$($x.Address):$($x.Port)")
|
|
||||||
}
|
|
||||||
return @($keys | Sort-Object)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-RunNames {
|
|
||||||
param($RunArray)
|
|
||||||
if (-not $RunArray) { return @() }
|
|
||||||
$s = New-Object System.Collections.Generic.HashSet[string]
|
|
||||||
foreach ($x in @($RunArray)) {
|
|
||||||
if ($x.Name) { [void]$s.Add([string]$x.Name) }
|
|
||||||
}
|
|
||||||
return @($s | Sort-Object)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-EnvSet {
|
|
||||||
param($Scan)
|
|
||||||
if (-not $Scan -or -not $Scan.EnvLikeRelPaths) { return @() }
|
|
||||||
return @($Scan.EnvLikeRelPaths | Sort-Object)
|
|
||||||
}
|
|
||||||
|
|
||||||
function Compare-StringSet {
|
|
||||||
param([string[]]$Old, [string[]]$New)
|
|
||||||
$oldH = @{}; foreach ($x in $Old) { $oldH[$x] = $true }
|
|
||||||
$newH = @{}; foreach ($x in $New) { $newH[$x] = $true }
|
|
||||||
$added = @($New | Where-Object { -not $oldH.ContainsKey($_) })
|
|
||||||
$removed = @($Old | Where-Object { -not $newH.ContainsKey($_) })
|
|
||||||
return [pscustomobject]@{ Added = $added; Removed = $removed }
|
|
||||||
}
|
|
||||||
|
|
||||||
$prevGen = $null
|
|
||||||
try { $prevGen = $Previous.generatedAtUtc } catch {}
|
|
||||||
|
|
||||||
$tcpOld = Get-TcpKeys $Previous.network
|
|
||||||
$tcpNew = Get-TcpKeys $Current['network']
|
|
||||||
$tcpDiff = Compare-StringSet -Old $tcpOld -New $tcpNew
|
|
||||||
|
|
||||||
$envOld = Get-EnvSet $Previous.projectScan
|
|
||||||
$envNew = Get-EnvSet $Current['projectScan']
|
|
||||||
$envDiff = Compare-StringSet -Old $envOld -New $envNew
|
|
||||||
|
|
||||||
$hkcuOld = Get-RunNames $Previous.registryRun.HKCU_Run
|
|
||||||
$hkcuNew = Get-RunNames $Current['registryRun'].HKCU_Run
|
|
||||||
$hkcuDiff = Compare-StringSet -Old $hkcuOld -New $hkcuNew
|
|
||||||
|
|
||||||
$hklmOld = Get-RunNames $Previous.registryRun.HKLM_Run
|
|
||||||
$hklmNew = Get-RunNames $Current['registryRun'].HKLM_Run
|
|
||||||
$hklmDiff = Compare-StringSet -Old $hklmOld -New $hklmNew
|
|
||||||
|
|
||||||
$adminCh = $null
|
|
||||||
try {
|
|
||||||
$a0 = $Previous.userContext.IsAdmin
|
|
||||||
$a1 = $Current['userContext'].IsAdmin
|
|
||||||
if ($a0 -ne $a1) {
|
|
||||||
$adminCh = [pscustomobject]@{ Previous = [bool]$a0; Current = [bool]$a1 }
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
$luaCh = $null
|
|
||||||
try {
|
|
||||||
$u0 = $Previous.uac.EnableLUA
|
|
||||||
$u1 = $Current['uac'].EnableLUA
|
|
||||||
if ($u0 -ne $u1) {
|
|
||||||
$luaCh = [pscustomobject]@{ Previous = $u0; Current = $u1 }
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
return [pscustomobject]@{
|
|
||||||
HasPrevious = $true
|
|
||||||
PreviousGeneratedAtUtc = $prevGen
|
|
||||||
TcpListeners = [pscustomobject]@{
|
|
||||||
AddedCount = @($tcpDiff.Added).Count
|
|
||||||
RemovedCount = @($tcpDiff.Removed).Count
|
|
||||||
AddedSample = @($tcpDiff.Added | Select-Object -First 40)
|
|
||||||
RemovedSample = @($tcpDiff.Removed | Select-Object -First 40)
|
|
||||||
}
|
|
||||||
EnvLikeRelPaths = [pscustomobject]@{
|
|
||||||
AddedCount = @($envDiff.Added).Count
|
|
||||||
RemovedCount = @($envDiff.Removed).Count
|
|
||||||
AddedSample = @($envDiff.Added | Select-Object -First 30)
|
|
||||||
RemovedSample = @($envDiff.Removed | Select-Object -First 30)
|
|
||||||
}
|
|
||||||
HKCU_Run = [pscustomobject]@{
|
|
||||||
AddedCount = @($hkcuDiff.Added).Count
|
|
||||||
RemovedCount = @($hkcuDiff.Removed).Count
|
|
||||||
AddedSample = @($hkcuDiff.Added | Select-Object -First 25)
|
|
||||||
RemovedSample = @($hkcuDiff.Removed | Select-Object -First 25)
|
|
||||||
}
|
|
||||||
HKLM_Run = [pscustomobject]@{
|
|
||||||
AddedCount = @($hklmDiff.Added).Count
|
|
||||||
RemovedCount = @($hklmDiff.Removed).Count
|
|
||||||
AddedSample = @($hklmDiff.Added | Select-Object -First 25)
|
|
||||||
RemovedSample = @($hklmDiff.Removed | Select-Object -First 25)
|
|
||||||
}
|
|
||||||
UserIsAdminChanged = $adminCh
|
|
||||||
UacEnableLuaChanged = $luaCh
|
|
||||||
NoteJa = "差分は「ひとつ前の実行(previous.json と同内容のスナップショット)」との比較です。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-RegistryRunValues {
|
|
||||||
param([string]$HivePath)
|
|
||||||
if (-not (Test-Path -LiteralPath $HivePath)) { return @() }
|
|
||||||
try {
|
|
||||||
Get-ItemProperty -LiteralPath $HivePath -ErrorAction Stop |
|
|
||||||
Get-Member -MemberType NoteProperty |
|
|
||||||
Where-Object { $_.Name -notmatch '^(PSPath|PSParentPath|PSChildName|PSDrive|PSProvider)$' } |
|
|
||||||
ForEach-Object {
|
|
||||||
$name = $_.Name
|
|
||||||
$val = (Get-ItemProperty -LiteralPath $HivePath -Name $name -ErrorAction SilentlyContinue).$name
|
|
||||||
if ($val -is [string] -and $val.Length -gt 0) {
|
|
||||||
[pscustomobject]@{ Name = $name; Value = $val }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { return @() }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SshPublicMetadata {
|
|
||||||
$sshDir = Join-Path $env:USERPROFILE ".ssh"
|
|
||||||
if (-not (Test-Path -LiteralPath $sshDir)) {
|
|
||||||
return [pscustomobject]@{ DirectoryExists = $false; Files = @() }
|
|
||||||
}
|
|
||||||
$files = Get-ChildItem -LiteralPath $sshDir -File -Force -ErrorAction SilentlyContinue |
|
|
||||||
ForEach-Object {
|
|
||||||
[pscustomobject]@{
|
|
||||||
Name = $_.Name
|
|
||||||
Length = $_.Length
|
|
||||||
LastWriteUtc = $_.LastWriteTimeUtc.ToString("o")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [pscustomobject]@{ DirectoryExists = $true; Files = @($files) }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-GitGlobalSummary {
|
|
||||||
$git = Get-Command git -ErrorAction SilentlyContinue
|
|
||||||
if (-not $git) {
|
|
||||||
return [pscustomobject]@{ GitAvailable = $false }
|
|
||||||
}
|
|
||||||
$helper = ""
|
|
||||||
$userName = ""
|
|
||||||
$userEmail = ""
|
|
||||||
try { $helper = (& git config --global credential.helper 2>$null).Trim() } catch {}
|
|
||||||
try { $userName = (& git config --global user.name 2>$null).Trim() } catch {}
|
|
||||||
try { $userEmail = (& git config --global user.email 2>$null).Trim() } catch {}
|
|
||||||
return [pscustomobject]@{
|
|
||||||
GitAvailable = $true
|
|
||||||
CredentialHelper = $helper
|
|
||||||
UserName = $userName
|
|
||||||
UserEmail = $userEmail
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-IdePaths {
|
|
||||||
param([switch]$ShareSafe)
|
|
||||||
$claudeKeys = @{
|
|
||||||
ClaudeUserSettings = $true; ClaudeUserLocal = $true; ProjectClaude = $true; ProjectClaudeLocal = $true
|
|
||||||
}
|
|
||||||
$pairs = @(
|
|
||||||
@{ Key = "CursorUserSettings"; Path = (Join-Path $env:APPDATA "Cursor\User\settings.json") },
|
|
||||||
@{ Key = "VsCodeUserSettings"; Path = (Join-Path $env:APPDATA "Code\User\settings.json") },
|
|
||||||
@{ Key = "CursorCliConfig"; Path = (Join-Path $env:USERPROFILE ".cursor\cli-config.json") },
|
|
||||||
@{ Key = "ClaudeUserSettings"; Path = (Join-Path $env:USERPROFILE ".claude\settings.json") },
|
|
||||||
@{ Key = "ClaudeUserLocal"; Path = (Join-Path $env:USERPROFILE ".claude\settings.local.json") },
|
|
||||||
@{ Key = "ProjectClaude"; Path = (Join-Path $ProjectRoot ".claude\settings.json") },
|
|
||||||
@{ Key = "ProjectClaudeLocal"; Path = (Join-Path $ProjectRoot ".claude\settings.local.json") }
|
|
||||||
)
|
|
||||||
$kw = @("mcpServers", "permissions", "dangerouslyAllowedPaths", "autoApprove", "enableTerminalIntegration")
|
|
||||||
$out = [ordered]@{}
|
|
||||||
foreach ($pair in $pairs) {
|
|
||||||
$p = $pair.Path
|
|
||||||
$exists = Test-Path -LiteralPath $p
|
|
||||||
$isClaude = $claudeKeys.ContainsKey($pair.Key)
|
|
||||||
if ($ShareSafe -and $isClaude) {
|
|
||||||
$pathOut = if ($pair.Key -like "Project*") {
|
|
||||||
"<project-root>\\.claude\\(settings file)"
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
"%USERPROFILE%\\.claude\\(settings file)"
|
|
||||||
}
|
|
||||||
$out[$pair.Key] = [pscustomobject]@{
|
|
||||||
Path = $pathOut
|
|
||||||
Exists = $exists
|
|
||||||
KeywordHits = @()
|
|
||||||
ShareSafeRedacted = $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$hits = if ($exists) { Search-JsonLikeSettings -Path $p -Keywords $kw } else { @() }
|
|
||||||
$out[$pair.Key] = [pscustomobject]@{
|
|
||||||
Path = $p
|
|
||||||
Exists = $exists
|
|
||||||
KeywordHits = @($hits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $out
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-BitLockerBrief {
|
|
||||||
try {
|
|
||||||
$v = Get-BitLockerVolume -ErrorAction Stop |
|
|
||||||
Select-Object MountPoint, VolumeStatus, ProtectionStatus
|
|
||||||
return @($v)
|
|
||||||
} catch { return @() }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-ClaudeDenyHint {
|
|
||||||
param([string]$Path)
|
|
||||||
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
|
||||||
try {
|
|
||||||
$raw = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
|
|
||||||
if ($raw -notmatch '"deny"') {
|
|
||||||
return "no_deny_key"
|
|
||||||
}
|
|
||||||
if ($raw -match '"deny"\s*:\s*\[\s*\]') {
|
|
||||||
return "deny_empty_array"
|
|
||||||
}
|
|
||||||
return "deny_configured"
|
|
||||||
} catch { return $null }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-JapaneseGuidance {
|
|
||||||
param(
|
|
||||||
[System.Collections.IDictionary]$A,
|
|
||||||
[switch]$ShareSafe
|
|
||||||
)
|
|
||||||
$L = New-Object System.Collections.Generic.List[object]
|
|
||||||
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "このレポートの位置づけ"
|
|
||||||
Body = "ここに示すのは、取得した事実に基づく補足説明です。セキュリティ製品の代替や、確定的な安全診断ではありません。会社の規程や情報システム担当への確認が必要な場合は、そちらを優先してください。"
|
|
||||||
})
|
|
||||||
|
|
||||||
if ($ShareSafe) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "共有安全モード(ShareSafe)"
|
|
||||||
Body = "社外・社内 IT などへファイルを渡す想定で、.claude 配下のフルパスと、設定ファイルから拾うキーワード一覧を出していません。AI ツールの権限まわりに踏み込む説明も省略しています。詳細は手元のフル監査(ShareSafe なし)でエンジニアが確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($A['userContext'].IsAdmin -eq $true) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "warn"
|
|
||||||
Title = "管理者に近い権限で動いている可能性"
|
|
||||||
Body = "管理者グループに属していると検出されました。操作ミスや自動実行ツール(AI のターミナルなど)の影響が広がりやすいため、日常作業は権限の狭いユーザーで行い、必要なときだけ管理者権限を使う運用が一般的に推奨されます。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "ok"
|
|
||||||
Title = "管理者ロール"
|
|
||||||
Body = "この監査を実行したユーザーに、組み込みの Administrators ロールは検出されませんでした(常に正しいとは限りません)。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$uac = $A['uac']
|
|
||||||
if ($null -ne $uac.EnableLUA -and [int]$uac.EnableLUA -eq 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "warn"
|
|
||||||
Title = "UAC(EnableLUA)が無効に近い値"
|
|
||||||
Body = "EnableLUA が 0 に見えます。ユーザーアカウント制御が弱い構成だと、不正ソフトや誤操作の影響を受けやすくなることがあります。意図した設定か、社内ポリシーと照らして確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if ($null -ne $uac.ConsentPromptBehaviorAdmin -and [int]$uac.ConsentPromptBehaviorAdmin -eq 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "warn"
|
|
||||||
Title = "UAC: 管理者昇格が自動承認(プロンプトなし)に設定されています"
|
|
||||||
Body = "ConsentPromptBehaviorAdmin が 0 の場合、管理者への昇格がダイアログなしで自動的に許可されます。EnableLUA が有効でも実質的に UAC の確認が省略される状態です。意図した設定か、社内ポリシーと照らして確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$bl = @($A['bitLocker'])
|
|
||||||
if ($bl.Count -eq 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "ディスク暗号化(BitLocker)の情報が取れませんでした"
|
|
||||||
Body = "権限やエディションにより取得できない場合があります。機器紛失時のリスクを減らすには暗号化が有効か、会社の基準と合わせて別途確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$lis = @()
|
|
||||||
try {
|
|
||||||
$lis = @($A['network'].TcpListeners)
|
|
||||||
} catch { }
|
|
||||||
|
|
||||||
$diff = $null
|
|
||||||
try { $diff = $A['diffFromPrevious'] } catch { }
|
|
||||||
if ($diff -and $diff.HasPrevious -eq $true) {
|
|
||||||
$tch = $diff.TcpListeners
|
|
||||||
if ($tch -and ($tch.AddedCount -gt 0 -or $tch.RemovedCount -gt 0)) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "前回実行から TCP 待ち受けの一覧が変わりました"
|
|
||||||
Body = "追加 $($tch.AddedCount) 行 / なくなった行 $($tch.RemovedCount) 行(Address:Port の組み合わせで比較)。VPN・Docker・開発サーバの起動停止で変わることがあります。詳細は diffFromPrevious を参照してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
$ed = $diff.EnvLikeRelPaths
|
|
||||||
if ($ed -and ($ed.AddedCount -gt 0 -or $ed.RemovedCount -gt 0)) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "前回から env 系ファイル名の一覧が変わりました"
|
|
||||||
Body = "追加 $($ed.AddedCount) / なくなった $($ed.RemovedCount)(ファイル名のみの比較)。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($lis.Count -gt 45) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "待ち受け(LISTEN)の数が多めです"
|
|
||||||
Body = "開いているポートが多いと、把握漏れや不要な公開につながることがあります。一覧をエンジニアまたは情報システムに見てもらい、不要なサービスがないか確認する価値があります。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sum = $A['network'].ListenerInsights.Summary
|
|
||||||
if ($sum -and $sum.AllInterfacesCount -gt 15) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "「全インターフェイス」向けの待ち受け行がまとまっています"
|
|
||||||
Body = "ListenerInsights の BindScope が allInterfaces の行が $($sum.AllInterfacesCount) 行あります。LAN 側から届く可能性があるソケットが含まれることがあり、開発用途でも把握しておくと安心です。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
$ssh = $A['ssh']
|
|
||||||
if ($ssh.DirectoryExists -and @($ssh.Files).Count -gt 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "SSH 用のファイルが ~/.ssh にあります"
|
|
||||||
Body = "鍵や設定ファイルが存在します(中身はこのツールでは読んでいません)。バックアップの有無・他者への共有・漏えい時の対応を、社内ルールに沿って確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$git = $A['git']
|
|
||||||
if ($git.GitAvailable -and $git.CredentialHelper.Length -gt 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "Git の資格情報ヘルパーが設定されています"
|
|
||||||
Body = "credential.helper が設定されていると、認証情報が OS や別ストアに保存されることがあります。会社の開発ポリシーと矛盾がないか、必要なら窓口に確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $ShareSafe) {
|
|
||||||
$projClaudeLocal = $A['ideAndAI']['ProjectClaudeLocal']
|
|
||||||
if ($projClaudeLocal -and $projClaudeLocal.Path -and ($projClaudeLocal.Path -notlike "<*") -and $projClaudeLocal.Exists) {
|
|
||||||
$hint = Get-ClaudeDenyHint -Path $projClaudeLocal.Path
|
|
||||||
if ($hint -eq "deny_empty_array") {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "warn"
|
|
||||||
Title = "Claude Code の permissions.deny が空です(プロジェクト内)"
|
|
||||||
Body = '.claude/settings.local.json に "deny": [] のように空配列が含まれる場合、意図せずプロジェクト外への操作が許されやすくなることがあります。エンジニアと相談し、不要な操作は deny で明示する運用を検討してください。'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
elseif ($hint -eq "no_deny_key") {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "Claude Code 設定に deny キーが見当たりません"
|
|
||||||
Body = "ファイル構成によっては問題ない場合もあります。プロジェクト外へ触らせたくない方針なら、deny の有無をエンジニアに確認してください。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$envs = @($A['projectScan'].EnvLikeRelPaths)
|
|
||||||
if ($envs.Count -gt 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "環境変数ファイルに相当する名前のファイルが見つかりました"
|
|
||||||
Body = '.env などのファイル名だけを列挙しています(中身は読んでいません)。API キーなどが入ることが多いので、Git に上げていないか、共有範囲が適切かをエンジニアと確認してください。'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$skipped = @($A['projectScan'].ScanSkippedDirs)
|
|
||||||
if ($skipped.Count -gt 0) {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "env 系スキャンで読み取れなかったフォルダがあります"
|
|
||||||
Body = "アクセス権限などの理由でスキャンできなかったフォルダが $($skipped.Count) 件ありました(詳細: projectScan.ScanSkippedDirs)。そのフォルダに .env ファイルが存在しても、このレポートには表示されません。"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
$claudeIdeKeys = @{ ClaudeUserSettings = $true; ClaudeUserLocal = $true; ProjectClaude = $true; ProjectClaudeLocal = $true }
|
|
||||||
$ide = $A['ideAndAI']
|
|
||||||
foreach ($k in @($ide.Keys)) {
|
|
||||||
$e = $ide[$k]
|
|
||||||
if (-not $e.Exists) { continue }
|
|
||||||
if ($ShareSafe -and $claudeIdeKeys.ContainsKey($k)) { continue }
|
|
||||||
if ($e.KeywordHits -contains "mcpServers") {
|
|
||||||
$L.Add([pscustomobject]@{
|
|
||||||
Level = "info"
|
|
||||||
Title = "設定ファイルに MCP 関連の記述があります ($k)"
|
|
||||||
Body = "MCP(外部ツール連携)は便利ですが、接続先や権限によっては想定外の操作につながることがあります。どの MCP が有効か、エンジニアに説明を求めると安心です。"
|
|
||||||
})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# PS 5.1: @($List[object]) throws type mismatch; ToArray() is safe.
|
|
||||||
return $L.ToArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
$audit = [ordered]@{
|
|
||||||
generatedAtUtc = (Get-Date).ToUniversalTime().ToString("o")
|
|
||||||
machine = [pscustomobject]@{
|
|
||||||
Name = $env:COMPUTERNAME
|
|
||||||
OS = [System.Environment]::OSVersion.VersionString
|
|
||||||
}
|
|
||||||
userContext = [pscustomobject]@{
|
|
||||||
UserName = $env:USERNAME
|
|
||||||
UserProfile = $env:USERPROFILE
|
|
||||||
IsAdmin = (Test-IsAdmin)
|
|
||||||
}
|
|
||||||
uac = Get-UacSummary
|
|
||||||
bitLocker = Get-BitLockerBrief
|
|
||||||
registryRun = [pscustomobject]@{
|
|
||||||
HKCU_Run = @(Get-RegistryRunValues "HKCU:\Software\Microsoft\Windows\CurrentVersion\Run")
|
|
||||||
HKLM_Run = @(Get-RegistryRunValues "HKLM:\Software\Microsoft\Windows\CurrentVersion\Run")
|
|
||||||
}
|
|
||||||
network = $( $tcpRows = @(Get-Listeners); [pscustomobject]@{
|
|
||||||
TcpListeners = $tcpRows
|
|
||||||
ListenerInsights = (Get-ListenerInsights -Rows $tcpRows)
|
|
||||||
} )
|
|
||||||
ssh = Get-SshPublicMetadata
|
|
||||||
git = Get-GitGlobalSummary
|
|
||||||
toolchain = [pscustomobject]@{
|
|
||||||
Node = Get-CommandVersion "node"
|
|
||||||
Npm = Get-CommandVersion "npm"
|
|
||||||
Git = Get-CommandVersion "git"
|
|
||||||
Python = Get-CommandVersion "python"
|
|
||||||
}
|
|
||||||
ideAndAI = (Get-IdePaths -ShareSafe:$ShareSafe)
|
|
||||||
projectScan = [pscustomobject]@{
|
|
||||||
Root = $ProjectRoot
|
|
||||||
EnvLikeRelPaths = @(Find-EnvLikeFiles -Root $ProjectRoot)
|
|
||||||
ScanSkippedDirs = @($script:envScanSkippedDirs)
|
|
||||||
Note = "Secret contents are never read. Only paths under -ProjectRoot."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$audit['diffFromPrevious'] = Get-AuditDiff -Current $audit -Previous $script:prevAuditObj
|
|
||||||
|
|
||||||
$disclaimerJa = "このレポートは補助的な説明であり、セキュリティ診断やコンプライアンス判定に代わるものではありません。"
|
|
||||||
$audit['meta'] = [ordered]@{
|
|
||||||
guidanceDisclaimerJa = $disclaimerJa
|
|
||||||
viewerRelativePath = "tools/pc-audit/report-viewer.html"
|
|
||||||
shareSafe = [bool]$ShareSafe
|
|
||||||
}
|
|
||||||
$audit['guidanceJa'] = @(Get-JapaneseGuidance -A $audit -ShareSafe:$ShareSafe)
|
|
||||||
|
|
||||||
$stamp = (Get-Date).ToUniversalTime().ToString("yyyyMMdd'T'HHmmss'Z'")
|
|
||||||
$jsonPath = Join-Path $OutDir "pc-audit-$stamp.json"
|
|
||||||
$mdPath = Join-Path $OutDir "pc-audit-$stamp.md"
|
|
||||||
$latestPath = Join-Path $OutDir "latest.json"
|
|
||||||
|
|
||||||
$json = $audit | ConvertTo-Json -Depth 12
|
|
||||||
Set-Content -LiteralPath $jsonPath -Value $json -Encoding UTF8
|
|
||||||
Copy-Item -LiteralPath $jsonPath -Destination $latestPath -Force
|
|
||||||
|
|
||||||
# Keep only the 10 most recent timestamped reports; delete older ones.
|
|
||||||
$maxReports = 10
|
|
||||||
@(Get-ChildItem -LiteralPath $OutDir -Filter "pc-audit-*.json" -File -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object Name -Descending | Select-Object -Skip $maxReports) |
|
|
||||||
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue }
|
|
||||||
@(Get-ChildItem -LiteralPath $OutDir -Filter "pc-audit-*.md" -File -ErrorAction SilentlyContinue |
|
|
||||||
Sort-Object Name -Descending | Select-Object -Skip $maxReports) |
|
|
||||||
ForEach-Object { Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue }
|
|
||||||
|
|
||||||
function ConvertTo-MarkdownReport {
|
|
||||||
param($Obj)
|
|
||||||
$sb = [System.Text.StringBuilder]::new()
|
|
||||||
[void]$sb.AppendLine("# PC audit (read-only)")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("Generated (UTC): $($Obj.generatedAtUtc)")
|
|
||||||
if ($Obj.meta -and $Obj.meta.shareSafe -eq $true) {
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("ShareSafe: true(.claude 詳細は匿名化)")
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## かんたん解説(非エンジニア向け)")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine($Obj.meta.guidanceDisclaimerJa)
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
foreach ($g in @($Obj.guidanceJa)) {
|
|
||||||
$label = switch ($g.Level) {
|
|
||||||
"warn" { "注意" }
|
|
||||||
"ok" { "問題なさそう" }
|
|
||||||
Default { "情報" }
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("### [$label] $($g.Title)")
|
|
||||||
[void]$sb.AppendLine("$($g.Body)")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("## User / privilege")
|
|
||||||
[void]$sb.AppendLine("- User: ``$($Obj.userContext.UserName)``")
|
|
||||||
[void]$sb.AppendLine("- Admin: ``$($Obj.userContext.IsAdmin)``")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## UAC (registry)")
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine(($Obj.uac | ConvertTo-Json -Compress))
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## TCP listeners (sample)")
|
|
||||||
$lis = @()
|
|
||||||
if ($Obj.network.TcpListeners) {
|
|
||||||
$lis = @($Obj.network.TcpListeners | Select-Object -First 40)
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("| Address | Port | PID |")
|
|
||||||
[void]$sb.AppendLine("|---------|------|-----|")
|
|
||||||
foreach ($r in $lis) {
|
|
||||||
[void]$sb.AppendLine("| $($r.Address) | $($r.Port) | $($r.OwningProcess) |")
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
if ($Obj.network.ListenerInsights -and $Obj.network.ListenerInsights.Summary) {
|
|
||||||
$ins = $Obj.network.ListenerInsights.Summary
|
|
||||||
[void]$sb.AppendLine("### LISTEN の見方(要約)")
|
|
||||||
[void]$sb.AppendLine("- 全インターフェイス待ち受け行数: ``$($ins.AllInterfacesCount)`` / localhost のみ行数: ``$($ins.LocalhostOnlyCount)`` / 合計行: ``$($ins.TotalListenRows)``")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
}
|
|
||||||
if ($Obj.diffFromPrevious) {
|
|
||||||
[void]$sb.AppendLine("## 前回実行からの差分(概要)")
|
|
||||||
$df = $Obj.diffFromPrevious
|
|
||||||
if (-not $df.HasPrevious) {
|
|
||||||
[void]$sb.AppendLine($df.NoteJa)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
[void]$sb.AppendLine("- 比較元(前回の実行時刻 UTC): ``$($df.PreviousGeneratedAtUtc)``")
|
|
||||||
[void]$sb.AppendLine("- TCP Address:Port: 追加 $($df.TcpListeners.AddedCount) / 削除 $($df.TcpListeners.RemovedCount)")
|
|
||||||
[void]$sb.AppendLine("- env 系パス: 追加 $($df.EnvLikeRelPaths.AddedCount) / 削除 $($df.EnvLikeRelPaths.RemovedCount)")
|
|
||||||
[void]$sb.AppendLine("- HKCU Run 名: 追加 $($df.HKCU_Run.AddedCount) / 削除 $($df.HKCU_Run.RemovedCount)")
|
|
||||||
[void]$sb.AppendLine("- HKLM Run 名: 追加 $($df.HKLM_Run.AddedCount) / 削除 $($df.HKLM_Run.RemovedCount)")
|
|
||||||
if ($df.UserIsAdminChanged) {
|
|
||||||
[void]$sb.AppendLine("- 管理者ロールの検出結果が前回と変わりました(要確認)")
|
|
||||||
}
|
|
||||||
if ($df.UacEnableLuaChanged) {
|
|
||||||
[void]$sb.AppendLine("- UAC EnableLUA の値が前回と変わりました(要確認)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("## SSH directory (names only)")
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine(($Obj.ssh | ConvertTo-Json -Depth 6 -Compress))
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## Git global (no remote URLs)")
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine(($Obj.git | ConvertTo-Json -Compress))
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## Toolchain")
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine(($Obj.toolchain | ConvertTo-Json -Depth 4 -Compress))
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## IDE / AI settings paths")
|
|
||||||
foreach ($k in @($Obj.ideAndAI.Keys)) {
|
|
||||||
$e = $Obj.ideAndAI[$k]
|
|
||||||
[void]$sb.AppendLine("- **$k**: exists=$($e.Exists); keyword hits: $($e.KeywordHits -join ', ')")
|
|
||||||
[void]$sb.AppendLine(" - ``$($e.Path)``")
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## Project env-like files (relative)")
|
|
||||||
[void]$sb.AppendLine("- Root: ``$($Obj.projectScan.Root)``")
|
|
||||||
foreach ($p in $Obj.projectScan.EnvLikeRelPaths) {
|
|
||||||
[void]$sb.AppendLine("- ``$p``")
|
|
||||||
}
|
|
||||||
[void]$sb.AppendLine("")
|
|
||||||
[void]$sb.AppendLine("## Run keys (HKCU / HKLM)")
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
[void]$sb.AppendLine(($Obj.registryRun | ConvertTo-Json -Depth 6 -Compress))
|
|
||||||
[void]$sb.AppendLine("``````")
|
|
||||||
return $sb.ToString()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Format -eq "md" -or $Format -eq "both") {
|
|
||||||
$md = ConvertTo-MarkdownReport -Obj $audit
|
|
||||||
Set-Content -LiteralPath $mdPath -Value $md -Encoding UTF8
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "PC audit complete."
|
|
||||||
Write-Host " JSON: $jsonPath"
|
|
||||||
Write-Host " latest.json: $latestPath"
|
|
||||||
if ($Format -eq "md" -or $Format -eq "both") {
|
|
||||||
Write-Host " MD: $mdPath"
|
|
||||||
}
|
|
||||||
$viewerPath = Join-Path $repoRoot "tools\pc-audit\report-viewer.html"
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "ブラウザで見る: エクスプローラーで次の HTML を開き、同じく out フォルダの JSON を選んでください。"
|
|
||||||
Write-Host " $viewerPath"
|
|
||||||
if ($ShareSafe) {
|
|
||||||
Write-Host ""
|
|
||||||
Write-Host "ShareSafe: .claude のフルパス・キーワードヒット・deny 関連ガイダンスを省略しています。"
|
|
||||||
}
|
|
||||||
|
|
@ -1,464 +1,351 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ja" data-theme="dark">
|
<html lang="ja">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>PC Audit — Posimai</title>
|
<title>PC audit viewer(ローカル専用)</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root { font-family: system-ui, sans-serif; line-height: 1.5; color: #1a1a1a; background: #f5f5f5; }
|
||||||
--bg: #0D0D0D;
|
body { max-width: 52rem; margin: 0 auto; padding: 1rem 1.25rem 3rem; }
|
||||||
--surface: #1A1A1A;
|
h1 { font-size: 1.25rem; }
|
||||||
--surface2: #252525;
|
h2 { font-size: 1.05rem; margin: 0 0 0.35rem; }
|
||||||
--border: #2D2D2D;
|
.box { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 1rem 0; }
|
||||||
--text: #F3F4F6;
|
.warn { border-left: 4px solid #c45c00; }
|
||||||
--text2: #9CA3AF;
|
.ok { border-left: 4px solid #2d6a4f; }
|
||||||
--text3: #6B7280;
|
.info { border-left: 4px solid #1d4ed8; }
|
||||||
--accent: #6EE7B7;
|
.muted { color: #555; font-size: 0.9rem; }
|
||||||
--warn-color: #F59E0B;
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(11rem, 1fr)); gap: 0.5rem 1rem; }
|
||||||
--warn-dim: rgba(245,158,11,0.08);
|
.k { font-size: 0.8rem; color: #555; }
|
||||||
--warn-border: rgba(245,158,11,0.2);
|
.v { font-weight: 600; word-break: break-all; }
|
||||||
--ok-color: #6EE7B7;
|
pre { overflow: auto; font-size: 0.8rem; background: #fafafa; padding: 0.75rem; border-radius: 6px; border: 1px solid #eee; }
|
||||||
--ok-dim: rgba(110,231,183,0.06);
|
label { display: block; margin-bottom: 0.35rem; font-weight: 600; }
|
||||||
--ok-border: rgba(110,231,183,0.2);
|
input[type="file"] { margin-bottom: 0.5rem; }
|
||||||
--info-color: #60A5FA;
|
summary { cursor: pointer; font-weight: 600; }
|
||||||
--info-dim: rgba(96,165,250,0.06);
|
|
||||||
--info-border: rgba(96,165,250,0.2);
|
|
||||||
--radius: 12px;
|
|
||||||
--radius-sm: 8px;
|
|
||||||
}
|
|
||||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
line-height: 1.6;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
.page { max-width: 1280px; padding: 2rem 1.5rem 4rem; }
|
|
||||||
|
|
||||||
.header { margin-bottom: 2rem; }
|
|
||||||
.header h1 { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.05em; color: var(--text2); }
|
|
||||||
.header h1 span { color: var(--accent); }
|
|
||||||
.privacy-note {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
color: var(--text3);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
|
||||||
.privacy-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); flex-shrink: 0; }
|
|
||||||
|
|
||||||
/* 3-column grid — collapses to 1 col on mobile */
|
|
||||||
.grid3 {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 280px 1fr 1fr;
|
|
||||||
gap: 1.25rem;
|
|
||||||
align-items: start;
|
|
||||||
}
|
|
||||||
@media (max-width: 860px) {
|
|
||||||
.grid3 { grid-template-columns: 1fr; }
|
|
||||||
.page { padding: 1rem 1rem 3rem; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Upload area */
|
|
||||||
.upload-area {
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px dashed var(--border);
|
|
||||||
border-radius: var(--radius);
|
|
||||||
padding: 1.25rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
.upload-label { font-size: 0.82rem; font-weight: 600; color: var(--text2); margin-bottom: 0.4rem; display: block; }
|
|
||||||
.upload-path {
|
|
||||||
font-size: 0.75rem; color: var(--text3); background: var(--surface2);
|
|
||||||
padding: 0.25rem 0.5rem; border-radius: var(--radius-sm); font-family: monospace;
|
|
||||||
display: inline-block; margin-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
input[type="file"] { font-size: 0.82rem; color: var(--text2); cursor: pointer; max-width: 100%; }
|
|
||||||
input[type="file"]::file-selector-button {
|
|
||||||
background: var(--surface2); color: var(--text); border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm); padding: 0.3rem 0.75rem; font-size: 0.78rem;
|
|
||||||
cursor: pointer; margin-right: 0.5rem; margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Section label */
|
|
||||||
.section-title {
|
|
||||||
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase; color: var(--text3); margin: 0 0 0.6rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Metric grid */
|
|
||||||
.summary-grid { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 0.75rem; }
|
|
||||||
.metric {
|
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius-sm); padding: 0.7rem 0.9rem;
|
|
||||||
}
|
|
||||||
.metric-label { font-size: 0.7rem; color: var(--text3); margin-bottom: 0.15rem; }
|
|
||||||
.metric-value { font-size: 0.9rem; font-weight: 600; color: var(--text); word-break: break-all; }
|
|
||||||
|
|
||||||
/* Diff card */
|
|
||||||
.diff-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 1rem 1.1rem; }
|
|
||||||
.diff-card-title { font-size: 0.8rem; font-weight: 600; color: var(--text2); margin-bottom: 0.6rem; }
|
|
||||||
.diff-col { display: flex; flex-direction: column; gap: 0.4rem; }
|
|
||||||
|
|
||||||
/* Cards */
|
|
||||||
.card {
|
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius); padding: 1rem 1.1rem; margin-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
.card.warn { background: var(--warn-dim); border-color: var(--warn-border); }
|
|
||||||
.card.ok { background: var(--ok-dim); border-color: var(--ok-border); }
|
|
||||||
.card.info { background: var(--info-dim); border-color: var(--info-border); }
|
|
||||||
.card-header { display: flex; align-items: flex-start; gap: 0.5rem; margin-bottom: 0.5rem; }
|
|
||||||
.badge {
|
|
||||||
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.06em;
|
|
||||||
padding: 0.18rem 0.5rem; border-radius: 99px; white-space: nowrap;
|
|
||||||
flex-shrink: 0; margin-top: 0.15rem;
|
|
||||||
}
|
|
||||||
.badge-warn { background: rgba(245,158,11,0.15); color: var(--warn-color); }
|
|
||||||
.badge-ok { background: rgba(110,231,183,0.15); color: var(--ok-color); }
|
|
||||||
.badge-info { background: rgba(96,165,250,0.15); color: var(--info-color); }
|
|
||||||
.card-title { font-size: 0.88rem; font-weight: 600; color: var(--text); line-height: 1.4; }
|
|
||||||
.card-body { font-size: 0.82rem; color: var(--text2); line-height: 1.65; }
|
|
||||||
.action-box {
|
|
||||||
margin-top: 0.75rem; background: var(--surface2);
|
|
||||||
border-radius: var(--radius-sm); padding: 0.65rem 0.85rem;
|
|
||||||
font-size: 0.8rem; color: var(--text2);
|
|
||||||
}
|
|
||||||
.action-label {
|
|
||||||
font-size: 0.63rem; font-weight: 700; letter-spacing: 0.1em;
|
|
||||||
color: var(--accent); text-transform: uppercase; display: block; margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Port table */
|
|
||||||
.table-wrap { overflow-x: auto; }
|
|
||||||
table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
|
|
||||||
thead th {
|
|
||||||
text-align: left; padding: 0.45rem 0.6rem;
|
|
||||||
font-size: 0.65rem; font-weight: 700; letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase; color: var(--text3); border-bottom: 1px solid var(--border);
|
|
||||||
}
|
|
||||||
tbody tr { border-bottom: 1px solid var(--border); }
|
|
||||||
tbody tr:last-child { border-bottom: none; }
|
|
||||||
tbody td { padding: 0.4rem 0.6rem; color: var(--text2); vertical-align: top; }
|
|
||||||
.addr-cell { font-family: monospace; font-size: 0.75rem; color: var(--text3); }
|
|
||||||
.port-cell { font-family: monospace; font-weight: 600; color: var(--text); }
|
|
||||||
.scope-all { color: var(--warn-color); font-size: 0.78rem; }
|
|
||||||
.scope-localhost { color: var(--ok-color); font-size: 0.78rem; }
|
|
||||||
.scope-specific { color: var(--info-color); font-size: 0.78rem; }
|
|
||||||
.hint-line { font-size: 0.7rem; color: var(--text3); margin-top: 0.1rem; }
|
|
||||||
|
|
||||||
/* Raw */
|
|
||||||
details {
|
|
||||||
background: var(--surface); border: 1px solid var(--border);
|
|
||||||
border-radius: var(--radius); padding: 0.9rem 1.1rem; margin-top: 1.5rem;
|
|
||||||
}
|
|
||||||
summary { cursor: pointer; font-size: 0.78rem; font-weight: 600; color: var(--text3); user-select: none; }
|
|
||||||
pre { margin-top: 0.6rem; font-size: 0.7rem; color: var(--text3); overflow: auto; max-height: 18rem; line-height: 1.5; }
|
|
||||||
.disclaimer { font-size: 0.73rem; color: var(--text3); border-top: 1px solid var(--border); padding-top: 0.9rem; margin-top: 1.5rem; }
|
|
||||||
|
|
||||||
.col-label { font-size: 0.65rem; font-weight: 700; letter-spacing: 0.1em; text-transform: uppercase; color: var(--text3); margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--border); }
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="page">
|
<h1>PC audit viewer</h1>
|
||||||
<div class="header">
|
<p class="muted">この HTML はブラウザ内だけで動きます。ファイルを選ぶまで外部にデータは送りません。Web 公開はしていません。</p>
|
||||||
<h1>PC Audit <span>/</span> Posimai</h1>
|
|
||||||
<p class="privacy-note"><span class="privacy-dot"></span>ブラウザ内のみで動作します。ファイルを選択しても外部にデータは一切送信されません。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Upload (always shown) -->
|
<div class="box">
|
||||||
<div class="upload-area">
|
<label for="f">JSON レポートを選択</label>
|
||||||
<label class="upload-label" for="f">レポートファイルを選択</label>
|
<p class="muted" style="margin:0 0 0.5rem;font-size:0.85rem;">
|
||||||
<div class="upload-path">tools\pc-audit\out\latest.json</div><br/>
|
場所: <code style="background:#f0f0f0;padding:2px 6px;border-radius:4px;">tools\pc-audit\out\latest.json</code>
|
||||||
<input id="f" type="file" accept=".json,application/json" />
|
(run-audit.bat を実行した後に作られます)
|
||||||
</div>
|
</p>
|
||||||
|
<input id="f" type="file" accept=".json,application/json" />
|
||||||
<!-- 3-column output -->
|
|
||||||
<div id="output" style="display:none">
|
|
||||||
<div class="grid3">
|
|
||||||
<!-- Col 1: summary + diff -->
|
|
||||||
<div>
|
|
||||||
<div class="col-label">概要 / 差分</div>
|
|
||||||
<div id="summary"></div>
|
|
||||||
<div id="diff"></div>
|
|
||||||
</div>
|
|
||||||
<!-- Col 2: check items -->
|
|
||||||
<div>
|
|
||||||
<div class="col-label">チェック項目</div>
|
|
||||||
<div id="guidance"></div>
|
|
||||||
</div>
|
|
||||||
<!-- Col 3: port list -->
|
|
||||||
<div>
|
|
||||||
<div class="col-label">待ち受けポート一覧</div>
|
|
||||||
<div id="listeners"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<details><summary>生データ(JSON 全体)</summary><pre id="raw"></pre></details>
|
|
||||||
<p class="disclaimer">このレポートは補助的な情報です。セキュリティ診断・コンプライアンス判定の代わりにはなりません。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="disclaimer" class="box muted" hidden></div>
|
||||||
|
<div id="summary"></div>
|
||||||
|
<div id="insights"></div>
|
||||||
|
<div id="guidance"></div>
|
||||||
|
<details class="box">
|
||||||
|
<summary>生データ(JSON 全体)</summary>
|
||||||
|
<pre id="raw"></pre>
|
||||||
|
</details>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
var d = document.createElement("div");
|
var d = document.createElement("div");
|
||||||
d.textContent = s == null ? "" : String(s);
|
d.textContent = s == null ? "" : String(s);
|
||||||
return d.innerHTML;
|
return d.innerHTML;
|
||||||
}
|
}
|
||||||
function len(a) { return Array.isArray(a) ? a.length : 0; }
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
function levelClass(level) {
|
||||||
/* Port descriptions */
|
if (level === "warn") return "warn";
|
||||||
/* ------------------------------------------------------------------ */
|
if (level === "ok") return "ok";
|
||||||
var PORT_NOTES = {
|
return "info";
|
||||||
"80": "HTTP(Web サーバー)",
|
}
|
||||||
"135": "Windows RPC — 正常",
|
|
||||||
"139": "NetBIOS ファイル共有 — LAN 内なら正常",
|
|
||||||
"443": "HTTPS(Web サーバー)",
|
|
||||||
"445": "SMB ファイル共有 — LAN 内なら正常",
|
|
||||||
"843": "Adobe Flash 関連またはローカルアプリ",
|
|
||||||
"902": "VMware 管理",
|
|
||||||
"912": "VMware 管理",
|
|
||||||
"1080": "SOCKSプロキシ",
|
|
||||||
"2015": "Caddy 開発サーバー",
|
|
||||||
"3000": "開発用ローカルサーバー(Node 等)— 正常",
|
|
||||||
"3306": "MySQL",
|
|
||||||
"3389": "リモートデスクトップ(RDP)",
|
|
||||||
"5040": "Windows 標準サービス — 正常",
|
|
||||||
"5354": "mDNS(ローカルデバイス検索)— 正常",
|
|
||||||
"5357": "Windows ネットワーク検出 — 正常",
|
|
||||||
"5432": "PostgreSQL",
|
|
||||||
"5900": "VNC リモートデスクトップ",
|
|
||||||
"6379": "Redis",
|
|
||||||
"7680": "Windows Update 配信最適化 — 正常",
|
|
||||||
"8080": "開発用 HTTP サーバー — 正常",
|
|
||||||
"8443": "開発用 HTTPS サーバー",
|
|
||||||
"17500": "Dropbox — インストール済みなら正常",
|
|
||||||
"17600": "Dropbox — インストール済みなら正常",
|
|
||||||
"27015": "Steam — インストール済みなら正常",
|
|
||||||
"27017": "MongoDB",
|
|
||||||
"33060": "MySQL X Protocol"
|
|
||||||
};
|
|
||||||
|
|
||||||
/* Ephemeral port ranges are dynamically assigned by Windows — always normal */
|
function labelJa(level) {
|
||||||
function isEphemeral(port) {
|
if (level === "warn") return "注意";
|
||||||
var p = parseInt(port, 10);
|
if (level === "ok") return "問題なさそう";
|
||||||
return p >= 49152 && p <= 65535;
|
return "情報";
|
||||||
}
|
}
|
||||||
|
|
||||||
function portNote(port, hint) {
|
function len(a) {
|
||||||
if (PORT_NOTES[String(port)]) return PORT_NOTES[String(port)];
|
return Array.isArray(a) ? a.length : 0;
|
||||||
if (hint) return hint;
|
}
|
||||||
if (isEphemeral(port)) return "一時ポート(Windows が一時的に割り当て)— 正常";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
function renderSummary(data) {
|
||||||
/* Address notes */
|
var uc = data.userContext || {};
|
||||||
/* ------------------------------------------------------------------ */
|
var m = data.machine || {};
|
||||||
function addressNote(addr) {
|
var uac = data.uac || {};
|
||||||
if (addr === "::" || addr === "0.0.0.0") return "全インターフェース(Windows 標準)";
|
var net = data.network || {};
|
||||||
if (addr === "::1" || addr === "127.0.0.1") return "localhost — PC 内だけ、安全";
|
var listeners = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
|
||||||
if (/^100\./.test(addr)) return "Tailscale VPN — 正常";
|
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
|
||||||
if (/^192\.168\./.test(addr)) return "LAN — 正常";
|
var hkcu = data.registryRun && data.registryRun.HKCU_Run ? len(data.registryRun.HKCU_Run) : 0;
|
||||||
if (/^fd7a:115c:/.test(addr)) return "Tailscale VPN(IPv6)— 正常";
|
var hklm = data.registryRun && data.registryRun.HKLM_Run ? len(data.registryRun.HKLM_Run) : 0;
|
||||||
if (/^::1$/.test(addr)) return "localhost(IPv6)— 安全";
|
var shareSafe = data.meta && data.meta.shareSafe === true;
|
||||||
return "";
|
var el = document.getElementById("summary");
|
||||||
}
|
el.innerHTML =
|
||||||
|
'<div class="box"><h2>取得結果のあらまし</h2><p class="muted">数値は「このレポートに載った件数」です。良い/悪いの判定そのものではありません。</p>' +
|
||||||
|
'<div class="grid">' +
|
||||||
|
(shareSafe
|
||||||
|
? '<div><div class="k">共有モード</div><div class="v">ShareSafe(.claude のパス・キーワード省略)</div></div>'
|
||||||
|
: "") +
|
||||||
|
'<div><div class="k">PC 名</div><div class="v">' +
|
||||||
|
esc(m.Name) +
|
||||||
|
"</div></div>" +
|
||||||
|
'<div><div class="k">ユーザー</div><div class="v">' +
|
||||||
|
esc(uc.UserName) +
|
||||||
|
"</div></div>" +
|
||||||
|
'<div><div class="k">管理者ロール</div><div class="v">' +
|
||||||
|
(uc.IsAdmin === true ? "検出あり(影響範囲が広がりやすい)" : "検出なし") +
|
||||||
|
"</div></div>" +
|
||||||
|
'<div><div class="k">UAC EnableLUA</div><div class="v">' +
|
||||||
|
esc(uac.EnableLUA) +
|
||||||
|
"</div></div>" +
|
||||||
|
'<div><div class="k">LISTEN ポート行数</div><div class="v">' +
|
||||||
|
listeners +
|
||||||
|
"</div></div>" +
|
||||||
|
'<div><div class="k">env 系ファイル名</div><div class="v">' +
|
||||||
|
envs +
|
||||||
|
" 件</div></div>" +
|
||||||
|
'<div><div class="k">Run 起動項目</div><div class="v">HKCU ' +
|
||||||
|
hkcu +
|
||||||
|
" / HKLM " +
|
||||||
|
hklm +
|
||||||
|
"</div></div>" +
|
||||||
|
"</div></div>";
|
||||||
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
function renderInsights(data) {
|
||||||
/* Render: Summary */
|
var el = document.getElementById("insights");
|
||||||
/* ------------------------------------------------------------------ */
|
var parts = [];
|
||||||
function renderSummary(data) {
|
var df = data.diffFromPrevious;
|
||||||
var uc = data.userContext || {};
|
if (df) {
|
||||||
var m = data.machine || {};
|
parts.push("<div class=\"box\"><h2>前回実行からの差分</h2>");
|
||||||
var uac = data.uac || {};
|
if (df.HasPrevious === false) {
|
||||||
var net = data.network || {};
|
parts.push("<p class=\"muted\">" + esc(df.NoteJa || "") + "</p>");
|
||||||
var listeners = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
|
} else {
|
||||||
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
|
parts.push("<p class=\"muted\">" + esc(df.NoteJa || "") + "</p>");
|
||||||
var hkcu = data.registryRun && data.registryRun.HKCU_Run ? len(data.registryRun.HKCU_Run) : 0;
|
parts.push("<div class=\"grid\">");
|
||||||
var hklm = data.registryRun && data.registryRun.HKLM_Run ? len(data.registryRun.HKLM_Run) : 0;
|
parts.push(
|
||||||
var shareSafe = data.meta && data.meta.shareSafe === true;
|
"<div><div class=\"k\">比較元(前回 UTC)</div><div class=\"v\">" +
|
||||||
|
esc(df.PreviousGeneratedAtUtc || "") +
|
||||||
|
"</div></div>"
|
||||||
|
);
|
||||||
|
var tcp = df.TcpListeners || {};
|
||||||
|
parts.push(
|
||||||
|
"<div><div class=\"k\">TCP 追加/削除</div><div class=\"v\">" +
|
||||||
|
(tcp.AddedCount || 0) +
|
||||||
|
" / " +
|
||||||
|
(tcp.RemovedCount || 0) +
|
||||||
|
"</div></div>"
|
||||||
|
);
|
||||||
|
var envd = df.EnvLikeRelPaths || {};
|
||||||
|
parts.push(
|
||||||
|
"<div><div class=\"k\">env パス 追加/削除</div><div class=\"v\">" +
|
||||||
|
(envd.AddedCount || 0) +
|
||||||
|
" / " +
|
||||||
|
(envd.RemovedCount || 0) +
|
||||||
|
"</div></div>"
|
||||||
|
);
|
||||||
|
var hkcu = df.HKCU_Run || {};
|
||||||
|
var hklm = df.HKLM_Run || {};
|
||||||
|
parts.push(
|
||||||
|
"<div><div class=\"k\">Run 名 HKCU / HKLM</div><div class=\"v\">" +
|
||||||
|
(hkcu.AddedCount || 0) +
|
||||||
|
"/" +
|
||||||
|
(hkcu.RemovedCount || 0) +
|
||||||
|
" ・ " +
|
||||||
|
(hklm.AddedCount || 0) +
|
||||||
|
"/" +
|
||||||
|
(hklm.RemovedCount || 0) +
|
||||||
|
"</div></div>"
|
||||||
|
);
|
||||||
|
parts.push("</div>");
|
||||||
|
if (df.UserIsAdminChanged) {
|
||||||
|
parts.push("<p class=\"muted\">管理者ロールの検出が前回と異なります。</p>");
|
||||||
|
}
|
||||||
|
if (df.UacEnableLuaChanged) {
|
||||||
|
parts.push("<p class=\"muted\">UAC EnableLUA が前回と異なります。</p>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push("</div>");
|
||||||
|
}
|
||||||
|
var li = data.network && data.network.ListenerInsights;
|
||||||
|
if (li && li.Summary) {
|
||||||
|
var s = li.Summary;
|
||||||
|
parts.push("<div class=\"box\"><h2>LISTEN の見方(自動付与)</h2>");
|
||||||
|
parts.push("<p class=\"muted\">BindScope は待ち受けアドレスの種類の目安です。WellKnownHint はポートの一般的用途の目安です。</p>");
|
||||||
|
parts.push("<div class=\"grid\">");
|
||||||
|
parts.push(
|
||||||
|
"<div><div class=\"k\">全IF / localhost / 合計行</div><div class=\"v\">" +
|
||||||
|
(s.AllInterfacesCount != null ? s.AllInterfacesCount : "?") +
|
||||||
|
" / " +
|
||||||
|
(s.LocalhostOnlyCount != null ? s.LocalhostOnlyCount : "?") +
|
||||||
|
" / " +
|
||||||
|
(s.TotalListenRows != null ? s.TotalListenRows : "?") +
|
||||||
|
"</div></div>"
|
||||||
|
);
|
||||||
|
parts.push("</div>");
|
||||||
|
var rows = li.RowsEnrichedSample || [];
|
||||||
|
if (rows.length) {
|
||||||
|
parts.push("<table style=\"width:100%;font-size:0.85rem;border-collapse:collapse;margin-top:0.75rem\">");
|
||||||
|
parts.push("<thead><tr><th align=\"left\">Address</th><th align=\"left\">Port</th><th align=\"left\">BindScope</th><th align=\"left\">目安</th></tr></thead><tbody>");
|
||||||
|
for (var i = 0; i < Math.min(rows.length, 40); i++) {
|
||||||
|
var r = rows[i];
|
||||||
|
parts.push(
|
||||||
|
"<tr><td>" +
|
||||||
|
esc(r.Address) +
|
||||||
|
"</td><td>" +
|
||||||
|
esc(r.Port) +
|
||||||
|
"</td><td>" +
|
||||||
|
esc(r.BindScope) +
|
||||||
|
"</td><td>" +
|
||||||
|
esc(r.WellKnownHint || "") +
|
||||||
|
"</td></tr>"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parts.push("</tbody></table>");
|
||||||
|
if (rows.length > 40) {
|
||||||
|
parts.push("<p class=\"muted\">先頭 40 行のみ表示。残りは下の生データを参照。</p>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.push("</div>");
|
||||||
|
}
|
||||||
|
el.innerHTML = parts.join("");
|
||||||
|
}
|
||||||
|
|
||||||
var items = [
|
function buildFallbackGuidance(data) {
|
||||||
{ label: "PC 名", value: m.Name || "—" },
|
var out = [];
|
||||||
{ label: "ユーザー", value: uc.UserName || "—" },
|
out.push({
|
||||||
{ label: "管理者権限", value: uc.IsAdmin === true ? "あり" : "なし" },
|
Level: "info",
|
||||||
{ label: "UAC EnableLUA", value: uac.EnableLUA != null ? String(uac.EnableLUA) : "—" },
|
Title: "この JSON には日本語解説(guidanceJa)がありません",
|
||||||
{ label: "LISTEN ポート数", value: String(listeners) },
|
Body:
|
||||||
{ label: ".env 系ファイル", value: envs + " 件" },
|
"古い版の Invoke-PcAudit.ps1 で作られたファイルの可能性があります。リポジトリの最新版で npm run audit:pc を再実行し、新しい latest.json を選び直してください。それまでの間、下記はデータから機械的に作った説明です。",
|
||||||
{ label: "自動起動 HKCU / HKLM", value: hkcu + " / " + hklm }
|
|
||||||
];
|
|
||||||
if (shareSafe) items.unshift({ label: "モード", value: "ShareSafe" });
|
|
||||||
|
|
||||||
var html = '<div class="summary-grid">';
|
|
||||||
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>';
|
|
||||||
document.getElementById("summary").innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Render: Diff */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
function renderDiff(data) {
|
|
||||||
var df = data.diffFromPrevious;
|
|
||||||
if (!df) return;
|
|
||||||
var html = '<div class="diff-card" style="margin-top:0.75rem">';
|
|
||||||
if (df.HasPrevious === false) {
|
|
||||||
html += '<div class="diff-card-title">前回データなし</div><p style="font-size:0.8rem;color:var(--text3)">' + esc(df.NoteJa || "初回実行のため差分はありません。") + '</p>';
|
|
||||||
} else {
|
|
||||||
html += '<div class="diff-card-title">前回との差分</div>';
|
|
||||||
var tcp = df.TcpListeners || {};
|
|
||||||
var envd = df.EnvLikeRelPaths || {};
|
|
||||||
var hkcu = df.HKCU_Run || {};
|
|
||||||
var hklm = df.HKLM_Run || {};
|
|
||||||
html += '<div class="diff-col">';
|
|
||||||
[
|
|
||||||
{ label: "TCP 追加/削除", value: (tcp.AddedCount || 0) + " / " + (tcp.RemovedCount || 0) },
|
|
||||||
{ label: ".env 追加/削除", value: (envd.AddedCount || 0) + " / " + (envd.RemovedCount || 0) },
|
|
||||||
{ label: "Run HKCU +/-", value: (hkcu.AddedCount || 0) + "/" + (hkcu.RemovedCount || 0) },
|
|
||||||
{ label: "Run HKLM +/-", value: (hklm.AddedCount || 0) + "/" + (hklm.RemovedCount || 0) }
|
|
||||||
].forEach(function (r) {
|
|
||||||
html += '<div class="metric"><div class="metric-label">' + esc(r.label) + '</div><div class="metric-value">' + esc(r.value) + '</div></div>';
|
|
||||||
});
|
});
|
||||||
html += '</div>';
|
var uc = data.userContext || {};
|
||||||
if (df.UserIsAdminChanged) html += '<p style="margin-top:0.6rem;font-size:0.8rem;color:var(--warn-color)">管理者権限の状態が前回と変わりました。</p>';
|
if (uc.IsAdmin === true) {
|
||||||
if (df.UacEnableLuaChanged) html += '<p style="margin-top:0.4rem;font-size:0.8rem;color:var(--warn-color)">UAC 設定が前回と変わりました。</p>';
|
out.push({
|
||||||
}
|
Level: "warn",
|
||||||
html += '</div>';
|
Title: "管理者ロールが検出されています",
|
||||||
document.getElementById("diff").innerHTML = html;
|
Body: "自動ツールや操作ミスで PC 全体に影響が及びやすい状態です。意図した使い方か、エンジニアに確認してください。",
|
||||||
}
|
});
|
||||||
|
} else {
|
||||||
/* ------------------------------------------------------------------ */
|
out.push({
|
||||||
/* Render: Guidance cards */
|
Level: "ok",
|
||||||
/* ------------------------------------------------------------------ */
|
Title: "管理者ロールは検出されませんでした",
|
||||||
function buildGuidanceItems(data) {
|
Body: "この監査時点での検出結果です。常に正しいとは限りません。",
|
||||||
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
|
});
|
||||||
if (list.length) return list;
|
|
||||||
var uc = data.userContext || {};
|
|
||||||
var uac = data.uac || {};
|
|
||||||
var net = data.network || {};
|
|
||||||
var ssh = data.ssh || {};
|
|
||||||
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
|
|
||||||
var out = [];
|
|
||||||
if (uc.IsAdmin === true) {
|
|
||||||
out.push({ Level:"warn", Title:"管理者権限が有効です", Body:"この PC は管理者として動作しています。意図的な設定なら問題ありませんが、誤操作やウイルスが PC 全体に影響しやすくなります。", ActionJa:"社内 IT に「管理者権限が必要か確認したい」と相談する。急ぎでなければ次回 IT 棚卸しのタイミングで OK。" });
|
|
||||||
} else {
|
|
||||||
out.push({ Level:"ok", Title:"管理者権限は検出されませんでした", Body:"この監査実行時点では管理者権限は確認されていません。" });
|
|
||||||
}
|
|
||||||
if (uac.EnableLUA === 0) {
|
|
||||||
out.push({ Level:"warn", Title:"UAC(確認ダイアログ)が無効です", Body:"アプリが PC 設定を変更しようとしても確認ダイアログが出ない状態です。マルウェアが気づかず実行されやすくなります。", ActionJa:"社内 IT に「UAC を有効にしてほしい」と伝える。" });
|
|
||||||
}
|
|
||||||
if (ssh.DirectoryExists && len(ssh.Files) > 0) {
|
|
||||||
out.push({ Level:"info", Title:"SSH 鍵ファイルがあります", Body:"サーバーへの接続に使う鍵ファイルが ~/.ssh フォルダに存在します。鍵が漏えいするとサーバーに不正アクセスされる可能性があります。", ActionJa:"鍵ファイルのバックアップがあるか確認する。パスフレーズ(パスワード)を設定済みか、エンジニアに確認する。" });
|
|
||||||
}
|
|
||||||
if (envs > 0) {
|
|
||||||
out.push({ Level:"info", Title:".env ファイルが " + envs + " 件見つかりました", Body:"API キーやパスワードが書かれることが多いファイル名です。中身はこのツールでは読んでいません。Git に含まれると外部に漏えいします。", ActionJa:"エンジニアに「.env が Git に含まれていないか確認して」と伝える。" });
|
|
||||||
}
|
|
||||||
var lc = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
|
|
||||||
if (lc > 45) {
|
|
||||||
out.push({ Level:"info", Title:"待ち受けポートが多めです(" + lc + " 件)", Body:"多くのサービスが通信を受け付けています。使っていないアプリが起動したままの可能性があります。", ActionJa:"エンジニアに一覧を見てもらい、不要なアプリがないか確認してもらう。" });
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderGuidance(data) {
|
|
||||||
var items = buildGuidanceItems(data);
|
|
||||||
var html = "";
|
|
||||||
items.forEach(function (item) {
|
|
||||||
var level = (item.Level || "info").toLowerCase();
|
|
||||||
var badgeClass = level === "warn" ? "badge-warn" : (level === "ok" ? "badge-ok" : "badge-info");
|
|
||||||
var badgeLabel = level === "warn" ? "注意" : (level === "ok" ? "問題なし" : "情報");
|
|
||||||
html += '<div class="card ' + level + '">';
|
|
||||||
html += '<div class="card-header"><span class="badge ' + badgeClass + '">' + badgeLabel + '</span><div class="card-title">' + esc(item.Title || "") + '</div></div>';
|
|
||||||
html += '<div class="card-body">' + esc(item.Body || "").replace(/\n/g, "<br/>") + '</div>';
|
|
||||||
var action = item.ActionJa || item.actionJa;
|
|
||||||
if (action) {
|
|
||||||
html += '<div class="action-box"><span class="action-label">対処方法</span>' + esc(action) + '</div>';
|
|
||||||
}
|
}
|
||||||
html += '</div>';
|
var uac = data.uac || {};
|
||||||
|
if (uac.EnableLUA === 0) {
|
||||||
|
out.push({
|
||||||
|
Level: "warn",
|
||||||
|
Title: "UAC(EnableLUA)が 0 です",
|
||||||
|
Body: "ユーザーアカウント制御が弱い可能性があります。社内ポリシーと照らしてください。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var net = data.network || {};
|
||||||
|
var lc = Array.isArray(net.TcpListeners) ? net.TcpListeners.length : 0;
|
||||||
|
if (lc > 45) {
|
||||||
|
out.push({
|
||||||
|
Level: "info",
|
||||||
|
Title: "待ち受けポートが多めです",
|
||||||
|
Body: "件数は " + lc + " 行です。不要なサービスがないか、エンジニアに一覧を見てもらう価値があります。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var ssh = data.ssh || {};
|
||||||
|
if (ssh.DirectoryExists && len(ssh.Files) > 0) {
|
||||||
|
out.push({
|
||||||
|
Level: "info",
|
||||||
|
Title: "SSH 用フォルダにファイルがあります",
|
||||||
|
Body: "鍵や設定があるようです。中身はこのレポートでは読んでいません。紛失・漏えい対策を社内ルールで確認してください。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var git = data.git || {};
|
||||||
|
if (git.GitAvailable && git.CredentialHelper) {
|
||||||
|
out.push({
|
||||||
|
Level: "info",
|
||||||
|
Title: "Git credential.helper が設定されています",
|
||||||
|
Body: "認証情報の保存先が OS や別ツールに関わります。会社の開発ポリシーと矛盾がないか必要なら確認してください。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var envs = data.projectScan && data.projectScan.EnvLikeRelPaths ? len(data.projectScan.EnvLikeRelPaths) : 0;
|
||||||
|
if (envs > 0) {
|
||||||
|
out.push({
|
||||||
|
Level: "info",
|
||||||
|
Title: "環境変数ファイルらしき名前が " + envs + " 件あります",
|
||||||
|
Body: "API キー等が入りがちです。Git に含めていないか、エンジニアに確認してください。",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendGuidanceCard(container, item) {
|
||||||
|
var div = document.createElement("div");
|
||||||
|
div.className = "box " + levelClass(item.Level);
|
||||||
|
div.innerHTML =
|
||||||
|
"<h2>[" +
|
||||||
|
esc(labelJa(item.Level)) +
|
||||||
|
"] " +
|
||||||
|
esc(item.Title) +
|
||||||
|
"</h2><p>" +
|
||||||
|
esc(item.Body).replace(/\n/g, "<br/>") +
|
||||||
|
"</p>";
|
||||||
|
container.appendChild(div);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("f").addEventListener("change", function (ev) {
|
||||||
|
var file = ev.target.files && ev.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function () {
|
||||||
|
var text = reader.result;
|
||||||
|
var data;
|
||||||
|
try {
|
||||||
|
data = JSON.parse(text);
|
||||||
|
} catch (e) {
|
||||||
|
document.getElementById("summary").innerHTML = "";
|
||||||
|
document.getElementById("insights").innerHTML = "";
|
||||||
|
document.getElementById("guidance").innerHTML =
|
||||||
|
'<div class="box warn">JSON の読み取りに失敗しました。pc-audit の出力ファイルか確認してください。</div>';
|
||||||
|
document.getElementById("raw").textContent = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var disc = document.getElementById("disclaimer");
|
||||||
|
if (data.meta && data.meta.guidanceDisclaimerJa) {
|
||||||
|
disc.hidden = false;
|
||||||
|
disc.textContent = data.meta.guidanceDisclaimerJa;
|
||||||
|
} else {
|
||||||
|
disc.hidden = false;
|
||||||
|
disc.textContent =
|
||||||
|
"このレポートは補助的な説明であり、セキュリティ診断やコンプライアンス判定に代わるものではありません。";
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSummary(data);
|
||||||
|
renderInsights(data);
|
||||||
|
|
||||||
|
var g = document.getElementById("guidance");
|
||||||
|
g.innerHTML = "";
|
||||||
|
var list = Array.isArray(data.guidanceJa) ? data.guidanceJa : [];
|
||||||
|
var usedFallback = false;
|
||||||
|
if (!list.length) {
|
||||||
|
usedFallback = true;
|
||||||
|
list = buildFallbackGuidance(data);
|
||||||
|
}
|
||||||
|
if (usedFallback) {
|
||||||
|
var note = document.createElement("div");
|
||||||
|
note.className = "box warn";
|
||||||
|
note.innerHTML =
|
||||||
|
"<h2>補足</h2><p>上の「解説がありません」は、<strong>古い JSON</strong>を開いているときに出ます。<strong>Invoke-PcAudit.ps1 を最新にして監査をやり直す</strong>と、PowerShell 側で書いた詳しい日本語解説が JSON に入ります。</p>";
|
||||||
|
g.appendChild(note);
|
||||||
|
}
|
||||||
|
list.forEach(function (item) {
|
||||||
|
appendGuidanceCard(g, item);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("raw").textContent = JSON.stringify(data, null, 2);
|
||||||
|
};
|
||||||
|
reader.readAsText(file, "UTF-8");
|
||||||
});
|
});
|
||||||
document.getElementById("guidance").innerHTML = html;
|
})();
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* Render: Port list */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
function renderListeners(data) {
|
|
||||||
var net = data.network || {};
|
|
||||||
var li = net.ListenerInsights;
|
|
||||||
var rows = (li && li.RowsEnrichedSample) ? li.RowsEnrichedSample : (net.TcpListeners || []);
|
|
||||||
if (!rows.length) return;
|
|
||||||
var s = (li && li.Summary) || {};
|
|
||||||
|
|
||||||
var html = '';
|
|
||||||
html += '<p style="font-size:0.8rem;color:var(--text2);margin-bottom:0.85rem;line-height:1.6">';
|
|
||||||
html += '<span style="color:var(--ok-color)">localhost</span> は自分の PC 内だけ、安全。';
|
|
||||||
html += '<br><span style="color:var(--warn-color)">:: / 0.0.0.0</span> は Windows 標準で出ることが多く、通常は問題なし。';
|
|
||||||
html += '<br>日本語説明がないポートは <strong style="color:var(--text)">一時ポート(Windows が自動割当)</strong>で、接続ごとに番号が変わる正常な動作です。';
|
|
||||||
html += '</p>';
|
|
||||||
|
|
||||||
if (s.AllInterfacesCount != null) {
|
|
||||||
html += '<div style="display:flex;gap:1rem;margin-bottom:0.85rem;flex-wrap:wrap">';
|
|
||||||
html += '<span style="font-size:0.78rem;color:var(--text3)">全IF: <strong style="color:var(--text)">' + s.AllInterfacesCount + '</strong></span>';
|
|
||||||
html += '<span style="font-size:0.78rem;color:var(--text3)">localhost: <strong style="color:var(--text)">' + s.LocalhostOnlyCount + '</strong></span>';
|
|
||||||
html += '<span style="font-size:0.78rem;color:var(--text3)">合計: <strong style="color:var(--text)">' + s.TotalListenRows + '</strong></span>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
|
|
||||||
html += '<div class="table-wrap"><table>';
|
|
||||||
html += '<thead><tr><th>Address</th><th>Port</th><th>種類</th><th>用途</th></tr></thead><tbody>';
|
|
||||||
var shown = Math.min(rows.length, 60);
|
|
||||||
for (var i = 0; i < shown; i++) {
|
|
||||||
var r = rows[i];
|
|
||||||
var sc = r.BindScope === "allInterfaces" ? "scope-all" : (r.BindScope === "localhost" ? "scope-localhost" : "scope-specific");
|
|
||||||
var aN = addressNote(r.Address || "");
|
|
||||||
var pN = portNote(r.Port, r.WellKnownHint);
|
|
||||||
html += '<tr>';
|
|
||||||
html += '<td class="addr-cell">' + esc(r.Address || "") + (aN ? '<div class="hint-line">' + esc(aN) + '</div>' : '') + '</td>';
|
|
||||||
html += '<td class="port-cell">' + esc(r.Port || "") + '</td>';
|
|
||||||
html += '<td class="' + sc + '">' + esc(r.BindScope || "") + '</td>';
|
|
||||||
html += '<td class="hint-line" style="font-size:0.78rem;color:var(--text3)">' + esc(pN) + '</td>';
|
|
||||||
html += '</tr>';
|
|
||||||
}
|
|
||||||
html += '</tbody></table></div>';
|
|
||||||
if (rows.length > 60) {
|
|
||||||
html += '<p style="margin-top:0.5rem;font-size:0.72rem;color:var(--text3)">先頭 60 行のみ表示。全データは下の生データを参照。</p>';
|
|
||||||
}
|
|
||||||
document.getElementById("listeners").innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
/* File load */
|
|
||||||
/* ------------------------------------------------------------------ */
|
|
||||||
document.getElementById("f").addEventListener("change", function (ev) {
|
|
||||||
var file = ev.target.files && ev.target.files[0];
|
|
||||||
if (!file) return;
|
|
||||||
var reader = new FileReader();
|
|
||||||
reader.onload = function () {
|
|
||||||
var data;
|
|
||||||
try { data = JSON.parse(reader.result); }
|
|
||||||
catch (e) {
|
|
||||||
document.getElementById("guidance").innerHTML = '<div class="card warn"><div class="card-header"><span class="badge badge-warn">エラー</span><div class="card-title">JSON の読み込みに失敗しました</div></div><div class="card-body">pc-audit が出力したファイルか確認してください。</div></div>';
|
|
||||||
document.getElementById("output").style.display = "";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById("output").style.display = "";
|
|
||||||
renderSummary(data);
|
|
||||||
renderDiff(data);
|
|
||||||
renderGuidance(data);
|
|
||||||
renderListeners(data);
|
|
||||||
document.getElementById("raw").textContent = JSON.stringify(data, null, 2);
|
|
||||||
};
|
|
||||||
reader.readAsText(file, "UTF-8");
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,31 @@
|
||||||
@echo off
|
@echo off
|
||||||
cd /d "%~dp0..\.."
|
chcp 65001 > nul
|
||||||
echo.
|
echo.
|
||||||
echo === Posimai PC Audit (Share-Safe) ===
|
echo ===========================
|
||||||
|
echo Posimai PC Audit (共有用)
|
||||||
|
echo .claude のパスは匿名化されます
|
||||||
|
echo ===========================
|
||||||
echo.
|
echo.
|
||||||
echo Running audit (share-safe mode)...
|
|
||||||
|
cd /d "%~dp0..\..\"
|
||||||
|
|
||||||
|
echo [1/2] 監査を実行しています(ShareSafe モード)...
|
||||||
call npm run audit:pc:share
|
call npm run audit:pc:share
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: Audit failed.
|
echo 監査の実行に失敗しました。
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Opening report viewer...
|
echo [2/2] レポートをブラウザで開きます...
|
||||||
start "" "%~dp0report-viewer.html"
|
start "" "%~dp0report-viewer.html"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Done. Select out\latest.json in the browser.
|
echo 完了しました。
|
||||||
echo This report is safe to share with IT / engineers.
|
echo ブラウザで report-viewer.html が開いたら、
|
||||||
|
echo ファイル選択で「out\latest.json」を選んでください。
|
||||||
|
echo このレポートは社内 IT やエンジニアに共有できます。
|
||||||
echo.
|
echo.
|
||||||
pause
|
pause
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,29 @@
|
||||||
@echo off
|
@echo off
|
||||||
cd /d "%~dp0..\.."
|
chcp 65001 > nul
|
||||||
echo.
|
echo.
|
||||||
echo === Posimai PC Audit ===
|
echo ===========================
|
||||||
|
echo Posimai PC Audit
|
||||||
|
echo ===========================
|
||||||
echo.
|
echo.
|
||||||
echo Running audit...
|
|
||||||
|
cd /d "%~dp0..\..\"
|
||||||
|
|
||||||
|
echo [1/2] 監査を実行しています...
|
||||||
call npm run audit:pc
|
call npm run audit:pc
|
||||||
if errorlevel 1 (
|
if errorlevel 1 (
|
||||||
echo.
|
echo.
|
||||||
echo ERROR: Audit failed.
|
echo 監査の実行に失敗しました。
|
||||||
pause
|
pause
|
||||||
exit /b 1
|
exit /b 1
|
||||||
)
|
)
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Opening report viewer...
|
echo [2/2] レポートをブラウザで開きます...
|
||||||
start "" "%~dp0report-viewer.html"
|
start "" "%~dp0report-viewer.html"
|
||||||
|
|
||||||
echo.
|
echo.
|
||||||
echo Done. Select out\latest.json in the browser.
|
echo 完了しました。
|
||||||
|
echo ブラウザで report-viewer.html が開いたら、
|
||||||
|
echo ファイル選択で「out\latest.json」を選んでください。
|
||||||
echo.
|
echo.
|
||||||
pause
|
pause
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue