# ===================================================================== # 万有AI·系统体检包V1.0 只读探针(Windows / PowerShell 5.1+) # --------------------------------------------------------------------- # 五条铁律(与AI体检员作业手册·checklist.yaml一致): # 1. 纯只读:仅存在性检查/内容模式匹配/git只读子命令·零写入零修改零安装 # 2. 零联网:本脚本无任何网络请求 # 3. 扫描收敛:排除node_modules/.git/dist等·单文件<=512KB·深度<=4·文件数<=2000 # 4. 自动脱敏:报告只含 通过/未过/未知+相对路径+计数·绝不输出密钥原文 # 5. fail-loud:单项异常记cannot_detect并给原因·不中断整体 # 用法: powershell -ExecutionPolicy Bypass -File checkup.ps1 [-TargetPath 系统根目录] [-OutputDir 报告输出目录] # 输出: report.json + report.md(落在OutputDir·可随时删除) # ===================================================================== param( [string]$TargetPath = ".", [string]$OutputDir = "" ) $ErrorActionPreference = "Continue" $PKV = "1.0" $SCHEMAV = "1.0" # ---- 路径解析 ---- try { $Target = (Resolve-Path -LiteralPath $TargetPath).Path } catch { Write-Error "[体检包] 目标路径不存在: $TargetPath"; exit 2 } if (-not $OutputDir) { $OutputDir = (Get-Location).Path } $OutputDir = (Resolve-Path -LiteralPath $OutputDir).Path $sw = [System.Diagnostics.Stopwatch]::StartNew() # ---- 扫描参数(SSOT=checklist.yaml scan_limits)---- $MaxDepth = 4 $MaxFileBytes = 524288 $MaxFiles = 2000 $ExcludeDirs = @("node_modules",".git","dist","build","vendor","__pycache__","venv",".venv",".next",".nuxt",".output","coverage") $TextExts = @(".py",".js",".ts",".jsx",".tsx",".go",".java",".php",".rb",".rs",".c",".cpp",".h",".cs",".yaml",".yml",".json",".toml",".ini",".cfg",".conf",".env",".sh",".bash",".ps1",".psm1",".md",".txt",".sql",".html",".vue",".properties",".gitignore",".bat",".cmd") $ScanScripts = @(".sh",".bash",".ps1",".yml",".yaml",".bat",".cmd") function Test-Excluded([string]$FullPath) { foreach ($d in $ExcludeDirs) { if ($FullPath -like "*\${d}\*" -or $FullPath -like "*\${d}") { return $true } } return $false } # ---- 文件清点(收敛)---- $all = Get-ChildItem -LiteralPath $Target -Recurse -Depth $MaxDepth -File -ErrorAction SilentlyContinue $files = @(); $truncated = $false foreach ($f in $all) { if ($files.Count -ge $MaxFiles) { $truncated = $true; break } if (Test-Excluded $f.FullName) { continue } if ($f.Length -gt $MaxFileBytes) { continue } $files += $f } function Get-Rel([System.IO.FileInfo]$f) { return $f.FullName.Substring($Target.Length).TrimStart("\","/") } function Read-Lines([System.IO.FileInfo]$f) { try { return [System.IO.File]::ReadAllLines($f.FullName) } catch { return $null } } # ---- 结果收集器 ---- $script:Items = New-Object System.Collections.ArrayList function Add-Result([string]$id,[string]$name,[string]$cat,[string]$level,[int]$maxScore,[string]$status,[int]$earned,[string[]]$ev,[bool]$qa,[string]$question) { $null = $script:Items.Add([pscustomobject]@{ id=$id; name=$name; category=$cat; level=$level; score_max=$maxScore status=$status; earned=$earned; evidence=$ev transfer_to_qa=$qa; qa_question=$question }) } function Mask-Val([string]$v) { if ($v.Length -le 4) { return "***" } return $v.Substring(0,2) + "***(" + $v.Length + "ch)" } # ============ N1 git版本管理 ============ $gitOk = $false; $gitHead = ""; $gitMissing = $false try { $gitHead = & git -C $Target rev-parse HEAD 2>$null if ($LASTEXITCODE -eq 0 -and $gitHead) { $gitOk = $true } elseif ($LASTEXITCODE -eq 127 -or $null -eq $LASTEXITCODE) { $gitMissing = $true } } catch { $gitMissing = $true } if ($gitOk) { Add-Result "N1" "git版本管理" "规范性" "P1" 5 "pass" 5 @("git HEAD=$($gitHead.Substring(0,[Math]::Min(12,$gitHead.Length)))") $false "" } elseif ($gitMissing) { Add-Result "N1" "git版本管理" "规范性" "P1" 5 "cannot_detect" 0 @("本机未安装git或不可执行") $true "系统是否纳入版本管理(git或其他)?改动能否回溯?" } else { if (Test-Path (Join-Path $Target ".git")) { Add-Result "N1" "git版本管理" "规范性" "P1" 5 "fail" 0 @("git仓库存在但无任何提交·HEAD不可解析") $false "" } else { Add-Result "N1" "git版本管理" "规范性" "P1" 5 "fail" 0 @("未发现.git仓库") $false "" } } # ============ N2 敏感文件忽略(P0)============ $giFile = Join-Path $Target ".gitignore" $giCover = $false; $giEv = @() if (Test-Path $giFile) { $giLines = Read-Lines (Get-Item $giFile) foreach ($l in $giLines) { $t = $l.Trim() if ($t -match "^\.env(\s|$)" -or $t -match "^\.env\.\*" -or $t -match "^\.env$") { if ($t -notmatch "^!") { $giCover = $true; $giEv += ".gitignore规则: $t" } } } } $trackedEnv = @() if ($gitOk) { $ls = & git -C $Target ls-files 2>$null foreach ($p in $ls) { $leaf = Split-Path $p -Leaf if ($leaf -match "^\.env(\.|$)" -and $leaf -notmatch "\.(example|template|sample)$") { $trackedEnv += $p } if ($leaf -eq ".env") { $trackedEnv += $p } } $trackedEnv = $trackedEnv | Select-Object -Unique } $naEv = @(); $n2status = "fail"; $n2earned = 0 if (-not (Test-Path $giFile) -and -not $gitOk) { $n2status = "cannot_detect"; $naEv = @("无.gitignore且git不可用") } elseif ($giCover -and $trackedEnv.Count -eq 0) { $n2status = "pass"; $n2earned = 8; $giEv += "git ls-files中无真实env文件" } elseif ($giCover -and $trackedEnv.Count -gt 0) { $giEv += ("被git追踪的env文件: " + ($trackedEnv -join ", ")) } elseif (-not $giCover) { $giEv += ".gitignore未发现覆盖.env的规则" } if ($n2status -eq "cannot_detect") { Add-Result "N2" "敏感文件忽略" "规范性" "P0" 8 $n2status 0 $naEv $true "有没有把含密码/密钥的配置文件提交进过仓库?" } else { Add-Result "N2" "敏感文件忽略" "规范性" "P0" 8 $n2status $n2earned $giEv $false "" } # ============ N3 依赖锁定 ============ $lockNames = @("package-lock.json","pnpm-lock.yaml","yarn.lock","poetry.lock","Pipfile.lock","requirements.txt","go.sum","Cargo.lock") $lockHit = @() foreach ($n in $lockNames) { $hit = $files | Where-Object { $_.Name -eq $n } | Select-Object -First 1 if ($hit) { $lockHit += (Get-Rel $hit) } } if ($lockHit.Count -gt 0) { Add-Result "N3" "依赖锁定" "规范性" "P1" 5 "pass" 5 @("找到: " + ($lockHit -join ", ")) $false "" } else { Add-Result "N3" "依赖锁定" "规范性" "P1" 5 "fail" 0 @("未发现任何依赖锁定文件") $false "" } # ============ N4 README ============ $readme = $files | Where-Object { $_.Name -match "^README" } | Select-Object -First 1 if ($readme -and $readme.Length -gt 0) { Add-Result "N4" "README" "规范性" "P1" 3 "pass" 3 @((Get-Rel $readme)) $false "" } else { Add-Result "N4" "README" "规范性" "P1" 3 "fail" 0 @("未发现README") $false "" } # ============ N5 部署文档 ============ $depDoc = $files | Where-Object { $_.Name -match "^(DEPLOY|部署)" -or $_.FullName -match "\\docs\\(deploy|deployment)" } | Select-Object -First 1 if ($depDoc) { Add-Result "N5" "部署文档" "规范性" "P1" 4 "pass" 4 @((Get-Rel $depDoc)) $false "" } else { Add-Result "N5" "部署文档" "规范性" "P1" 4 "fail" 0 @("未发现部署文档") $false "" } # ============ N6 环境配置分离 ============ $envTpl = $files | Where-Object { $_.Name -match "^(\.env\.(example|template|sample)|env\.example|config\.example\.)" } | Select-Object -First 1 if ($envTpl) { Add-Result "N6" "环境配置分离" "规范性" "P1" 6 "pass" 6 @((Get-Rel $envTpl)) $false "" } else { Add-Result "N6" "环境配置分离" "规范性" "P1" 6 "fail" 0 @("未发现env模板文件(.env.example等)") $false "" } # ============ Docker适用性 ============ $dockerfile = $files | Where-Object { $_.Name -match "^Dockerfile" } | Select-Object -First 1 $compose = $files | Where-Object { $_.Name -match "^(docker-compose|compose)\.(yml|yaml)$" } | Select-Object -First 1 $hasDocker = [bool]($dockerfile -or $compose) # ============ S1 健康检查 ============ if (-not $hasDocker) { Add-Result "S1" "健康检查" "稳定性" "P1" 7 "na" 0 @("未发现Dockerfile/compose·不适用") $false "" } else { $s1ev = @(); $s1ok = $false if ($dockerfile) { $dl = Read-Lines $dockerfile; if ($dl | Where-Object { $_ -match "^\s*HEALTHCHECK" }) { $s1ok = $true; $s1ev += "Dockerfile含HEALTHCHECK" } } if (-not $s1ok -and $compose) { $cl = Read-Lines $compose; if ($cl | Where-Object { $_ -match "healthcheck:" }) { $s1ok = $true; $s1ev += "compose含healthcheck" } } if (-not $s1ok) { $codeHit = $files | Where-Object { $TextExts -contains $_.Extension.ToLower() } | Select-Object -First 400 foreach ($cf in $codeHit) { $ln = Read-Lines $cf; if ($ln | Where-Object { $_ -match "/healthz?" }) { $s1ok = $true; $s1ev += "代码含/health路由: $(Get-Rel $cf)"; break } } } if ($s1ok) { Add-Result "S1" "健康检查" "稳定性" "P1" 7 "pass" 7 $s1ev $false "" } else { Add-Result "S1" "健康检查" "稳定性" "P1" 7 "fail" 0 @("Dockerfile/compose/代码均未发现健康检查痕迹") $false "" } } # ============ S2 容器非root运行(P0)============ if (-not $hasDocker -or -not $dockerfile) { Add-Result "S2" "容器非root运行" "稳定性" "P0" 7 "na" 0 @("未发现Dockerfile·不适用") $false "" } else { $dl = Read-Lines $dockerfile $userLine = $dl | Where-Object { $_ -match "^\s*USER\s+" } | Select-Object -Last 1 if ($userLine) { $u = ($userLine -split "\s+")[1] if ($u -match "^(root|0)$") { Add-Result "S2" "容器非root运行" "稳定性" "P0" 7 "fail" 0 @("Dockerfile末尾USER为root: $userLine") $false "" } else { Add-Result "S2" "容器非root运行" "稳定性" "P0" 7 "pass" 7 @("USER=$u") $false "" } } else { Add-Result "S2" "容器非root运行" "稳定性" "P0" 7 "fail" 0 @("Dockerfile无USER指令·默认以root运行") $false "" } } # ============ S3 重启策略与资源上限 ============ if (-not $hasDocker -or -not $compose) { Add-Result "S3" "重启策略与资源上限" "稳定性" "P1" 6 "na" 0 @("未发现compose文件·不适用") $false "" } else { $cl = Read-Lines $compose $restart = $cl | Where-Object { $_ -match "^\s*restart:\s*(.+)$" } | Select-Object -First 1 $res = $cl | Where-Object { $_ -match "mem_limit:|cpus:|resources:" } | Select-Object -First 1 $rok = $restart -and ($restart -match "unless-stopped|always|on-failure") if ($rok -and $res) { Add-Result "S3" "重启策略与资源上限" "稳定性" "P1" 6 "pass" 6 @($restart.Trim(), $res.Trim()) $false "" } elseif ($rok) { Add-Result "S3" "重启策略与资源上限" "稳定性" "P1" 6 "fail" 0 @("有restart策略但未发现资源上限(mem_limit/cpus)", $restart.Trim()) $false "" } else { Add-Result "S3" "重启策略与资源上限" "稳定性" "P1" 6 "fail" 0 @("未发现restart策略") $false "" } } # ============ S4 自动化测试 ============ $testHit = $files | Where-Object { $_.FullName -match "\\(tests?|__tests__)\\|(^|\\)test_" -or $_.Name -match "\.(test|spec)\.(js|ts|jsx|tsx)$" } | Select-Object -First 3 if ($testHit) { Add-Result "S4" "自动化测试" "稳定性" "P2" 5 "pass" 5 @(($testHit | ForEach-Object { Get-Rel $_ }) -join ", ") $false "" } else { Add-Result "S4" "自动化测试" "稳定性" "P2" 5 "fail" 0 @("未发现测试目录或测试文件") $false "" } # ============ O1/O2/O5 关键词痕迹 ============ $kwDefs = @( @{ id="O1"; name="备份机制"; score=6; level="P1"; pat="备份|backup|_bak|\.bak"; qa="数据库每天自动备份吗?备份存另一台机器吗?恢复演练过吗?" }, @{ id="O2"; name="回滚预案"; score=6; level="P1"; pat="回滚|rollback"; qa="新版本出问题怎么退回旧版?演练过吗?" }, @{ id="O5"; name="日志与监控"; score=3; level="P2"; pat="logging|log4j|logback|serilog|winston|pino|监控|monitor"; qa="出问题时你怎么知道?日志去哪看?" } ) foreach ($kw in $kwDefs) { $hits = @() foreach ($f in ($files | Where-Object { $TextExts -contains $_.Extension.ToLower() } | Select-Object -First 800)) { if ($f.Name -match $kw.pat) { $hits += "文件名命中: $(Get-Rel $f)"; if ($hits.Count -ge 3) { break }; continue } $ln = Read-Lines $f if ($ln -and ($ln | Where-Object { $_ -match $kw.pat })) { $hits += "内容命中: $(Get-Rel $f)"; if ($hits.Count -ge 3) { break } } } if ($hits.Count -gt 0) { Add-Result $kw.id $kw.name "运维面" $kw.level $kw.score "pass" $kw.score $hits $false "" } else { Add-Result $kw.id $kw.name "运维面" $kw.level $kw.score "fail" 0 @("未发现相关痕迹") $true $kw.qa } } # ============ O3 版本锚 ============ $o3ev = @(); $o3ok = $false if ($files | Where-Object { $_.Name -match "^(VERSION|CHANGELOG)" }) { $o3ok = $true; $o3ev += "存在VERSION/CHANGELOG文件" } if (-not $o3ok -and $gitOk) { $tags = & git -C $Target tag 2>$null if ($tags -and @($tags).Count -ge 1) { $o3ok = $true; $o3ev += "git tag $($tags.Count)个(例: $(@($tags)[0]))" } } if ($o3ok) { Add-Result "O3" "版本锚" "运维面" "P2" 3 "pass" 3 $o3ev $false "" } else { Add-Result "O3" "版本锚" "运维面" "P2" 3 "fail" 0 @("无VERSION/CHANGELOG/git tag") $false "" } # ============ O4 危险命令扫描(P0)============ $dangerHits = @() $scanSet = $files | Where-Object { $ScanScripts -contains $_.Extension.ToLower() -or $_.Name -match "^Dockerfile" } foreach ($f in $scanSet) { $ln = Read-Lines $f if (-not $ln) { continue } for ($i = 0; $i -lt $ln.Count; $i++) { $L = $ln[$i] if ($L -match "(?i)^\s*#") { continue } if ($L -match "\brm\s+") { $hasR = $L -match "(^|\s)-[a-zA-Z]*r" ; $hasF = $L -match "(^|\s)-[a-zA-Z]*f" if ($hasR -and $hasF) { $args = ($L -replace "^\s*rm\s+", "") -split "\s+" $tgt = $args | Where-Object { $_ -notmatch "^-" -and $_ -ne "" } | Select-Object -First 1 if ($tgt -and ($tgt -match "^(/|\$|~)" -or $tgt -match '^\*\*?/')) { $dangerHits += "L$($i+1): rm类全量删除目标=[$($tgt -replace '\\','\\')]@$(Get-Rel $f)" } } } if ($L -match "(?i)\b(rd|rmdir)\s+/s\s+/q\s+`"?[a-zA-Z]:\\`"?\s*$" -or $L -match "(?i)Remove-Item\s+.*-Recurse.*-Force.*[a-zA-Z]:\\\s*$") { $dangerHits += "L$($i+1): Windows盘根删除@$(Get-Rel $f)" } } } if ($dangerHits.Count -gt 0) { Add-Result "O4" "危险命令扫描" "运维面" "P0" 8 "fail" 0 ($dangerHits | Select-Object -First 5) $false "" } else { Add-Result "O4" "危险命令扫描" "运维面" "P0" 8 "pass" 8 @("部署类脚本未发现全量删除类命令(相对路径删除不计)") $false "" } # ============ X1 密钥硬编码(P0)============ $secretPats = @( @{ n="private_key_block"; re="-----BEGIN (RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY( BLOCK)?-----"; mask=$true }, @{ n="aws_access_key"; re="AKIA[0-9A-Z]{16}"; mask=$true }, @{ n="sk_prefixed_key"; re="sk-[A-Za-z0-9]{20,}"; mask=$true }, @{ n="password_literal"; re="(?i)(password|passwd|pwd|secret|api[_-]?key|access[_-]?token)\s*[=:]\s*[`"']([^`"']{8,})[`"']"; mask=$true } ) $placeholderRe = "(?i)^(your[_-].*|xxx+|change.?me|<[^>]*>|`$\{.*\}|\{\{.*\}\}|\*+|test[_-]?.*|dummy.*|fake.*|example.*|placeholder.*|)$" $x1Hits = @(); $x1Scanned = 0 foreach ($f in ($files | Where-Object { $TextExts -contains $_.Extension.ToLower() } | Select-Object -First 1200)) { $rel = Get-Rel $f if ($f.Name -match "(example|template|sample|spec|fixture)" ) { continue } $ln = Read-Lines $f if (-not $ln) { continue } $x1Scanned++ for ($i = 0; $i -lt $ln.Count; $i++) { foreach ($p in $secretPats) { $m = [regex]::Match($ln[$i], $p.re) if ($m.Success) { $frag = $m.Value if ($p.n -eq "password_literal" -and $m.Groups.Count -gt 1) { $frag = $m.Groups[1].Value } if ($frag -match $placeholderRe) { continue } $x1Hits += "$($p.n)@$(Get-Rel $f):L$($i+1) 值已掩码=$(Mask-Val $frag)" if ($x1Hits.Count -ge 10) { break } } } if ($x1Hits.Count -ge 10) { break } } if ($x1Hits.Count -ge 10) { break } } if ($x1Hits.Count -gt 0) { Add-Result "X1" "密钥硬编码" "安全红线" "P0" 10 "fail" 0 ($x1Hits | Select-Object -First 6) $false "" } else { Add-Result "X1" "密钥硬编码" "安全红线" "P0" 10 "pass" 10 @("已扫描$x1Scanned个文本文件·未发现密钥形态(模板/示例/测试文件已排除)") $false "" } # ============ X2 调试态暴露 ============ $x2Hits = @() foreach ($f in ($files | Where-Object { $TextExts -contains $_.Extension.ToLower() -and $_.Name -notmatch "(example|template|sample)" } | Select-Object -First 800)) { $ln = Read-Lines $f if (-not $ln) { continue } for ($i = 0; $i -lt $ln.Count; $i++) { if ($ln[$i] -match "^\s*#" ) { continue } if ($ln[$i] -match "(?i)\b(DEBUG\s*=\s*True\b|APP_DEBUG\s*=\s*true\b)" -or $ln[$i] -match "(?i)\bdebug:\s*true\b") { $x2Hits += "$(Get-Rel $f):L$($i+1)" if ($x2Hits.Count -ge 5) { break } } } if ($x2Hits.Count -ge 5) { break } } if ($x2Hits.Count -gt 0) { Add-Result "X2" "调试态暴露" "安全红线" "P1" 5 "fail" 0 ($x2Hits | ForEach-Object { "$_ (需人工确认是否生产配置面)" }) $false "" } else { Add-Result "X2" "调试态暴露" "安全红线" "P1" 5 "pass" 5 @("未发现DEBUG=True/APP_DEBUG=true/debug:true形态") $false "" } # ============ X3 依赖漏洞审计 ============ $eco = @() if ($lockHit | Where-Object { $_ -match "package-lock|pnpm-lock|yarn\.lock" }) { $eco += "npm" } if ($lockHit | Where-Object { $_ -match "requirements|Pipfile|poetry" }) { $eco += "python" } if ($eco.Count -gt 0) { $cmds = @(); if ($eco -contains "npm") { $cmds += "npm audit" }; if ($eco -contains "python") { $cmds += "pip-audit" } Add-Result "X3" "依赖漏洞审计" "安全红线" "P1" 3 "transfer_to_qa" 0 @("检测到生态: $($eco -join '/')·请AI体检员引导学员执行: $($cmds -join ' / ')") $true "(AI体检员引导学员在其机器执行$($cmds -join '/'))扫描出多少高危?修了吗?" } else { Add-Result "X3" "依赖漏洞审计" "安全红线" "P1" 3 "na" 0 @("未识别到npm/python生态·不适用") $false "" } # ============ 汇总(红牌规则·宁严勿松: P0终态=未通过或未知均触发·唯N/A豁免)============ $eligible = 0; $earned = 0; $redCardItems = @(); $qaItems = @() foreach ($it in $script:Items) { if ($it.transfer_to_qa) { $qaItems += $it.id } if ($it.status -eq "na") { continue } # N/A豁免: 不计分·不触发红牌 if ($it.status -eq "cannot_detect") { # 未知态不计分不扣分·但P0未知=安全红线不默认放行→触发红牌(明察2026-09-16审查写死) if ($it.level -eq "P0") { $redCardItems += "$($it.id)?未知" } continue } $eligible += $it.score_max; $earned += $it.earned if ($it.level -eq "P0" -and $it.status -ne "pass") { $redCardItems += $it.id } } $percent = 0; if ($eligible -gt 0) { $percent = [math]::Round($earned / $eligible * 100, 1) } $grade = "红"; if ($percent -ge 85) { $grade = "绿" } elseif ($percent -ge 70) { $grade = "黄" } $conclusion = "" if ($redCardItems.Count -gt 0) { $conclusion = "黄·暂缓上线(P0红线触警: $($redCardItems -join '/'))" } elseif ($grade -eq "绿") { $conclusion = "绿·具备上线条件" } elseif ($grade -eq "黄") { $conclusion = "黄·建议修复后上线" } else { $conclusion = "红·暂缓上线" } $report = [pscustomobject]@{ package = "wanyou-checkup"; version = $PKV; schema_version = $SCHEMAV stance = "read_only_offline"; network_calls = 0; files_truncated = $truncated target = $Target; platform = "windows" generated_at = (Get-Date).ToString("yyyy-MM-ddTHH:mm:sszzz") elapsed_seconds = [math]::Round($sw.Elapsed.TotalSeconds, 1) items = $script:Items totals = [pscustomobject]@{ eligible_max = $eligible; earned = $earned; percent = $percent grade = $grade; conclusion = $conclusion red_card_items = $redCardItems; transfer_to_qa_items = $qaItems } disclaimer = "本体检为引导式自测+静态只读检查·非渗透测试·非安全审计·结论以申报与本地事实为据·不构成可用性担保" } $jsonPath = Join-Path $OutputDir "report.json" $mdPath = Join-Path $OutputDir "report.md" $utf8 = New-Object System.Text.UTF8Encoding($false) [System.IO.File]::WriteAllText($jsonPath, ($report | ConvertTo-Json -Depth 5), $utf8) # ---- Markdown报告 ---- $md = New-Object System.Text.StringBuilder $null = $md.AppendLine("# 自用系统上线体检报告") $null = $md.AppendLine("") $null = $md.AppendLine("- 体检对象: ``$Target``") $null = $md.AppendLine("- 体检时间: $($report.generated_at) · 耗时$($report.elapsed_seconds)s · 平台: Windows") $null = $md.AppendLine("- 体检包版本: V$PKV(只读·零联网)") if ($truncated) { $null = $md.AppendLine("- 注意: 文件数超上限$MaxFiles·扫描已截断·结果可能不完整") } $null = $md.AppendLine("") $null = $md.AppendLine("## 评分卡") $null = $md.AppendLine("") $null = $md.AppendLine("| 项目 | 结果 |") $null = $md.AppendLine("|---|---|") $null = $md.AppendLine("| 得分 | $earned / $eligible(适用满分·N/A已剔除) |") $null = $md.AppendLine("| 百分制 | **$percent** |") $null = $md.AppendLine("| 红牌(P0触警·未过或未知) | $(if ($redCardItems.Count) { $redCardItems -join ", " } else { "无" }) |") $null = $md.AppendLine("| 结论 | **$conclusion** |") $null = $md.AppendLine("") if ($qaItems.Count -gt 0) { $null = $md.AppendLine("## 待问答补测项(AI体检员请逐题询问并按清单计分)") $null = $md.AppendLine("") foreach ($it in $script:Items) { if ($it.transfer_to_qa) { $null = $md.AppendLine("- **$($it.id) $($it.name)**: $($it.qa_question)") } } $null = $md.AppendLine("") } $null = $md.AppendLine("## 逐项明细") $null = $md.AppendLine("") $null = $md.AppendLine("| ID | 检查项 | 级别 | 状态 | 得分 | 证据 |") $null = $md.AppendLine("|---|---|---|---|---|---|") foreach ($it in $script:Items) { $st = @{ pass="通过"; fail="未过"; na="不适用"; cannot_detect="无法检测" }[$it.status] $ev = ($(if ($it.evidence.Count) { ($it.evidence -join ";") } else { "-" })) $null = $md.AppendLine("| $($it.id) | $($it.name) | $($it.level) | $st | $($it.earned)/$($it.score_max) | $ev |") } $null = $md.AppendLine("") $null = $md.AppendLine("## 改进建议(未过项)") $null = $md.AppendLine("") $remed = @{ "N1"="在项目根目录执行git init并完成首次提交·此后先commit再部署" "N2"=".gitignore加入.env与.env.*(保留!*.example)·历史提交过的真实密钥立即轮换" "N3"="npm用package-lock.json并commit·Python用pip freeze>requirements.txt" "N4"="补README: 项目是什么/技术栈/本地跑通三步" "N5"="写部署文档并真实演练一遍" "N6"="建立.env.example模板(只含键名+占位值)" "S1"="Dockerfile加HEALTHCHECK或compose加healthcheck·非容器暴露/health端点" "S2"="Dockerfile加USER appuser(先useradd+chown应用目录)" "S3"="compose加restart: unless-stopped与mem_limit/cpus上限" "S4"="为核心路径补最小测试集(成功路径+3个异常路径)" "O1"="每日自动备份+异地存放·每季度真实恢复演练" "O2"="部署用mv备份式归档·文档写明回滚命令" "O3"="每次发布打git tag或写CHANGELOG" "O4"="禁全量删除替换·改增量+mv归档·删除命令带明确路径" "O5"="统一错误日志落盘+每天人工看一次" "X1"="密钥全部改env注入·已写死的key立即去服务商后台轮换" "X2"="生产显式关DEBUG·错误页只给通用文案" "X3"="npm audit fix / pip-audit后按建议升级·高危必须处理" } foreach ($it in $script:Items) { if ($it.status -eq "fail") { $null = $md.AppendLine("- **$($it.id) $($it.name)**: $($remed[$it.id])") } } $null = $md.AppendLine("") $null = $md.AppendLine("---") $null = $md.AppendLine("") $null = $md.AppendLine("*本体检为引导式自测+静态只读检查,非渗透测试、非安全审计;结论以申报与本地事实为据,不构成可用性担保。报告由万有AI系统体检包V$PKV在本机生成,未上传任何数据。*") [System.IO.File]::WriteAllText($mdPath, $md.ToString(), $utf8) Write-Host "" Write-Host "===== 体检完成 =====" Write-Host "得分: $earned/$eligible (百分制 $percent) · 结论: $conclusion" if ($redCardItems.Count) { Write-Host "红牌(P0触警): $($redCardItems -join ', ')" } if ($qaItems.Count) { Write-Host "问答补测项: $($qaItems -join ', ')(AI体检员请逐题询问)" } Write-Host "报告: $mdPath" Write-Host "数据: $jsonPath"