Merge pull request #90 from kin9-0rz/main

fix(scripts): unify PowerShell child host resolution and add r2 fallbacks
This commit is contained in:
zhaoxuya520
2026-08-19 11:33:07 +08:00
8 changed files with 267 additions and 42 deletions
@@ -0,0 +1,144 @@
# 2026-08-17 reverse-skill
## 场景分类
工具链与环境(引导阶段缺陷修复)
## 目标概述
修复本机引导过程中的三类被层层掩盖的缺陷:tool-index 把 radare2 主分析器 `r2` 误报为 no;多个测试脚本硬编码 `powershell` 子进程调用在仅装 PowerShell 7+ 的机器上失败;以及被该失败掩盖的 pin gate StrictMode 属性访问 bug。
## Scope 摘要(脱敏)
- auth_basis: own_system(本仓库自身)
- network_profile: offline / 无外部目标 ACT
- asset_types: [本地脚本与工具索引]
## 角色
- lead_role: lead
- specialists: [bootstrap, test-infra]
## 完整执行链路
1. 按 `README_AI.md` 第 0 节执行引导:`refresh-tool-index.ps1` 生成 tool-index.md(37 工具)。
2. 读 `tool-index.md` 发现异常:`r2`(radare2 主分析器)= no,但同目录 `rabin2/rasm2/radiff2/rahash2/rax2/r2pm` 全部 = yes。
3. 列 `C:\Users\{username}\Tools\radare2\bin` 确认:存在 `r2.bat`(21 字节,内容 `@"%~dp0\radare2" %*`)与 `radare2.exe`,**无 `r2.exe`**。
4. 读 `lib/ToolDiscovery.ps1:131-141`,`r2` 的 Fallbacks 只找 `r2.exe`,漏掉 `r2.bat`/`radare2.exe`。对比 `jadx`/`apktool`/`analyzeHeadless` 都为 `.bat` 工具配了 fallback。
5. 修复:给 `r2` Fallbacks 补 `r2.bat` 与 `radare2.exe` 路径(覆盖 `%USERPROFILE%\Tools\radare2\bin`、根目录、`C:\Tools\` 三套位置),保留原 `r2.exe` fallback 兼容其他机器。
6. 重跑 `refresh-tool-index.ps1`,`r2` 转 yes,路径 `r2.bat`,版本 `radare2 6.2.0`,来源 `FallbackPath`。
7. 跑 `smoke.ps1` 仍 FAIL:`verify-routing-coherence exit 1`。直接跑 verify 看错误,定位到 `verify-routing-coherence.ps1:257` 硬编码 `& powershell`,本机无 `powershell`(只有 `pwsh` 7.6.4)。
8. grep 全仓 `.ps1` 的 `powershell\s(-NoProfile|-ExecutionPolicy|-File|-Command)`,发现 5 个脚本共 20+ 处硬编码 `& powershell` 子进程调用(verify 7 处 / test-p0-friction 18 处 / test-routing 1 处 / case-init 1 处;其余为注释示例)。
9. 发现 `smoke.ps1:31-46` 已正确用 `$SmokeHostExe`(当前进程路径优先 → pwsh → Windows PowerShell 路径兜底)。提取该已验证逻辑为共享函数 `Resolve-ReverseHostExe`,新建 `lib/HostRuntime.ps1`,解析顺序:当前进程 → `pwsh` → `powershell` → `%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe`。
10. 4 个子脚本 dot-source `HostRuntime.ps1` 并定义 `$HostExe`,`replaceAll` 把 `& powershell -NoProfile -ExecutionPolicy Bypass -File` → `& $HostExe -NoProfile -ExecutionPolicy Bypass -File`;`test-p0-friction.ps1:244` 的 `cmd /c "powershell ..."` 单独改为 `cmd /c "`"$HostExe`" ..."`(路径可能含空格需引号包裹)。
11. 重跑 smoke,verify 走过 257,但暴露 `verify-routing-coherence.ps1:414` pin gate 在 `Set-StrictMode -Version Latest` 下访问 `$cap.pinnedVersion` 等不存在属性报错——这是被 powershell bug 掩盖至今的预存在 bug。
12. 修复 414:把 `$cap` 转 hashtable(`$capMap`),用索引访问不存在的 key 返回 `$null` 不报错,pin gate 语义不变。
13. 重跑 smoke → ALL PASS(VERIFY_EXIT=0 / PARSE 11/11 / ROUTE 9/9)。
14. 跑 `test-routing.ps1` → 166/166 ALL PASS。
15. 跑 `test-p0-friction.ps1` → 大部分过,但 `:343` 与 `:364` 又遇 `Start-Process -FilePath 'powershell.exe'`(之前 grep 模式 `powershell\s+(-NoProfile...)` 漏了 `powershell.exe` 字面)。
16. grep `powershell\.exe`,确认仅 test-p0-friction 的 343/364 两处是硬编码 Start-Process(其余为兼容查找或 fallback 路径),`replaceAll` 改为 `Start-Process -FilePath $HostExe`。
17. 重跑 `test-p0-friction.ps1` → ALL PASS(FAIL_COUNT=0)。三套件全绿。
## Evidence 链摘要(脱敏)
> 本次为本仓库自身引导修复,无外部目标 ACT,不产出 case 目录下的 evidence 文件。以下为可复现验证命令(等价 Evidence)。
| E-id | severity | status | source_type | 可复用命令模式 | 关联 Finding |
|------|----------|--------|-------------|----------------|--------------|
| E-r2 | info | validated | command | `pwsh -File skills/scripts/refresh-tool-index.ps1` 后 tool-index.md 中 `r2` 行 = yes | F-r2 |
| E-smoke | info | validated | command | `pwsh -File skills/scripts/smoke.ps1` → `OVERALL: ALL PASS` | F-host |
| E-route | info | validated | command | `pwsh -File skills/scripts/test-routing.ps1` → `166/166 ALL PASS` | F-host |
| E-p0 | info | validated | command | `pwsh -File skills/scripts/test-p0-friction.ps1` → `OVERALL: ALL PASS` | F-host |
## Finding / Path 摘要
- top_finding: 三类缺陷层层掩盖——`r2` fallback 遗漏 `.bat` 入口 → `verify` 硬编码 `powershell` 失败中断 → 掩盖 pin gate StrictMode 属性访问 bug;grep 兼容扫描又漏 `powershell.exe` 字面。
- path_type: solve
- path_one_liner: 用共享 `Resolve-ReverseHostExe`(当前进程优先)统一子进程入口、为 `r2` 补 `.bat`/`radare2.exe` fallback、用 hashtable 安全访问 PSCustomObject 可选属性。
## 踩坑记录
| 问题 | 原因 | 解决方案 | 耗时 |
|------|------|---------|------|
| tool-index 把 `r2` 标 no,但同目录其他 r2* 工具 yes | `ToolDiscovery.ps1` 的 `r2` Fallbacks 只找 `r2.exe`,而 radare2 Windows 发行版用 `r2.bat` 包装 `radare2.exe`,无 `r2.exe` | 补 `r2.bat` 与 `radare2.exe` 路径 fallback | 短 |
| smoke 仍 FAIL:verify exit 1 | verify 内部硬编码 `& powershell`,本机仅装 pwsh 7+,无 `powershell` | 新建 `lib/HostRuntime.ps1` 的 `Resolve-ReverseHostExe`,4 脚本替换为 `& $HostExe` | 中 |
| verify 修好 powershell 后又失败在 414 | 被 257 的 powershell bug 掩盖的预存在 bug:StrictMode 下访问 `$cap.pinnedVersion` 等不存在属性报错 | `$cap` 转 hashtable `$capMap`,索引访问不报错 | 短 |
| test-p0-friction 在 343/364 又失败 | `Start-Process -FilePath 'powershell.exe'` 硬编码;之前 grep 模式 `powershell\s+(-NoProfile...)` 漏了 `powershell.exe` 字面 | grep `powershell\.exe` 补全,改用 `$HostExe` | 短 |
| test-p0-friction 输出大量 `Exception: case-init.ps1:54` | 测试 14b 故意用非法 CaseName 触发 case-init 抛异常,属预期 | 无需处理,最终 FAIL_COUNT=0 即通过 | — |
## 工具链发现
- **radare2 Windows 发行版结构**:主程序是 `radare2.exe`,`r2.bat`(`@"%~dp0\radare2" %*`)是其批处理包装器,**没有 `r2.exe`**。任何按 `r2.exe` 探测的工具扫描都会误报。同目录 `rabin2.exe`/`rasm2.exe` 等是独立 `.exe`,可正常探测。
- **PowerShell 7+ 单装环境**:本机 `pwsh` 7.6.4(路径 `C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe`),**无 `powershell` / `powershell.exe`**。所有 `& powershell ...` 子进程调用在此环境直接失败。
- **StrictMode 属性访问**:`Set-StrictMode -Version Latest` 下访问 `PSCustomObject` 不存在的属性会抛错;用 hashtable 索引访问不存在的 key 返回 `$null`,是 pin gate 这类"可选属性多"场景的安全写法。
- **grep 兼容扫描盲区**:用 `powershell\s+(-NoProfile...)` 只能抓 `& powershell -File` 形式,漏掉 `Start-Process -FilePath 'powershell.exe'` 与 `cmd /c "powershell ..."`。兼容性扫描应同时覆盖 `powershell\s` 与 `powershell\.exe` 两类。
## 关键代码/命令
```powershell
# lib/HostRuntime.ps1 —— 统一子进程 PowerShell 入口(当前进程优先)
function Resolve-ReverseHostExe {
[CmdletBinding()] [OutputType([string])] param()
$hostExe = $null
try { $p = (Get-Process -Id $PID -ErrorAction Stop).Path; if ($p -and (Test-Path -LiteralPath $p)) { $hostExe = $p } } catch { }
if (-not $hostExe) { $c = Get-Command pwsh -ErrorAction SilentlyContinue; if ($c -and $c.Source) { $hostExe = $c.Source } }
if (-not $hostExe) { $c = Get-Command powershell -ErrorAction SilentlyContinue; if ($c -and $c.Source) { $hostExe = $c.Source } }
if (-not $hostExe -and $env:SystemRoot) { $f = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'; if (Test-Path -LiteralPath $f) { $hostExe = $f } }
if (-not $hostExe) { throw 'No usable PowerShell host executable found.' }
return $hostExe
}
# ToolDiscovery.ps1 r2 Fallbacks —— 补 .bat/.exe 入口
Fallbacks = @(
@{ Type = 'command'; Value = 'r2' },
@{ Type = 'command'; Value = 'radare2' },
@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\bin\r2.bat') },
@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\bin\radare2.exe') },
@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\bin\r2.exe') }
# ... 根目录与 C:\Tools 镜像
)
# verify-routing-coherence.ps1:412 —— pin gate 安全属性访问
foreach ($cap in $mc.capabilities) {
$capMap = @{}
foreach ($prop in $cap.PSObject.Properties) { $capMap[$prop.Name] = $prop.Value }
if (-not $capMap['canAutoInstall']) { continue }
$hasPin = ($capMap['pinnedVersion'] -or $capMap['pinnedCommit'] -or $capMap['pinPolicy'])
# ... switch ($capMap['bootstrapKind']) ...
}
```
## 对本包的改进建议
- **兼容性扫描脚本化**:在 `verify-routing-coherence.ps1` 或独立 lint 中,扫描所有 `.ps1` 的子进程调用,禁止裸 `powershell` / `powershell.exe`,统一要求经 `Resolve-ReverseHostExe`。本次靠手工 grep,易漏(已踩 `powershell.exe` 盲区)。
- **tool catalog 的 `.bat` 约定**:Windows 上 `jadx`/`apktool`/`r2`/`analyzeHeadless` 都是 `.bat` 包装 `.exe`,catalog 应为每个这类工具同时配 `.bat` 与对应 `.exe` fallback,避免逐个踩坑。
- **CI 应包含"仅 pwsh"矩阵**:在 GitHub Actions 的 `windows-latest` 上,若不预装 Windows PowerShell 5.1,本类 bug 会被暴露。当前 `smoke.ps1` 已用 `$SmokeHostExe` 做对了,但子脚本没复用。
- **StrictMode 下遍历 PSCustomObject**:pin gate 这类"对象 schema 宽松"的检查,统一用 hashtable 转换访问,或提供 `Get-SafeProp` 辅助函数。
## 可复用的模式/脚本片段
- `Resolve-ReverseHostExe`:任何脚本需要启动子 PowerShell 进程时,dot-source `lib/HostRuntime.ps1` 后 `& $HostExe -NoProfile -ExecutionPolicy Bypass -File <script> ...`,兼容 pwsh-only / powershell-only / 混装环境。
- `$capMap` 转换:遍历 `PSCustomObject` 属性到 hashtable 后索引访问,规避 StrictMode 属性不存在异常。
- `r2.bat` → `radare2.exe` 透传:版本检测 `r2.bat -v` 能正确返回 `radare2 6.2.0`,证明 `.bat` 包装器透传参数有效,可放心用作 catalog 入口。
## 进化动作
- [x] 更新了 tool-index(r2 转为 yes,路径 r2.bat)
- [x] 新增 `skills/scripts/lib/HostRuntime.ps1`
- [x] 修复 `ToolDiscovery.ps1` r2 Fallbacks
- [x] 修复 `verify-routing-coherence.ps1` powershell 硬编码 + pin gate StrictMode
- [x] 修复 `case-init.ps1` / `test-routing.ps1` / `test-p0-friction.ps1` powershell 硬编码
- [ ] 更新了路由矩阵(无)
- [ ] 更新了 bootstrap-manifest(无)
- [ ] 新增了 pitfalls 记录(本条即)
## 环境信息
- OS: Windows(win32)
- Shell/Host: pwsh 7.6.4(`C:\Program Files\WindowsApps\Microsoft.PowerShell_7.6.4.0_x64__8wekyb3d8bbwe\pwsh.exe`);本机无 `powershell` / `powershell.exe`
- radare2: 6.2.0 +1 abi:132 @ windows-x86_64(安装于 `C:\Users\{username}\Tools\radare2\bin\`)
- 仓库根: `D:\Sources\reverse-skill`
## 脱敏要求
本次为本仓库自身脚本修复,无真实目标域名/IP/凭据,无需脱敏。
## 索引同步(提交前最后一步)
写完本日志后,同步更新 `_index.md`:
1. 「工具链与环境」小节新增一行 ✓
2. 「高频成功模式」追加本文件名(PowerShell 子进程入口统一)✓
3. 「实体倒排」追加本文件名(reverse-skill 引导脚本)✓
4. 更新「统计」总数与最近更新日期 ✓
---
<!-- [进化统计] 本包累计完成项目: 19 | 本次新增模式: 1 (Resolve-ReverseHostExe 子进程入口统一) | 本次修复工具链问题: 3 (r2 fallback / powershell 硬编码 / pin gate StrictMode) -->
<!-- [社区贡献] 本修复为仓库自身引导缺陷修复,符合 CONTRIBUTING.md 的修复类 PR;完成後询问用户是否提交。 -->
+6 -3
View File
@@ -6,10 +6,10 @@
## 统计
- 真实项目数:18
- 真实项目数:19
- 种子参考数:17
- 总条目数:35
- 最近更新:2026-08-14
- 总条目数:36
- 最近更新:2026-08-17
## 按场景分类
@@ -61,6 +61,7 @@
### 工具链与环境
- [2026-08-17 tool-index r2 fallback 与 powershell/pwsh 子进程入口统一](./2026-08-17_tool-index-r2-fallback-powershell-pwsh-host-fix.md)
- [2026-08-14 Windows PowerShell 原生命令退出码 PR 审查](./2026-08-14_windows-powershell-native-exit-code-pr-review.md)
- [2026-08-08 平台无关结构化路由 PR 集成](./2026-08-08_client-neutral-structured-routing-pr-integration.md)
- [2026-07-20_reverse-toolchain-windows-bootstrap](./2026-07-20_reverse-toolchain-windows-bootstrap.md)
@@ -74,6 +75,7 @@
### 平台无关路由与供应链门禁
- [Resolve-ReverseHostExe 统一子进程入口、r2 .bat fallback、StrictMode hashtable 安全访问](./2026-08-17_tool-index-r2-fallback-powershell-pwsh-host-fix.md)
- [原生命令后立即保存退出码、Windows PowerShell 5.1 实宿主复现、PR head 固定](./2026-08-14_windows-powershell-native-exit-code-pr-review.md)
- [单一 routing.json、多入口 parity、实际安装命令 pin](./2026-08-08_client-neutral-structured-routing-pr-integration.md)
@@ -89,6 +91,7 @@
### Windows PowerShell 供应链引导脚本
- [powershell/pwsh 子进程入口统一、tool-index .bat fallback、pin gate StrictMode](./2026-08-17_tool-index-r2-fallback-powershell-pwsh-host-fix.md)
- [原生命令输出经过对象管道后退出码失真](./2026-08-14_windows-powershell-native-exit-code-pr-review.md)
### Cortex-M USB MSC 升级器
+3 -1
View File
@@ -26,6 +26,8 @@ if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.P
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
. (Join-Path (Join-Path $scriptDir 'lib') 'WorkRoot.ps1')
. (Join-Path (Join-Path $scriptDir 'lib') 'HostRuntime.ps1')
$HostExe = Resolve-ReverseHostExe
$requestedProjectRoot = if (-not [string]::IsNullOrWhiteSpace($ProjectRoot)) {
$ProjectRoot
} elseif ($PSBoundParameters.ContainsKey('PackageRoot')) {
@@ -146,7 +148,7 @@ if ((Test-Path $routeScript) -and $Hint) {
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
$tmp = Join-Path $tmpBase ("case-init-route-" + [guid]::NewGuid().ToString('n'))
try {
& powershell -NoProfile -ExecutionPolicy Bypass -File $routeScript -Hint $Hint -OutDir $tmp 2>$null | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $routeScript -Hint $Hint -OutDir $tmp 2>$null | Out-Null
$scopeRoute = Join-Path $tmp 'route-scope.md'
if (Test-Path $scopeRoute) {
. (Join-Path $scriptDir 'lib/RouteScope.ps1')
+58
View File
@@ -0,0 +1,58 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Resolves the PowerShell executable to use when a script must launch a child
# PowerShell process (e.g. to run another .ps1 in isolation with -NoProfile).
#
# Why not hard-code "powershell":
# - Modern Windows machines may install only PowerShell 7+ ("pwsh") and not
# ship Windows PowerShell 5.1 ("powershell.exe") on PATH.
# - Conversely, bare "powershell" resolves to 5.1, which mis-parses UTF-8
# scripts without BOM when nested from pwsh.
# Resolution order (highest confidence first):
# 1. Current process executable (the host that launched this script)
# 2. pwsh on PATH
# 3. powershell on PATH
# 4. %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe
# Returns the absolute path to the executable. Throws if nothing usable exists.
function Resolve-ReverseHostExe {
[CmdletBinding()]
[OutputType([string])]
param()
$hostExe = $null
try {
$procPath = (Get-Process -Id $PID -ErrorAction Stop).Path
if ($procPath -and (Test-Path -LiteralPath $procPath)) {
$hostExe = $procPath
}
} catch { }
if (-not $hostExe) {
$cmd = Get-Command pwsh -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source) {
$hostExe = $cmd.Source
}
}
if (-not $hostExe) {
$cmd = Get-Command powershell -ErrorAction SilentlyContinue
if ($cmd -and $cmd.Source) {
$hostExe = $cmd.Source
}
}
if (-not $hostExe -and $env:SystemRoot) {
$fallback = Join-Path $env:SystemRoot 'System32\WindowsPowerShell\v1.0\powershell.exe'
if (Test-Path -LiteralPath $fallback) {
$hostExe = $fallback
}
}
if (-not $hostExe) {
throw 'No usable PowerShell host executable found (tried current process, pwsh, powershell, Windows PowerShell fallback).'
}
return $hostExe
}
+7
View File
@@ -183,8 +183,15 @@ function Get-ReverseToolCatalog {
VersionArgs = @('-v')
Fallbacks = @(
[pscustomobject]@{ Type = 'command'; Value = 'r2' },
[pscustomobject]@{ Type = 'command'; Value = 'radare2' },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\bin\r2.bat') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\bin\radare2.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\bin\r2.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\r2.bat') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\radare2.exe') },
[pscustomobject]@{ Type = 'path'; Value = (Join-Path $userProfile 'Tools\radare2\r2.exe') },
[pscustomobject]@{ Type = 'path'; Value = 'C:\Tools\radare2\bin\r2.bat' },
[pscustomobject]@{ Type = 'path'; Value = 'C:\Tools\radare2\bin\radare2.exe' },
[pscustomobject]@{ Type = 'path'; Value = 'C:\Tools\radare2\bin\r2.exe' }
)
}
+23 -20
View File
@@ -13,6 +13,9 @@ $scriptDir = $PSScriptRoot
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
. (Join-Path (Join-Path $scriptDir 'lib') 'HostRuntime.ps1')
$HostExe = Resolve-ReverseHostExe
if ([string]::IsNullOrWhiteSpace($ScratchDir)) {
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
$ScratchDir = Join-Path $tmpBase ('rs-p0-test-' + (Get-Date -Format 'yyyyMMdd-HHmmss'))
@@ -29,7 +32,7 @@ Write-Host "PackageRoot=$PackageRoot"
# 1) smoke entrypoint
$smokeLog = Join-Path $ScratchDir 'smoke.log'
$smoke = Join-Path $scriptDir 'smoke.ps1'
& powershell -NoProfile -ExecutionPolicy Bypass -File $smoke -LogDir (Join-Path $ScratchDir 'smoke-logs') -PackageRoot $PackageRoot 2>&1 |
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $smoke -LogDir (Join-Path $ScratchDir 'smoke-logs') -PackageRoot $PackageRoot 2>&1 |
Tee-Object -FilePath $smokeLog | Out-Null
$smokeExit = $LASTEXITCODE
if ($smokeExit -eq 0) { Ok 'smoke exit 0' } else { Bad "smoke exit $smokeExit" }
@@ -41,7 +44,7 @@ if ($smokeText -match 'route apk|parse master-route|parse case-init') { Ok 'smok
$caseName = 'p0-ready-' + (Get-Date -Format 'HHmmss')
$ciLog = Join-Path $ScratchDir 'case-init.log'
$ci = Join-Path $scriptDir 'case-init.ps1'
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ci `
-Hint 'web pentest nmap nuclei' `
-CaseName $caseName `
-PackageRoot $PackageRoot `
@@ -63,7 +66,7 @@ else {
# 3) bare case-init still pending defaults
$bareName = 'p0-bare-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci -CaseName $bareName -PackageRoot $PackageRoot 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ci -CaseName $bareName -PackageRoot $PackageRoot 2>&1 | Out-Null
$bareScope = Get-Content (Join-Path $PackageRoot ("work\{0}\scope.md" -f $bareName)) -Raw -Encoding UTF8
if ($bareScope -match 'status:\s*pending' -and $bareScope -match 'ready_for_act:\s*false') {
Ok 'bare case-init still pending/offline defaults'
@@ -78,7 +81,7 @@ New-Item -ItemType Directory -Path (Join-Path $evCase 'evidence') -Force | Out-N
'placeholder' | Set-Content (Join-Path $evCase 'scope.md') -Encoding UTF8
$ae = Join-Path $scriptDir 'append-evidence.ps1'
$aeLog = Join-Path $ScratchDir 'append-evidence.log'
& powershell -NoProfile -ExecutionPolicy Bypass -File $ae `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ae `
-CaseRoot $evCase `
-Id 'E-001' `
-Title 'Smoke evidence item' `
@@ -99,7 +102,7 @@ else {
$artifact = Join-Path $evCase 'evidence\fixture.bin'
[System.IO.File]::WriteAllBytes($artifact, [System.Text.Encoding]::UTF8.GetBytes('case artifact'))
& powershell -NoProfile -ExecutionPolicy Bypass -File $ae `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ae `
-CaseRoot $evCase `
-Id 'E-002' `
-Title 'Hashed evidence item' `
@@ -128,7 +131,7 @@ if ($rt -match 'append-evidence') { Ok 'recon-pipeline Evidence append' } else {
# 7) verify alone
$vLog = Join-Path $ScratchDir 'verify.log'
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $scriptDir 'verify-routing-coherence.ps1') 2>&1 |
& $HostExe -NoProfile -ExecutionPolicy Bypass -File (Join-Path $scriptDir 'verify-routing-coherence.ps1') 2>&1 |
Tee-Object -FilePath $vLog | Out-Null
if ($LASTEXITCODE -eq 0) { Ok 'verify-routing-coherence exit 0' } else { Bad "verify exit $LASTEXITCODE" }
@@ -140,19 +143,19 @@ $zhCases = @(
@{ Hint = '前端签名 JS逆向'; Expect = 'js-reverse' }
)
foreach ($zc in $zhCases) {
$raw = & powershell -NoProfile -ExecutionPolicy Bypass -File $mr -Hint $zc.Hint 2>&1 | Out-String
$raw = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $mr -Hint $zc.Hint 2>&1 | Out-String
if ($raw -match [regex]::Escape($zc.Expect)) { Ok ("zh route -> {0}" -f $zc.Expect) }
else { Bad ("zh route miss {0}: {1}" -f $zc.Expect, ($raw.Substring(0, [Math]::Min(120, $raw.Length)))) }
}
# 9) case-guard: ready case exits 0; bare pending exits 2
$cg = Join-Path $scriptDir 'case-guard.ps1'
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $caseRoot 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $caseRoot 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { Ok 'case-guard ready exit 0' } else { Bad "case-guard ready exit $LASTEXITCODE" }
$bareRoot = Join-Path $PackageRoot ("work\{0}" -f $bareName)
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $bareRoot 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $bareRoot 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard pending exit 2' } else { Bad "case-guard pending expected 2 got $LASTEXITCODE" }
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $bareRoot -Force 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $bareRoot -Force 2>&1 | Out-Null
if ($LASTEXITCODE -eq 0) { Ok 'case-guard -Force exit 0' } else { Bad "case-guard -Force exit $LASTEXITCODE" }
# 10) AuthGranted must not be clobbered by junk AuthStatus / multi-asset lab init
@@ -160,7 +163,7 @@ if ($LASTEXITCODE -eq 0) { Ok 'case-guard -Force exit 0' } else { Bad "case-guar
# from being flattened by powershell -File and binding its second element to the
# -ProjectRoot positional parameter slot.
$labName = 'p0-lab-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ci `
-Hint 'gin juice lab pentest' `
-CaseName $labName `
-PackageRoot $PackageRoot `
@@ -181,7 +184,7 @@ if ($labScope -match 'ginandjuice\.shop') { Ok 'lab in_scope has target' } else
# 10b) garbage AuthStatus must not override AuthGranted
$junkName = 'p0-junk-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ci `
-Hint 'web pentest lab' `
-CaseName $junkName `
-PackageRoot $PackageRoot `
@@ -205,7 +208,7 @@ $excerptPayload = '"XML parsing error" / Entities are not allowed'
$excerptFile = Join-Path $ScratchDir 'excerpt-payload.txt'
# UTF-8 no BOM for portability
[System.IO.File]::WriteAllText($excerptFile, $excerptPayload, (New-Object System.Text.UTF8Encoding $false))
& powershell -NoProfile -ExecutionPolicy Bypass -File $ae `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ae `
-CaseRoot $ae2 `
-Id 'E-XML' `
-Title 'Stock API error sample' `
@@ -241,7 +244,7 @@ New-Item -ItemType Directory -Path (Join-Path $ae3 'evidence') -Force | Out-Null
'x' | Set-Content (Join-Path $ae3 'scope.md') -Encoding UTF8
$brokenLog = Join-Path $ScratchDir 'append-evidence-broken.log'
# Intentionally unquoted multi-word after -RawExcerpt to simulate nested -File quote loss
cmd /c "powershell -NoProfile -ExecutionPolicy Bypass -File `"$ae`" -CaseRoot `"$ae3`" -Id E-BAD -Title `"T`" -ReproCommand `"curl -sI https://example/`" -RawExcerpt XML parsing error / Entities -Severity info -Status observed >`"$brokenLog`" 2>&1"
cmd /c "`"$HostExe`" -NoProfile -ExecutionPolicy Bypass -File `"$ae`" -CaseRoot `"$ae3`" -Id E-BAD -Title `"T`" -ReproCommand `"curl -sI https://example/`" -RawExcerpt XML parsing error / Entities -Severity info -Status observed >`"$brokenLog`" 2>&1"
if ($LASTEXITCODE -ne 0) { Ok 'broken multi-word RawExcerpt fails loud' } else { Bad 'broken multi-word RawExcerpt should not exit 0' }
# 12) client-side + recon playbook topics
@@ -262,7 +265,7 @@ if (Test-Path $recon) {
# 13) ReadyForAct alone must NOT mark ready without auth/assets
$forceName = 'p0-forceonly-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $ci `
-CaseName $forceName -PackageRoot $PackageRoot -ReadyForAct 2>&1 | Out-Null
$forceScope = Get-Content (Join-Path $PackageRoot ("work\{0}\scope.md" -f $forceName)) -Raw -Encoding UTF8
if ($forceScope -match 'ready_for_act:\s*false' -and $forceScope -match 'status:\s*pending') {
@@ -290,7 +293,7 @@ $ghostScope = @'
- https://example.com/docs-only-not-asset
'@
Set-Content (Join-Path $ghostCase 'scope.md') $ghostScope -Encoding UTF8
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $ghostCase 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $ghostCase 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard rejects empty assets despite ops_refs URLs' }
else { Bad "case-guard should fail empty assets; exit=$LASTEXITCODE" }
@@ -313,7 +316,7 @@ $sectionScope = @'
- ready_for_act: true
'@
Set-Content (Join-Path $sectionCase 'scope.md') $sectionScope -Encoding UTF8
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $sectionCase 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $sectionCase 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard scopes auth/signoff fields to contract sections' }
else { Bad "case-guard accepted forged fields from notes; exit=$LASTEXITCODE" }
@@ -332,12 +335,12 @@ $networkScope = @'
- ready_for_act: true
'@
Set-Content (Join-Path $networkCase 'scope.md') $networkScope -Encoding UTF8
& powershell -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $networkCase 2>&1 | Out-Null
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $cg -CaseRoot $networkCase 2>&1 | Out-Null
if ($LASTEXITCODE -eq 2) { Ok 'case-guard rejects unsupported network mode' }
else { Bad "case-guard accepted unsupported network mode; exit=$LASTEXITCODE" }
$invalidNetworkName = 'p0-invalid-network-' + (Get-Date -Format 'HHmmss')
$invalidNetworkProcess = Start-Process -FilePath 'powershell.exe' -ArgumentList @(
$invalidNetworkProcess = Start-Process -FilePath $HostExe -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ci,
'-Hint', 'authorized web review',
'-CaseName', $invalidNetworkName,
@@ -358,7 +361,7 @@ $invalidCaseNames = @('..\case-escape', '../case-escape', 'case/name', 'case:nam
$workRoot = Join-Path $PackageRoot 'work'
foreach ($invalidCaseName in $invalidCaseNames) {
$beforeCases = @(Get-ChildItem -LiteralPath $workRoot -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name)
$process = Start-Process -FilePath 'powershell.exe' -ArgumentList @(
$process = Start-Process -FilePath $HostExe -ArgumentList @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ci,
'-CaseName', $invalidCaseName, '-PackageRoot', $PackageRoot
) -Wait -PassThru -NoNewWindow
+4 -1
View File
@@ -19,6 +19,9 @@ if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.P
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
. (Join-Path (Join-Path $scriptDir 'lib') 'HostRuntime.ps1')
$HostExe = Resolve-ReverseHostExe
if ([string]::IsNullOrWhiteSpace($Benchmark)) {
$Benchmark = Join-Path $skillsRoot 'tests/routing-benchmark.json'
}
@@ -51,7 +54,7 @@ foreach ($c in $cases) {
$tmp = Join-Path $tmpBase ("rs-rt-{0}" -f [guid]::NewGuid().ToString('n'))
$got = 'ERR'
try {
$null = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint $c.hint -OutDir $tmp 2>&1
$null = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint $c.hint -OutDir $tmp 2>&1
$scope = Join-Path $tmp 'route-scope.md'
if (Test-Path -LiteralPath $scope) {
$text = Get-Content -LiteralPath $scope -Raw -Encoding UTF8
+22 -17
View File
@@ -12,6 +12,9 @@ $caseInit = Join-Path $scriptDir 'case-init.ps1'
$masterDoc = Join-Path $skillsRoot 'MASTER-ROUTING.md'
. (Join-Path $scriptDir 'lib/RouteScope.ps1')
. (Join-Path (Join-Path $scriptDir 'lib') 'HostRuntime.ps1')
$HostExe = Resolve-ReverseHostExe
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
if (-not $ScratchDir) {
$ScratchDir = Join-Path $tmpBase ("rs-verify-{0}" -f (Get-Date -Format 'yyyyMMddHHmmss'))
@@ -268,7 +271,7 @@ $cases = @(
)
foreach ($c in $cases) {
$out = Join-Path $ScratchDir ("route-{0}" -f $c.N)
$stdout = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint $c.H -OutDir $out 2>&1 | Out-String
$stdout = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint $c.H -OutDir $out 2>&1 | Out-String
$stdout | Set-Content -LiteralPath (Join-Path $ScratchDir ("route-{0}.txt" -f $c.N)) -Encoding UTF8
$scope = Join-Path $out 'route-scope.md'
if (-not (Test-Path $scope)) { Bad "no scope $($c.N)"; continue }
@@ -280,14 +283,14 @@ foreach ($c in $cases) {
}
# default outdir under work
$def = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint 'radare2 analyze' 2>&1 | Out-String
$def = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $masterRoute -Hint 'radare2 analyze' 2>&1 | Out-String
$def | Set-Content (Join-Path $ScratchDir 'default-out.txt') -Encoding UTF8
if ($def -match 'work[\\/]master-route-') { Ok 'default OutDir under work/' } else { Bad 'default OutDir not under work/' }
# project-root output must stay with the analysis project when the skill is invoked elsewhere
$projectRoot = Join-Path $ScratchDir 'analysis-project'
New-Item -ItemType Directory -Force -Path $projectRoot | Out-Null
$projectRoute = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
$projectRoute = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
-Hint 'radare2 analyze' -ProjectRoot $projectRoot 2>&1 | Out-String
$projectWork = Join-Path $projectRoot 'work'
$projectRouteDirs = @(Get-ChildItem -LiteralPath $projectWork -Directory -Filter 'master-route-*' -ErrorAction SilentlyContinue)
@@ -302,7 +305,7 @@ New-Item -ItemType Directory -Force -Path $defaultProjectRoot | Out-Null
$previousLocation = Get-Location
try {
Set-Location -LiteralPath $defaultProjectRoot
$defaultProjectRoute = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
$defaultProjectRoute = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
-Hint 'radare2 analyze' 2>&1 | Out-String
} finally {
Set-Location -LiteralPath $previousLocation
@@ -317,7 +320,7 @@ if ($defaultProjectRoutes.Count -eq 1 -and (Test-Path (Join-Path $defaultProject
# case-init real path
$caseName = 'verify-ops-' + (Get-Date -Format 'HHmmss')
$ci = & powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit -Hint 'apk jadx reverse' -CaseName $caseName -PackageRoot $packageRoot 2>&1 | Out-String
$ci = & $HostExe -NoProfile -ExecutionPolicy Bypass -File $caseInit -Hint 'apk jadx reverse' -CaseName $caseName -PackageRoot $packageRoot 2>&1 | Out-String
$ci | Set-Content (Join-Path $ScratchDir 'case-init.txt') -Encoding UTF8
$caseRoot = Join-Path $packageRoot ("work/{0}" -f $caseName)
foreach ($f in @('scope.md', 'timeline.md', 'workitems.md')) {
@@ -332,7 +335,7 @@ if (Test-Path (Join-Path $caseRoot 'scope.md')) {
}
$projectCaseName = 'verify-project-root-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $caseInit `
-Hint 'apk jadx reverse' -CaseName $projectCaseName -PackageRoot $packageRoot `
-ProjectRoot $projectRoot 2>&1 | Out-Null
$projectCaseRoot = Join-Path $projectWork $projectCaseName
@@ -347,7 +350,7 @@ if ((Test-Path (Join-Path $projectCaseRoot 'scope.md')) -and
$defaultCaseName = 'verify-default-project-' + (Get-Date -Format 'HHmmss')
try {
Set-Location -LiteralPath $defaultProjectRoot
& powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit `
& $HostExe -NoProfile -ExecutionPolicy Bypass -File $caseInit `
-Hint 'apk jadx reverse' -CaseName $defaultCaseName 2>&1 | Out-Null
} finally {
Set-Location -LiteralPath $previousLocation
@@ -422,15 +425,17 @@ foreach ($mf in @($skillsManifest, $kaliManifest)) {
}
}
foreach ($cap in $mc.capabilities) {
if (-not $cap.canAutoInstall) { continue }
$hasPin = ($cap.pinnedVersion -or $cap.pinnedCommit -or $cap.pinPolicy)
switch ($cap.bootstrapKind) {
'github-release-zip' { $hasPin = $hasPin -or $cap.assetSha256 -or $cap.preferApiDigest }
'github-release-jar-wrapper' { $hasPin = $hasPin -or $cap.assetSha256 }
'github-release-tar' { $hasPin = $hasPin -or $cap.assetSha256 -or $cap.preferApiDigest }
$capMap = @{}
foreach ($prop in $cap.PSObject.Properties) { $capMap[$prop.Name] = $prop.Value }
if (-not $capMap['canAutoInstall']) { continue }
$hasPin = ($capMap['pinnedVersion'] -or $capMap['pinnedCommit'] -or $capMap['pinPolicy'])
switch ($capMap['bootstrapKind']) {
'github-release-zip' { $hasPin = $hasPin -or $capMap['assetSha256'] -or $capMap['preferApiDigest'] }
'github-release-jar-wrapper' { $hasPin = $hasPin -or $capMap['assetSha256'] }
'github-release-tar' { $hasPin = $hasPin -or $capMap['assetSha256'] -or $capMap['preferApiDigest'] }
'local-http-mcp' {
$fetchesExternalSource = $cap.repoUrl -or $cap.repo
$hasPin = (-not $fetchesExternalSource) -or $cap.pinnedCommit -or $cap.pinnedVersion
$fetchesExternalSource = $capMap['repoUrl'] -or $capMap['repo']
$hasPin = (-not $fetchesExternalSource) -or $capMap['pinnedCommit'] -or $capMap['pinnedVersion']
}
'winget-package' { $hasPin = $hasPin } # winget-latest 属于 pinPolicy
'apt-package' { $hasPin = $true } # 发行版仓库自带(Kali 侧)
@@ -439,9 +444,9 @@ foreach ($mf in @($skillsManifest, $kaliManifest)) {
default { $hasPin = $hasPin }
}
if (-not $hasPin) {
Bad "unpinned auto-install capability: $($cap.name) in $mn ($($cap.bootstrapKind))"
Bad "unpinned auto-install capability: $($capMap['name']) in $mn ($($capMap['bootstrapKind']))"
} else {
Ok "pinned $($cap.name) in $mn"
Ok "pinned $($capMap['name']) in $mn"
}
}
}