reverse-skills-max: initial public release (restored, redacted, hardened)

This commit is contained in:
baradok609hecl1pq6qi
2026-10-20 02:17:15 +03:00
commit da24c2bd20
2227 changed files with 443931 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
#Requires -Version 5.1
# Append one Evidence item under work/<case>/evidence/ (ops Evidence contract).
#
# Usage (simple):
# powershell -File skills/scripts/append-evidence.ps1 -CaseRoot work\my-case `
# -Id E-001 -Title "Open clock API" -ReproCommand 'curl -sI https://example/' `
# -Severity info -Status observed
#
# Special characters / spaces / quotes in excerpts (recommended for nested -File):
# Set-Content excerpt.txt -Value '"XML parsing error" / Entities are not allowed'
# powershell -File skills/scripts/append-evidence.ps1 ... -RawExcerptFile excerpt.txt
#
# NOTE: Nested `powershell -File` splits unquoted multi-word values. Prefer -RawExcerptFile
# or call the script in-process: & .\append-evidence.ps1 -RawExcerpt 'full text here'
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string] $CaseRoot,
[Parameter(Mandatory = $true)]
[string] $Id,
[Parameter(Mandatory = $true)]
[string] $Title,
[Parameter(Mandatory = $true)]
[string] $ReproCommand,
[string] $Severity = 'info',
[string] $Status = 'observed',
[string] $SourceType = 'command',
[string] $RawExcerpt = '',
[string] $RawExcerptFile = '',
[string] $Location = '',
[string] $Notes = '',
[string] $NotesFile = '',
[string] $ReproCommandFile = '',
[string] $ArtifactPath = ''
)
$ErrorActionPreference = 'Stop'
# Advanced functions reject unknown positionals at bind time; keep explicit check for edge hosts.
if ($args -and $args.Count -gt 0) {
$extra = ($args | ForEach-Object { [string]$_ }) -join ' | '
throw ("Unexpected arguments (likely -RawExcerpt/-Title quoting broke multi-word value): {0}. Use -RawExcerptFile for special characters, or pass a single quoted string." -f $extra)
}
$allowedSeverity = @('critical', 'high', 'medium', 'low', 'info', 'n/a', 'n/a_re')
$allowedStatus = @('observed', 'candidate', 'validated', 'false_positive', 'accepted_risk')
function ConvertTo-YamlBlock([string] $text) {
if ([string]::IsNullOrEmpty($text)) { return ' n/a' }
$clean = ($text -replace '[\x00-\x08\x0B\x0C\x0E-\x1F]', '')
$lines = $clean -split "`r?`n", -1
if ($lines.Count -eq 0) { return ' n/a' }
return (($lines | ForEach-Object { ' ' + $_ }) -join [Environment]::NewLine)
}
function ConvertTo-SingleLine([string] $text) {
if ([string]::IsNullOrEmpty($text)) { return 'n/a' }
$t = ($text -replace '[\x00-\x1F]', ' ')
$t = $t -replace '\s+', ' '
return $t.Trim()
}
function Read-OptionalFile([string] $path) {
if ([string]::IsNullOrWhiteSpace($path)) { return $null }
if (-not (Test-Path -LiteralPath $path)) {
throw "File not found: $path"
}
return [System.IO.File]::ReadAllText((Resolve-Path -LiteralPath $path), [System.Text.Encoding]::UTF8)
}
if (-not (Test-Path -LiteralPath $CaseRoot)) {
throw "CaseRoot does not exist: $CaseRoot"
}
# File overrides beat inline strings (safe path for special characters under powershell -File)
$fromReproFile = Read-OptionalFile $ReproCommandFile
if ($null -ne $fromReproFile) { $ReproCommand = $fromReproFile }
$fromExcerptFile = Read-OptionalFile $RawExcerptFile
if ($null -ne $fromExcerptFile) { $RawExcerpt = $fromExcerptFile }
$fromNotesFile = Read-OptionalFile $NotesFile
if ($null -ne $fromNotesFile) { $Notes = $fromNotesFile }
$contentHash = 'n/a'
$artifactRef = 'n/a'
if (-not [string]::IsNullOrWhiteSpace($ArtifactPath)) {
$artifactCandidate = $ArtifactPath
if (-not [System.IO.Path]::IsPathRooted($artifactCandidate)) {
$caseArtifact = Join-Path $CaseRoot $artifactCandidate
if (Test-Path -LiteralPath $caseArtifact -PathType Leaf) { $artifactCandidate = $caseArtifact }
}
if (-not (Test-Path -LiteralPath $artifactCandidate -PathType Leaf)) {
throw "ArtifactPath must point to a file inside Caseroot:REDACTED"
}
$caseRootFull = (Resolve-Path -LiteralPath $CaseRoot).Path
$artifactFull = (Resolve-Path -LiteralPath $artifactCandidate).Path
$casePrefix = $caseRootFull.TrimEnd('\', '/') + [System.IO.Path]::DirectorySeparatorChar
if (-not $artifactFull.StartsWith($casePrefix, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "ArtifactPath must point inside Caseroot:REDACTED"
}
$artifactRef = $artifactFull.Substring($casePrefix.Length) -replace '\\', '/'
$contentHash = 'sha256:' + (Get-FileHash -LiteralPath $artifactFull -Algorithm SHA256).Hash.ToLowerInvariant()
}
$evDir = Join-Path $CaseRoot 'evidence'
New-Item -ItemType Directory -Force -Path $evDir | Out-Null
$idSafe = ($Id -replace '[^\w\-]+', '-').Trim('-')
if ($idSafe -notmatch '^E-') { $idSafe = 'E-' + $idSafe }
$fileName = $idSafe + '.md'
$path = Join-Path $evDir $fileName
$observed = Get-Date -Format 'o'
$titleLine = ConvertTo-SingleLine $Title
$sev = (ConvertTo-SingleLine $Severity).ToLowerInvariant()
if ($allowedSeverity -notcontains $sev) {
throw ("Invalid -Severity '{0}' (allowed: {1})" -f $Severity, ($allowedSeverity -join ', '))
}
$st = (ConvertTo-SingleLine $Status).ToLowerInvariant()
if ($allowedStatus -notcontains $st) {
throw ("Invalid -Status '{0}' (allowed: {1})" -f $Status, ($allowedStatus -join ', '))
}
$src = ConvertTo-SingleLine $SourceType
$loc = ConvertTo-SingleLine $(if ($Location) { $Location } else { 'n/a' })
if ([string]::IsNullOrWhiteSpace($ReproCommand)) {
throw '-ReproCommand (or -ReproCommandFile) is required and must be non-empty'
}
$reproBlock = ConvertTo-YamlBlock $ReproCommand
$excerptBlock = ConvertTo-YamlBlock $(if ($RawExcerpt) { $RawExcerpt } else { 'n/a' })
$notesBlock = ConvertTo-YamlBlock $(if ($Notes) { $Notes } else { 'n/a' })
$body = @"
### $idSafe
- title: $titleLine
- observed_at: $observed
- source_type: $src
- source_ref: append-evidence.ps1
- content_hash: $contentHash
- artifact_path: $artifactRef
- severity: $sev
- status: $st
- location: $loc
- repro_command: |
$reproBlock
- raw_excerpt: |
$excerptBlock
- linked_workitem: n/a
- supersedes: none
- notes: |
$notesBlock
"@
$utf8 = New-Object System.Text.UTF8Encoding $false
$stream = $null
$writer = $null
try {
try {
$stream = [System.IO.File]::Open(
$path,
[System.IO.FileMode]::CreateNew,
[System.IO.FileAccess]::Write,
[System.IO.FileShare]::None
)
} catch [System.IO.IOException] {
if (Test-Path -LiteralPath $path) {
throw ("Evidence record already exists and is immutable: {0}. Use a new -Id." -f $fileName)
}
throw
}
$writer = [System.IO.StreamWriter]::new($stream, $utf8)
$writer.Write($body)
} finally {
if ($null -ne $writer) {
$writer.Dispose()
} elseif ($null -ne $stream) {
$stream.Dispose()
}
}
$index = Join-Path $evDir 'INDEX.md'
$line = "- $idSafe | $sev | $st | $titleLine | $fileName"
if (-not (Test-Path -LiteralPath $index)) {
$hdr = @"
# Evidence index
| id | severity | status | title | file |
|----|----------|--------|-------|------|
"@
[System.IO.File]::WriteAllText($index, $hdr, $utf8)
}
Add-Content -Path $index -Value $line -Encoding UTF8
Write-Host ("EVIDENCE -> {0}" -f $path) -ForegroundColor Green
Write-Host ("ID: {0} | {1} | {2}" -f $idSafe, $sev, $st)
exit 0

View File

@@ -0,0 +1,328 @@
{
"capabilities": [
{
"name": "jadx",
"bootstrapKind": "github-release-zip",
"repo": "skylot/jadx",
"assetRegex": "^jadx-1\\.5\\.6\\.zip$",
"installDir": "%USERPROFILE%\\Tools\\jadx",
"docsUrl": "https://github.com/skylot/jadx",
"canAutoInstall": true,
"verifyCommand": "jadx",
"releaseTag": "v1.5.6",
"assetSha256": "545ea2be9c242511bc145755cf4bda2485ade42966e096f8b4d3da2a230e8974"
},
{
"name": "apktool",
"bootstrapKind": "github-release-jar-wrapper",
"repo": "iBotPeaches/Apktool",
"assetRegex": "^apktool_3\\.0\\.2\\.jar$",
"installDir": "%USERPROFILE%\\Tools\\apktool",
"wrapperName": "apktool.bat",
"docsUrl": "https://apktool.org/",
"canAutoInstall": true,
"verifyCommand": "apktool",
"releaseTag": "v3.0.2",
"assetSha256": "eee4669a704a14e0623407e6701b0b91887e61e1e4049cb7a82833e14ae8b5fd"
},
{
"name": "jeb-pro",
"bootstrapKind": "manual",
"docsUrl": "https://www.pnfsoftware.com/jeb/",
"canAutoInstall": false,
"verifyCommand": "jeb_wincon",
"manualInstallHint": "JEB Pro is commercial. Obtain a valid license from PNF Software, install it manually, then run refresh-tool-index.ps1. Optional community MCP bridges must pass the skill-supply-chain review before registration."
},
{
"name": "frida",
"bootstrapKind": "pip-package",
"pipPackage": "frida-tools==14.10.4",
"pinnedVersion": "14.10.4",
"docsUrl": "https://frida.re/",
"canAutoInstall": true,
"verifyCommand": "frida"
},
{
"name": "frida-ps",
"bootstrapKind": "pip-package",
"pipPackage": "frida-tools==14.10.4",
"pinnedVersion": "14.10.4",
"docsUrl": "https://frida.re/",
"canAutoInstall": true,
"verifyCommand": "frida-ps"
},
{
"name": "idalib-mcp",
"bootstrapKind": "pip-package",
"pipPackage": "ida-pro-mcp",
"pipSource": "git+https://github.com/mrexodia/ida-pro-mcp.git@f82e6e2517a161b77e738951c3071cd446480ba0",
"pinnedCommit": "f82e6e2517a161b77e738951c3071cd446480ba0",
"docsUrl": "https://github.com/mrexodia/ida-pro-mcp",
"canAutoInstall": true,
"verifyCommand": "ida-pro-mcp",
"postInstallSteps": [
"ida-pro-mcp --install (选择 Streamable HTTP + Global)",
"重启 IDA Pro 使插件生效",
"IDA 打开文件后 MCP 自动监听 127.0.0.1:13337"
]
},
{
"name": "reqable-mcp",
"bootstrapKind": "npm-mcp",
"npmPackage": "reqable-mcp-server@1.0.1",
"mcpNames": [
"reqable-mcp"
],
"mcpCommand": "npx",
"mcpArgs": [
"-y",
"reqable-mcp-server@1.0.1",
"--scope",
"minimal"
],
"mcpEnv": {},
"docsUrl": "https://github.com/reqable/reqable-mcp-server",
"canAutoInstall": true,
"verifyCommand": "npx",
"pinnedVersion": "1.0.1",
"note": "Requires the separately installed Reqable desktop application. The MCP server uses Reqable's local API; scope and authorization rules still apply to captured or replayed traffic."
},
{
"name": "jshookmcp",
"bootstrapKind": "npm-mcp",
"npmPackage": "@jshookmcp/jshook@0.3.4",
"mcpNames": [
"jshook"
],
"mcpCommand": "npx",
"mcpArgs": [
"-y",
"@jshookmcp/jshook@0.3.4"
],
"mcpEnv": {
"JSHOOK_BASE_PROFILE": "search"
},
"docsUrl": "https://github.com/vmoranv/jshookmcp",
"canAutoInstall": true,
"verifyCommand": "npx",
"pinnedVersion": "0.3.4"
},
{
"name": "anything-analyzer",
"bootstrapKind": "local-http-mcp",
"repoUrl": "https://github.com/Mouseww/anything-analyzer",
"installDir": "%USERPROFILE%\\Tools\\anything-analyzer",
"startupDirCandidates": [
"%USERPROFILE%\\Tools\\anything-analyzer"
],
"startCommand": "pnpm",
"startArgs": [
"dev"
],
"mcpNames": [
"anything-analyzer",
"local-mcp"
],
"mcpUrl": "http://localhost:23816/mcp",
"servicePort": 23816,
"docsUrl": "https://github.com/Mouseww/anything-analyzer",
"canAutoInstall": true,
"verificationMode": "service-or-registration"
},
{
"name": "idapro",
"bootstrapKind": "local-http-mcp",
"dependsOn": [
"idalib-mcp"
],
"mcpNames": [
"idapro"
],
"mcpUrl": "http://127.0.0.1:13337/mcp",
"servicePort": 13337,
"servicePortRange": [
13337,
13350
],
"portNote": "IDA 多实例时端口递增 (13337→13338→...),检查 IDA Output 窗口的 [MCP] port=xxxxx 日志",
"startScript": "%SKILL_ROOT%\\ida-reverse\\scripts\\start.ps1",
"docsUrl": "https://hex-rays.com/ida-pro/",
"canAutoInstall": true,
"verificationMode": "service-and-registration"
},
{
"name": "r2",
"bootstrapKind": "github-release-zip",
"repo": "radareorg/radare2",
"assetRegex": "^radare2-.*-w64\\.zip$",
"installDir": "%USERPROFILE%\\Tools\\radare2",
"docsUrl": "https://github.com/radareorg/radare2",
"canAutoInstall": true,
"verifyCommand": "r2",
"preferApiDigest": true,
"note": "radare2 发版频繁,不 pin assetSha256;改用 GitHub API digest(sha256:...)动态校验,格式已实测兼容 Assert-DownloadedFileIntegrity。"
},
{
"name": "rabin2",
"bootstrapKind": "github-release-zip",
"repo": "radareorg/radare2",
"assetRegex": "^radare2-.*-w64\\.zip$",
"installDir": "%USERPROFILE%\\Tools\\radare2",
"docsUrl": "https://github.com/radareorg/radare2",
"canAutoInstall": true,
"verifyCommand": "rabin2",
"preferApiDigest": true,
"note": "与 r2 同一发布资产,API digest 动态校验(见 r2.note)。"
},
{
"name": "adb",
"bootstrapKind": "winget-package",
"wingetId": "Google.PlatformTools",
"pinPolicy": "winget-latest",
"installDir": "%LOCALAPPDATA%\\Android\\Sdk\\platform-tools",
"docsUrl": "https://developer.android.com/tools/releases/platform-tools",
"canAutoInstall": true,
"verifyCommand": "adb"
},
{
"name": "agent-browser",
"bootstrapKind": "npm-global",
"npmPackage": "agent-browser@0.31.1",
"pinnedVersion": "0.31.1",
"postInstall": "npx playwright install chromium",
"installDir": "%LOCALAPPDATA%\\ms-playwright",
"docsUrl": "https://github.com/nicepkg/agent-browser",
"canAutoInstall": true,
"verifyCommand": "agent-browser",
"setupScript": "%SKILL_ROOT%\\browser-automation\\scripts\\setup.ps1"
},
{
"name": "ghidra-mcp",
"bootstrapKind": "github-release-zip",
"repo": "NationalSecurityAgency/ghidra",
"assetRegex": "^ghidra_.*_PUBLIC_.*\\.zip$",
"installDir": "%USERPROFILE%\\Tools\\ghidra",
"docsUrl": "https://ghidra-sre.org/",
"canAutoInstall": true,
"verifyCommand": "analyzeHeadless",
"postInstallSteps": [
"下载 GhidraMCP 插件: https://github.com/LaurieWired/GhidraMCP/releases",
"将插件 zip 安装到 Ghidra: File → Install Extensions → 选择下载的 zip",
"MCP server 在 Ghidra 项目打开后自动监听 localhost:8765"
],
"mcpNames": [
"ghidra"
],
"mcpUrl": "http://localhost:8765/mcp",
"servicePort": 8765,
"verificationMode": "service-or-registration",
"preferApiDigest": true,
"note": "Ghidra 发版频繁,不 pin assetSha256;改用 GitHub API digest(sha256:...)动态校验,格式已实测兼容 Assert-DownloadedFileIntegrity。"
},
{
"name": "seclists",
"bootstrapKind": "git-clone",
"repo": "https://github.com/danielmiessler/SecLists.git",
"pinnedCommit": "e5e49caa6fb648476f3bca391b26a45a4f5d3f13",
"installDir": "%USERPROFILE%\\Tools\\SecLists",
"docsUrl": "https://github.com/danielmiessler/SecLists",
"canAutoInstall": true
},
{
"name": "proxycat",
"bootstrapKind": "git-clone",
"repo": "https://github.com/honmashironeko/ProxyCat.git",
"pinnedCommit": "2309b713e2e4f574df14c2ace7e8fa6c00eb6941",
"installDir": "%USERPROFILE%\\Tools\\ProxyCat",
"docsUrl": "https://github.com/honmashironeko/ProxyCat",
"canAutoInstall": true,
"verifyCommand": "proxycat",
"postInstallSteps": [
"git clone https://github.com/honmashironeko/ProxyCat.git %USERPROFILE%\\Tools\\ProxyCat",
"pip install -r %USERPROFILE%\\Tools\\ProxyCat\\requirements.txt",
"使用 %USERPROFILE%\\Tools\\bin\\proxycat.bat 启动"
]
},
{
"name": "burpsuite-mcp",
"bootstrapKind": "local-http-mcp",
"mcpNames": [
"burpsuite"
],
"mcpUrl": "http://localhost:9876/mcp",
"servicePort": 9876,
"docsUrl": "https://portswigger.net/burp",
"canAutoInstall": false,
"verificationMode": "service-or-registration",
"manualInstallHint": "在 BurpSuite 扩展市场搜索 MCP 插件安装,启动后监听 localhost:9876"
},
{
"name": "nmap",
"bootstrapKind": "winget-package",
"wingetId": "Insecure.Nmap",
"pinPolicy": "winget-latest",
"docsUrl": "https://nmap.org/",
"canAutoInstall": true,
"verifyCommand": "nmap"
},
{
"name": "pentestswarm",
"bootstrapKind": "go-install",
"goPackage": "github.com/Armur-Ai/Pentest-Swarm-AI/cmd/pentestswarm@v0.1.0",
"fallbackKind": "docker-image",
"dockerImage": "ghcr.io/armur-ai/pentestswarm:v0.1.0",
"mcpNames": [
"pentestswarm"
],
"mcpCommand": "pentestswarm",
"mcpArgs": [
"mcp",
"serve"
],
"docsUrl": "https://github.com/Armur-Ai/Pentest-Swarm-AI",
"canAutoInstall": true,
"verifyCommand": "pentestswarm",
"note": "群体智能渗透框架。需要 Go 1.24+ 或 Docker。支持 MCP server 模式。需要 Claude/Ollama API key。",
"pinnedVersion": "v0.1.0"
},
{
"name": "binwalk",
"bootstrapKind": "winget-package",
"wingetId": "ReFirmLabs.binwalk",
"pinPolicy": "winget-latest",
"docsUrl": "https://github.com/ReFirmLabs/binwalk",
"canAutoInstall": true,
"verifyCommand": "binwalk"
},
{
"name": "yara",
"bootstrapKind": "winget-package",
"wingetId": "VirusTotal.YARA",
"pinPolicy": "winget-latest",
"docsUrl": "https://github.com/VirusTotal/yara",
"canAutoInstall": true,
"verifyCommand": "yara"
},
{
"name": "pwntools",
"bootstrapKind": "pip-package",
"pipPackage": "pwntools==4.15.0",
"pinnedVersion": "4.15.0",
"docsUrl": "https://github.com/Gallopsled/pwntools",
"canAutoInstall": true,
"verifyCommand": "pwntools"
},
{
"name": "bkcrack",
"bootstrapKind": "github-release-zip",
"repo": "kimci86/bkcrack",
"assetRegex": "^bkcrack-1\\.8\\.1-win64\\.zip$",
"installDir": "%USERPROFILE%\\Tools\\bkcrack",
"docsUrl": "https://github.com/kimci86/bkcrack",
"canAutoInstall": true,
"verifyCommand": "bkcrack",
"releaseTag": "v1.8.1",
"preferApiDigest": true
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,808 @@
#!/usr/bin/env bash
# bootstrap-reverse.sh — generic Linux/macOS bootstrapper
#
# Parity target: skills/scripts/bootstrap-reverse.ps1
# Supports the same capability names and the same high-level modes:
# - dependency expansion
# - package / release / pipx / npm installation
# - MCP registration hints / config writing
# - optional service start with --start-services
# - refresh tool index unless --skip-refresh
#
# Usage:
# bash skills/scripts/bootstrap-reverse.sh <capability1> [capability2] ... [--start-services] [--skip-refresh]
# bash skills/scripts/bootstrap-reverse.sh --list
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$SKILL_ROOT/.." && pwd)"
TOOLS_ROOT="${REVERSE_SKILL_TOOLS_DIR:-$HOME/tools}"
if [[ "$TOOLS_ROOT" != /* ]]; then
TOOLS_ROOT="$PWD/$TOOLS_ROOT"
fi
if [[ -z "$TOOLS_ROOT" || "$TOOLS_ROOT" == "/" || "$TOOLS_ROOT" == "$HOME" ]]; then
echo "Unsafe REVERSE_SKILL_TOOLS_DIR: $TOOLS_ROOT" >&2
exit 2
fi
MCP_CONFIG_PATH="${CLAUDE_MCP_CONFIG:-$HOME/.claude/mcp.json}"
UNAME_S="$(uname -s 2>/dev/null || echo unknown)"
case "$UNAME_S" in
Darwin) PLATFORM="macos" ;;
Linux) PLATFORM="linux" ;;
*) PLATFORM="unknown" ;;
esac
START_SERVICES=false
SKIP_REFRESH=false
LIST_ONLY=false
MANUAL_REQUIRED=false
LAST_CAPABILITY_MANUAL=false
CAPABILITIES=()
for arg in "$@"; do
case "$arg" in
--start-services) START_SERVICES=true ;;
--skip-refresh) SKIP_REFRESH=true ;;
--list|-l) LIST_ONLY=true ;;
--help|-h) CAPABILITIES+=("__help__") ;;
-*) echo "Unknown option: $arg" >&2; exit 2 ;;
*) CAPABILITIES+=("$arg") ;;
esac
done
log_info() { printf '\033[36m[INFO]\033[0m %s\n' "$*"; }
log_ok() { printf '\033[32m[OK]\033[0m %s\n' "$*"; }
log_warn() { printf '\033[33m[WARN]\033[0m %s\n' "$*"; }
log_err() { printf '\033[31m[ERR]\033[0m %s\n' "$*"; }
has_cmd() { command -v "$1" >/dev/null 2>&1; }
cmd_path() { command -v "$1" 2>/dev/null || true; }
json_escape() {
python3 -c 'import json,sys; print(json.dumps(sys.stdin.read()))'
}
ensure_dir() { mkdir -p "$1"; }
safe_remove_install_dir() {
local target="$1"
local tmp_target="${2:-}"
if [[ -z "$target" || "$target" == "/" || "$target" == "$HOME" || "$target" == "$TOOLS_ROOT" ]]; then
log_err "Refusing to remove unsafe install path: $target"
return 1
fi
case "$target" in
"$TOOLS_ROOT"/*) ;;
*) log_err "Refusing to remove path outside tools root: $target"; return 1 ;;
esac
rm -rf "$target"
if [[ -n "$tmp_target" ]]; then
case "$tmp_target" in
/tmp/reverse-bootstrap-*|"$TOOLS_ROOT"/*.tmp) rm -rf "$tmp_target" ;;
*) log_err "Refusing to remove unsafe tmp path: $tmp_target"; return 1 ;;
esac
fi
}
make_temp_file() {
local suffix="${1:-download}"
local tmp_dir
tmp_dir="$(mktemp -d /tmp/reverse-bootstrap-XXXXXX)"
printf '%s/%s
' "$tmp_dir" "$suffix"
}
sudo_cmd() {
if [[ ${EUID:-$(id -u)} -eq 0 ]]; then
"$@"
elif has_cmd sudo; then
sudo "$@"
else
log_err "sudo is required for: $*"
return 1
fi
}
is_kali() {
[[ -f /etc/os-release ]] && grep -qi '^ID=.*kali' /etc/os-release
}
platform_doc() {
case "$PLATFORM" in
macos) echo "docs/platforms/macos.md" ;;
linux)
if is_kali; then echo "kali/README-kali.md"; else echo "docs/platforms/linux.md"; fi
;;
*) echo "PLATFORMS.md" ;;
esac
}
print_usage() {
cat <<'EOF'
Usage:
bash skills/scripts/bootstrap-reverse.sh <capability1> [capability2] ... [--start-services] [--skip-refresh]
bash skills/scripts/bootstrap-reverse.sh --list
Capabilities (parity with bootstrap-reverse.ps1):
jadx apktool frida frida-ps idalib-mcp jshookmcp reqable-mcp anything-analyzer idapro
r2 rabin2 adb agent-browser ghidra-mcp seclists proxycat burpsuite-mcp
nmap pentestswarm binwalk yara pwntools
Examples:
bash skills/scripts/bootstrap-reverse.sh jadx apktool frida
bash skills/scripts/bootstrap-reverse.sh jshookmcp reqable-mcp
bash skills/scripts/bootstrap-reverse.sh idapro --start-services
bash skills/scripts/bootstrap-reverse.sh burpsuite-mcp
Notes:
- This script supports Linux and macOS.
- It writes MCP config to ~/.claude/mcp.json by default.
- Override with CLAUDE_MCP_CONFIG=/path/to/mcp.json.
- Override install root with REVERSE_SKILL_TOOLS_DIR=~/tools.
EOF
}
ALL_CAPABILITIES=(
jadx apktool jeb-pro frida frida-ps idalib-mcp jshookmcp reqable-mcp anything-analyzer idapro
r2 rabin2 adb agent-browser ghidra-mcp seclists proxycat burpsuite-mcp
nmap pentestswarm binwalk yara pwntools
)
if $LIST_ONLY; then
printf '%s\n' "${ALL_CAPABILITIES[@]}"
exit 0
fi
if [[ ${#CAPABILITIES[@]} -eq 0 || "${CAPABILITIES[0]}" == "__help__" ]]; then
print_usage
exit 0
fi
install_apt() {
local package="$1"
log_info "apt install $package"
sudo_cmd apt-get update -qq
sudo_cmd apt-get install -y "$package"
}
install_brew() {
local package="$1"
if ! has_cmd brew; then
log_err "Homebrew is required. Install it first: https://brew.sh/"
return 1
fi
log_info "brew install $package"
brew install "$package"
}
install_brew_cask() {
local package="$1"
if ! has_cmd brew; then
log_err "Homebrew is required. Install it first: https://brew.sh/"
return 1
fi
log_info "brew install --cask $package"
brew install --cask "$package"
}
ensure_python_runtime() {
if ! has_cmd python3; then
case "$PLATFORM" in
macos) install_brew python ;;
linux) install_apt python3 ;;
*) log_err "Install Python 3 manually. See $(platform_doc)"; return 1 ;;
esac
fi
if ! has_cmd pipx; then
case "$PLATFORM" in
macos)
python3 -m pip install --user pipx || install_brew pipx
;;
linux)
install_apt pipx || python3 -m pip install --user pipx
;;
esac
fi
python3 -m pipx ensurepath >/dev/null 2>&1 || true
export PATH="$HOME/.local/bin:$PATH"
}
ensure_node_runtime() {
if has_cmd node && has_cmd npm && has_cmd npx; then return 0; fi
case "$PLATFORM" in
macos) install_brew node ;;
linux) install_apt nodejs; install_apt npm ;;
*) log_err "Install Node.js manually. See $(platform_doc)"; return 1 ;;
esac
}
ensure_java_runtime() {
if has_cmd java; then return 0; fi
case "$PLATFORM" in
macos) install_brew openjdk ;;
linux) install_apt openjdk-17-jdk ;;
*) log_err "Install Java manually. See $(platform_doc)"; return 1 ;;
esac
}
ensure_pnpm() {
ensure_node_runtime
if has_cmd pnpm; then return 0; fi
if has_cmd corepack; then corepack enable || true; fi
if ! has_cmd pnpm; then npm install -g pnpm; fi
}
# Args: repo regex [release_tag]
# Prints: url\tdigest_or_empty
latest_github_asset_meta() {
local repo="$1"
local regex="$2"
local tag="${3:-}"
python3 - "$repo" "$regex" "$tag" <<'PY'
import json, re, sys, urllib.request
repo, pattern, tag = sys.argv[1:4]
if tag:
url = f'https://api.github.com/repos/{repo}/releases/tags/{tag}'
else:
url = f'https://api.github.com/repos/{repo}/releases/latest'
req = urllib.request.Request(url, headers={'User-Agent':'reverse-skill-bootstrap'})
with urllib.request.urlopen(req, timeout=30) as r:
data = json.load(r)
for asset in data.get('assets', []):
if re.search(pattern, asset.get('name','')):
digest = asset.get('digest') or ''
print(f"{asset.get('browser_download_url')}\t{digest}")
raise SystemExit(0)
raise SystemExit(f'no asset matched {pattern} for {repo} tag={tag!r}')
PY
}
latest_github_asset_url() {
local repo="$1"
local regex="$2"
local tag="${3:-}"
latest_github_asset_meta "$repo" "$regex" "$tag" | cut -f1
}
# verify_sha256 file expected_or_empty [github_digest]
# expected may be "hex" or "sha256:hex"; github_digest same. Prefer expected.
verify_sha256() {
local file="$1"
local expected="$2"
local api_digest="${3:-}"
local actual
if command -v sha256sum >/dev/null 2>&1; then
actual=$(sha256sum "$file" | awk '{print tolower($1)}')
elif command -v shasum >/dev/null 2>&1; then
actual=$(shasum -a 256 "$file" | awk '{print tolower($1)}')
else
log_warn "sha256 tool missing; skip integrity for $file"
return 0
fi
local exp="$expected"
if [[ -z "$exp" && -n "$api_digest" ]]; then
exp="$api_digest"
fi
exp=$(printf '%s' "$exp" | sed -E 's/^[Ss][Hh][Aa]256://')
exp=$(printf '%s' "$exp" | tr 'A-F' 'a-f')
if [[ -n "$exp" ]]; then
if [[ "$actual" != "$exp" ]]; then
rm -f "$file"
log_err "SHA256 mismatch for $(basename "$file"): expected $exp got $actual (file deleted)"
return 1
fi
log_ok "SHA256 OK: $actual"
else
log_warn "No pinned digest for $(basename "$file"); recorded sha256=$actual"
fi
}
extract_archive() {
local archive="$1"
local dest="$2"
safe_remove_install_dir "$dest" "$dest.tmp"
mkdir -p "$dest"
case "$archive" in
*.zip)
unzip -q "$archive" -d "$dest.tmp"
;;
*.tar.gz|*.tgz)
mkdir -p "$dest.tmp"
tar -xzf "$archive" -C "$dest.tmp"
;;
*)
mkdir -p "$dest"
cp "$archive" "$dest/"
return 0
;;
esac
local top_count
top_count=$(find "$dest.tmp" -mindepth 1 -maxdepth 1 | wc -l | tr -d ' ')
if [[ "$top_count" == "1" ]] && [[ -d "$(find "$dest.tmp" -mindepth 1 -maxdepth 1 | head -n1)" ]]; then
cp -a "$(find "$dest.tmp" -mindepth 1 -maxdepth 1 | head -n1)"/. "$dest/"
else
cp -a "$dest.tmp"/. "$dest/"
fi
case "$dest.tmp" in /tmp/reverse-bootstrap-*|"$TOOLS_ROOT"/*.tmp) rm -rf "$dest.tmp" ;; esac
}
# install_github_release repo regex dest [release_tag] [expected_sha256]
install_github_release() {
local repo="$1"
local regex="$2"
local dest="$3"
local tag="${4:-}"
local expected_sha="${5:-}"
local meta url digest file
ensure_dir "$TOOLS_ROOT"
meta=$(latest_github_asset_meta "$repo" "$regex" "$tag")
url=$(printf '%s' "$meta" | cut -f1)
digest=$(printf '%s' "$meta" | cut -f2)
file="$(make_temp_file "$(basename "$url")")"
log_info "download $url"
curl -L -o "$file" "$url"
verify_sha256 "$file" "$expected_sha" "$digest" || return 1
extract_archive "$file" "$dest"
rm -rf "$(dirname "$file")"
export PATH="$dest/bin:$dest:$PATH"
log_ok "installed $repo to $dest"
}
write_mcp_server() {
local name="$1"
local json_payload="$2"
ensure_dir "$(dirname "$MCP_CONFIG_PATH")"
python3 - "$MCP_CONFIG_PATH" "$name" "$json_payload" <<'PY'
import json, pathlib, sys
path = pathlib.Path(sys.argv[1])
name = sys.argv[2]
payload = json.loads(sys.argv[3])
if path.exists():
try:
data = json.loads(path.read_text(encoding='utf-8'))
except Exception:
data = {}
else:
data = {}
data.setdefault('mcpServers', {})[name] = payload
path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding='utf-8')
print(path)
PY
log_ok "MCP server '$name' registered in $MCP_CONFIG_PATH"
}
test_tcp_port() {
local port="$1"
python3 - "$port" <<'PY' >/dev/null 2>&1
import socket, sys
port=int(sys.argv[1])
s=socket.socket()
s.settimeout(1)
s.connect(('127.0.0.1', port))
PY
}
test_mcp_http() {
local port="$1"
local timeout_seconds="${2:-3}"
python3 - "$port" "$timeout_seconds" <<'PY' >/dev/null 2>&1
import sys, json, urllib.request
port = int(sys.argv[1])
timeout = int(sys.argv[2])
body = json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{port}/mcp",
data=body,
headers={"Content-Type": "application/json"},
method="POST"
)
try:
with urllib.request.urlopen(req, timeout=timeout) as resp:
exit(0 if resp.status == 200 else 1)
except Exception:
exit(1)
PY
}
wait_for_port() {
local port="$1"
local timeout_seconds="${2:-90}"
local elapsed=0
while (( elapsed < timeout_seconds )); do
if test_tcp_port "$port"; then return 0; fi
sleep 2
elapsed=$((elapsed+2))
done
return 1
}
manual_required() {
local name="$1"
local hint="$2"
log_warn "MANUAL_INSTALL_REQUIRED: $name — $hint"
}
is_ready_cmd() {
local cmd="$1"
has_cmd "$cmd"
}
ensure_jeb_pro() {
if has_cmd jeb_wincon || has_cmd jeb; then
log_ok "JEB Pro ready: $(cmd_path jeb_wincon)$(cmd_path jeb)"
return 0
fi
manual_required jeb-pro "JEB Pro is commercial. Install it with a valid PNF Software license, then refresh the tool index."
LAST_CAPABILITY_MANUAL=true
MANUAL_REQUIRED=true
return 0
}
ensure_jadx() {
if has_cmd jadx; then log_ok "jadx ready: $(cmd_path jadx)"; return 0; fi
ensure_java_runtime
local tag="v1.5.6"
local sha="545ea2be9c242511bc145755cf4bda2485ade42966e096f8b4d3da2a230e8974"
local re='^jadx-1\.5\.6\.zip$'
case "$PLATFORM" in
macos) install_brew jadx || install_github_release skylot/jadx "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;;
linux) install_github_release skylot/jadx "$re" "$TOOLS_ROOT/jadx" "$tag" "$sha" ;;
esac
}
ensure_apktool() {
if has_cmd apktool; then log_ok "apktool ready: $(cmd_path apktool)"; return 0; fi
ensure_java_runtime
case "$PLATFORM" in
macos) install_brew apktool ;;
linux)
if install_apt apktool; then return 0; fi
ensure_dir "$TOOLS_ROOT/apktool"
local meta url digest jar wrapper
local tag="v3.0.2"
local sha="eee4669a704a14e0623407e6701b0b91887e61e1e4049cb7a82833e14ae8b5fd"
local re='^apktool_3\.0\.2\.jar$'
meta=$(latest_github_asset_meta iBotPeaches/Apktool "$re" "$tag")
url=$(printf '%s' "$meta" | cut -f1)
digest=$(printf '%s' "$meta" | cut -f2)
jar="$TOOLS_ROOT/apktool/apktool.jar"
curl -L -o "$jar" "$url"
verify_sha256 "$jar" "$sha" "$digest" || return 1
wrapper="$TOOLS_ROOT/apktool/apktool"
printf '#!/usr/bin/env bash\njava -jar "%s" "$@"\n' "$jar" > "$wrapper"
chmod +x "$wrapper"
export PATH="$TOOLS_ROOT/apktool:$PATH"
;;
esac
}
ensure_frida_tools() {
ensure_python_runtime
if has_cmd frida && has_cmd frida-ps; then log_ok "frida-tools ready"; return 0; fi
pipx install frida-tools || pipx upgrade frida-tools
export PATH="$HOME/.local/bin:$PATH"
}
ensure_idalib_mcp() {
ensure_python_runtime
if has_cmd ida-pro-mcp; then log_ok "ida-pro-mcp ready: $(cmd_path ida-pro-mcp)"; return 0; fi
pipx install 'git+https://github.com/mrexodia/ida-pro-mcp.git' || pipx upgrade ida-pro-mcp
export PATH="$HOME/.local/bin:$PATH"
log_warn "Post-install: run 'ida-pro-mcp --install', choose Streamable HTTP + Global, then restart IDA Pro."
}
ensure_jshookmcp() {
ensure_node_runtime
write_mcp_server "jshook" '{"command":"npx","args":["-y","@jshookmcp/jshook@0.3.4"],"env":{"JSHOOK_BASE_PROFILE":"search"}}'
}
ensure_reqable_mcp() {
ensure_node_runtime
write_mcp_server "reqable-mcp" '{"command":"npx","args":["-y","reqable-mcp-server@1.0.1"]}'
log_warn "Reqable MCP requires the separately installed Reqable desktop application and its local API."
}
ensure_anything_analyzer() {
ensure_node_runtime
ensure_pnpm
local dir="$TOOLS_root:REDACTED"
if [[ ! -d "$dir/.git" ]]; then
if ! has_cmd git; then
case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac
fi
rm -rf "$dir"
git clone https://github.com/Mouseww/anything-analyzer "$dir"
fi
write_mcp_server "anything-analyzer" '{"url":"http://localhost:23816/mcp"}'
if $START_SERVICES; then
(cd "$dir" && pnpm install && nohup pnpm dev >/tmp/anything-analyzer.log 2>&1 &)
if wait_for_port 23816 120; then
if test_mcp_http 23816; then
log_ok "anything-analyzer MCP server ready on port 23816 (HTTP verified)"
else
log_warn "anything-analyzer port 23816 open but MCP HTTP handshake failed; check /tmp/anything-analyzer.log"
fi
else
log_warn "anything-analyzer did not open port 23816; see /tmp/anything-analyzer.log"
fi
fi
}
ensure_idapro() {
ensure_idalib_mcp
write_mcp_server "idapro" '{"url":"http://127.0.0.1:13337/mcp"}'
if $START_SERVICES; then
case "$PLATFORM" in
linux)
log_warn "Linux package does not include an IDA GUI launcher. Start IDA manually and ensure MCP listens on 127.0.0.1:13337."
;;
macos)
log_warn "Start IDA Pro manually on macOS and confirm MCP port in the IDA Output window."
;;
esac
wait_for_port 13337 45 || log_warn "idapro MCP service is not online on port 13337 yet."
if test_mcp_http 13337 3; then
log_ok "idapro MCP server ready (HTTP verified)"
else
log_warn "idapro port 13337 open but MCP HTTP handshake failed; check IDA Output window"
fi
fi
}
ensure_r2() {
if has_cmd r2; then log_ok "r2 ready: $(cmd_path r2)"; return 0; fi
case "$PLATFORM" in
macos) install_brew radare2 ;;
linux)
if install_apt radare2; then return 0; fi
manual_required r2 "Install radare2 from GitHub/source: https://github.com/radareorg/radare2"
;;
esac
}
ensure_adb() {
if has_cmd adb; then log_ok "adb ready: $(cmd_path adb)"; return 0; fi
case "$PLATFORM" in
macos) install_brew android-platform-tools ;;
linux) install_apt adb || manual_required adb "Install Android platform-tools from https://developer.android.com/tools/releases/platform-tools" ;;
esac
}
ensure_agent_browser() {
ensure_node_runtime
if has_cmd agent-browser; then log_ok "agent-browser ready"; return 0; fi
npm install -g agent-browser
if has_cmd npx; then npx playwright install chromium || true; fi
local setup="$SKILL_root:REDACTED/scripts/setup.sh"
if [[ -x "$setup" ]]; then "$setup" --skip-browser-install || true; fi
}
ensure_ghidra_mcp() {
ensure_java_runtime
case "$PLATFORM" in
macos)
if ! has_cmd ghidraRun && [[ ! -d /Applications/Ghidra.app ]]; then
install_brew ghidra || brew install --cask ghidra || true
fi
;;
linux)
if ! has_cmd ghidraRun; then
install_github_release NationalSecurityAgency/ghidra '^ghidra_.*_PUBLIC_.*\.zip$' "$TOOLS_ROOT/ghidra" || \
manual_required ghidra-mcp "Install Ghidra from GitHub release or Flatpak, then configure ghidra-mcp if used."
fi
;;
esac
log_warn "ghidra-mcp requires local Ghidra MCP plugin/server setup. See docs/platforms/$( [[ "$PLATFORM" == macos ]] && echo macos || echo linux ).md"
}
ensure_seclists() {
local dir="$TOOLS_root:REDACTED"
if [[ -d "$dir/.git" || -d /usr/share/seclists ]]; then log_ok "SecLists ready"; return 0; fi
if ! has_cmd git; then case "$PLATFORM" in macos) install_brew git ;; linux) install_apt git ;; esac; fi
git clone https://github.com/danielmiessler/SecLists "$dir"
}
ensure_proxycat() {
ensure_python_runtime
if has_cmd proxycat; then log_ok "proxycat ready"; return 0; fi
pipx install git+https://github.com/honmashironeko/ProxyCat.git || manual_required proxycat "Clone/install ProxyCat manually; verify command 'proxycat'."
}
ensure_burpsuite_mcp() {
local bridge_json
bridge_json=$(python3 - "$REPO_root:REDACTED/mcp-bridge.js" <<'PY'
import json, sys
print(json.dumps({"command":"node","args":[sys.argv[1]]}))
PY
)
write_mcp_server "burpsuite" "$bridge_json"
manual_required burpsuite-mcp "Build burp-mcp-full and load build/libs/burp-mcp-full.jar in BurpSuite Extensions."
}
ensure_nmap() {
if has_cmd nmap; then log_ok "nmap ready"; return 0; fi
case "$PLATFORM" in macos) install_brew nmap ;; linux) install_apt nmap ;; esac
}
register_pentestswarm_mcp() {
local executable="$1"
local definition
definition=$(python3 - "$executable" <<'PY'
import json, sys
print(json.dumps({"command": sys.argv[1], "args": ["mcp", "serve"]}))
PY
)
write_mcp_server "pentestswarm" "$definition"
}
ensure_pentestswarm() {
local pentestswarm_path
pentestswarm_path="$(cmd_path pentestswarm)"
if [[ -n "$pentestswarm_path" ]]; then
register_pentestswarm_mcp "$pentestswarm_path"
log_ok "pentestswarm ready"
return 0
fi
if ! has_cmd go; then
case "$PLATFORM" in macos) install_brew go ;; linux) install_apt golang-go ;; esac
fi
if go install github.com/Armur-Ai/Pentest-Swarm-AI/cmd/pentestswarm@v0.1.0; then
local go_bin
go_bin="$(go env GOBIN 2>/dev/null || true)"
if [[ -z "$go_bin" ]]; then
go_bin="$(go env GOPATH 2>/dev/null || true)/bin"
fi
if [[ -x "$go_bin/pentestswarm" ]]; then
register_pentestswarm_mcp "$go_bin/pentestswarm"
log_ok "pentestswarm installed and registered"
return 0
fi
log_warn "pentestswarm installed but no executable was found in GOBIN/GOPATH; trying Docker fallback"
fi
if has_cmd docker; then
write_mcp_server "pentestswarm" '{"command":"docker","args":["run","--rm","-i","ghcr.io/armur-ai/pentestswarm:v0.1.0","mcp","serve"]}'
log_warn "pentestswarm Go install failed or produced no runnable binary; registered Docker fallback ghcr.io/armur-ai/pentestswarm:v0.1.0"
else
manual_required pentestswarm "Install Go 1.24+ or Docker, then install Pentest-Swarm-AI and ensure pentestswarm is on PATH."
fi
}
ensure_binwalk() {
if has_cmd binwalk; then log_ok "binwalk ready: $(cmd_path binwalk)"; return 0; fi
case "$PLATFORM" in
macos) install_brew binwalk ;;
linux) install_apt binwalk || manual_required binwalk "git clone https://github.com/ReFirmLabs/binwalk.git and install manually" ;;
esac
}
ensure_yara() {
if has_cmd yara; then log_ok "yara ready: $(cmd_path yara)"; return 0; fi
case "$PLATFORM" in
macos) install_brew yara ;;
linux) install_apt yara || manual_required yara "Install from source: https://github.com/VirusTotal/yara" ;;
esac
}
ensure_pwntools() {
ensure_python_runtime
if python3 -c "import pwn" 2>/dev/null; then log_ok "pwntools ready"; return 0; fi
pipx install pwntools || python3 -m pip install --user pwntools
}
status_json_line() {
local name="$1"
local status="$2"
local extra="${3:-}"
if [[ -n "$extra" ]]; then
printf '{"name":"%s","status":"%s","note":"%s"}\n' "$name" "$status" "$extra"
else
printf '{"name":"%s","status":"%s"}\n' "$name" "$status"
fi
}
cap_depends() {
case "$1" in
idapro) echo "idalib-mcp idapro" ;;
frida-ps) echo "frida frida-ps" ;;
rabin2) echo "r2 rabin2" ;;
*) echo "$1" ;;
esac
}
expand_capabilities() {
local seen=" "
local out=()
local cap dep
for cap in "$@"; do
for dep in $(cap_depends "$cap"); do
if [[ "$seen" != *" $dep "* ]]; then
out+=("$dep")
seen+="$dep "
fi
done
done
printf '%s\n' "${out[@]}"
}
ensure_capability() {
local name="$1"
case "$name" in
jeb-pro) ensure_jeb_pro ;;
jadx) ensure_jadx ;;
apktool) ensure_apktool ;;
frida|frida-ps) ensure_frida_tools ;;
idalib-mcp) ensure_idalib_mcp ;;
jshookmcp) ensure_jshookmcp ;;
reqable-mcp) ensure_reqable_mcp ;;
anything-analyzer) ensure_anything_analyzer ;;
idapro) ensure_idapro ;;
r2|rabin2) ensure_r2 ;;
adb) ensure_adb ;;
agent-browser) ensure_agent_browser ;;
ghidra-mcp) ensure_ghidra_mcp ;;
seclists) ensure_seclists ;;
proxycat) ensure_proxycat ;;
burpsuite-mcp) ensure_burpsuite_mcp ;;
nmap) ensure_nmap ;;
pentestswarm) ensure_pentestswarm ;;
binwalk) ensure_binwalk ;;
yara) ensure_yara ;;
pwntools) ensure_pwntools ;;
*) log_err "No bootstrap definition for capability: $name"; return 1 ;;
esac
}
RESULTS_FILE="$(mktemp)"
trap 'rm -f "$RESULTS_FILE"' EXIT
# macOS ships Bash 3.2, which has no mapfile/readarray. Keep this path portable
# instead of requiring users to install a newer Bash just to run the bootstrapper.
EXPANDED=()
while IFS= read -r capability; do
[[ -n "$capability" ]] || continue
EXPANDED+=("$capability")
done < <(expand_capabilities "${CAPABILITIES[@]}")
log_info "platform=$PLATFORM doc=$(platform_doc) tools_root=$TOOLS_ROOT"
for cap in "${EXPANDED[@]}"; do
log_info "ensure $cap"
LAST_CAPABILITY_MANUAL=false
if ensure_capability "$cap"; then
if $LAST_CAPABILITY_MANUAL; then
status_json_line "$cap" "manual-required" "see $(platform_doc)" >> "$RESULTS_FILE"
else
status_json_line "$cap" "ready" >> "$RESULTS_FILE"
fi
else
status_json_line "$cap" "failed" "see $(platform_doc)" >> "$RESULTS_FILE"
fi
done
if ! $SKIP_REFRESH; then
bash "$SCRIPT_DIR/refresh-tool-index.sh" >/dev/null || log_warn "refresh-tool-index.sh failed"
fi
FINAL_EXIT_CODE=0
if $MANUAL_REQUIRED; then
FINAL_EXIT_CODE=2
fi
if has_cmd python3; then
python3 - "$RESULTS_FILE" <<'PY'
import json, sys
items=[]
with open(sys.argv[1], encoding='utf-8') as f:
for line in f:
if line.strip(): items.append(json.loads(line))
print(json.dumps(items, ensure_ascii=False, indent=2))
PY
else
log_warn "Python 3 is unavailable; emitting bootstrap results without JSON formatting"
cat "$RESULTS_FILE"
fi
exit "$FINAL_EXIT_CODE"

View File

@@ -0,0 +1,102 @@
#Requires -Version 5.1
# Lightweight scope gate before ACT. Exit 0 = ok, 2 = not ready, 1 = usage/error.
# Usage:
# powershell -File skills/scripts/case-guard.ps1 -CaseRoot work\my-case
# powershell -File skills/scripts/case-guard.ps1 -CaseRoot work\my-case -Force # warn but exit 0
param(
[Parameter(Mandatory = $true)]
[string] $CaseRoot,
[switch] $Force,
[switch] $Quiet
)
$ErrorActionPreference = 'Stop'
function Write-Info([string] $m) {
if (-not $Quiet) { Write-Host $m }
}
if (-not (Test-Path -LiteralPath $CaseRoot)) {
Write-Host ("ERROR: CaseRoot missing: {0}" -f $CaseRoot) -ForegroundColor Red
exit 1
}
$scopePath = Join-Path $CaseRoot 'scope.md'
if (-not (Test-Path -LiteralPath $scopePath)) {
Write-Host ("ERROR: scope.md missing under {0}" -f $CaseRoot) -ForegroundColor Red
exit 1
}
$scope = Get-Content -LiteralPath $scopePath -Raw -Encoding UTF8
$issues = New-Object System.Collections.Generic.List[string]
function Get-ScopeSection([string] $Text, [string] $Name) {
$pattern = '(?ms)^##\s*' + [regex]::Escape($Name) + '\s*\r?\n(?<body>.*?)(?=^##\s|\z)'
$match = [regex]::Match($Text, $pattern)
if ($match.Success) { return $match.Groups['body'].Value }
return ''
}
function Get-SectionField([string] $Section, [string] $Name) {
$pattern = '(?m)^\s*-\s*' + [regex]::Escape($Name) + ':\s*(?<value>.*?)\s*$'
$match = [regex]::Match($Section, $pattern)
if ($match.Success) { return $match.Groups['value'].Value.Trim() }
return ''
}
$authSection = Get-ScopeSection -Text $scope -Name 'auth'
$networkSection = Get-ScopeSection -Text $scope -Name 'network_profile'
$signoffSection = Get-ScopeSection -Text $scope -Name 'signoff'
# auth.status
$authGranted = (Get-SectionField -Section $authSection -Name 'status') -eq 'granted'
if (-not $authGranted) { [void]$issues.Add('auth.status is not granted') }
# network_profile.mode
$netMode = Get-SectionField -Section $networkSection -Name 'mode'
$allowedNetworkModes = @('offline', 'lab_only', 'authorized_target_only', 'unrestricted_lab')
if ([string]::IsNullOrWhiteSpace($netMode)) {
[void]$issues.Add('network_profile.mode missing')
} elseif ($netMode -notin $allowedNetworkModes) {
[void]$issues.Add("network_profile.mode is unsupported: $netMode")
} elseif ($netMode -eq 'offline') {
# offline is only OK if sample path mentioned in assets/notes — soft note
if ($scope -notmatch 'sample|offline.?path|本地.?样本|\.apk\b|\.bin\b|\.exe\b') {
[void]$issues.Add('network_profile.mode is offline without offline sample cue')
}
}
# in_scope assets: only list items under "- assets:" inside ## in_scope
# Do NOT treat "- assets:" itself, ops_refs, or evidence_of_auth URLs as assets.
$hasAsset = $false
$inScopeSection = Get-ScopeSection -Text $scope -Name 'in_scope'
if ($inScopeSection -and $inScopeSection -match '(?ms)-\s*assets:\s*\r?\n(?<body>(?:\s+.+\r?\n?|\s+\r?\n?)*)') {
$assetBody = $Matches['body']
# Require indented list entries: " - value" where value is not empty [] marker
if ($assetBody -match '(?m)^\s+-\s+(?!\[\s*\])\S+') {
$hasAsset = $true
}
}
if (-not $hasAsset -and $netMode -ne 'offline') {
[void]$issues.Add('in_scope.assets appears empty')
}
# ready_for_act
$ready = (Get-SectionField -Section $signoffSection -Name 'ready_for_act') -eq 'true'
if (-not $ready) { [void]$issues.Add('ready_for_act is not true') }
if ($issues.Count -eq 0) {
Write-Info ("CASE-GUARD OK: {0}" -f $CaseRoot)
exit 0
}
Write-Host ("CASE-GUARD NOT READY: {0}" -f $CaseRoot) -ForegroundColor Yellow
foreach ($i in $issues) { Write-Host (" - {0}" -f $i) -ForegroundColor Yellow }
if ($Force) {
Write-Host 'CASE-GUARD: -Force set; continuing with warnings only.' -ForegroundColor Yellow
exit 0
}
Write-Host 'Fix scope (or re-run case-init -AuthGranted -TargetUrl ...) or pass -Force.' -ForegroundColor Yellow
exit 2

View File

@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# Lightweight scope gate before ACT. Exit 0=ok, 2=not ready, 1=usage/error.
# Usage:
# bash skills/scripts/case-guard.sh --case-root work/my-case
# bash skills/scripts/case-guard.sh --case-root work/my-case --force
set -euo pipefail
CASE_ROOT=""
FORCE=0
QUIET=0
while [[ $# -gt 0 ]]; do
case "$1" in
-CaseRoot|--case-root) CASE_ROOT="${2:-}"; shift 2 ;;
-Force|--force) FORCE=1; shift ;;
-Quiet|--quiet) QUIET=1; shift ;;
-h|--help) sed -n '2,6p' "$0"; exit 0 ;;
*) echo "Unknown arg: $1" >&2; exit 1 ;;
esac
done
info() { [[ $QUIET -eq 1 ]] || echo "$*"; }
if [[ -z "$CASE_ROOT" ]]; then
echo "ERROR: --case-root required" >&2
exit 1
fi
if [[ ! -d "$CASE_ROOT" ]]; then
echo "ERROR: CaseRoot missing: $CASE_ROOT" >&2
exit 1
fi
SCOPE_PATH="$CASE_ROOT/scope.md"
if [[ ! -f "$SCOPE_PATH" ]]; then
echo "ERROR: scope.md missing under $CASE_ROOT" >&2
exit 1
fi
SCOPE="$(cat "$SCOPE_PATH")"
ISSUES=()
# Read fields only from their contract sections. A status-like line in notes,
# evidence, or ops_refs must never satisfy the authorization gate.
section_field() {
local section="$1"
local field="$2"
awk -v wanted_section="$section" -v wanted_field="$field" '
/^##[[:space:]]+/ {
heading=$0
sub(/^##[[:space:]]+/, "", heading)
active=(tolower(heading)==tolower(wanted_section))
next
}
active {
line=$0
pattern="^[[:space:]]*-[[:space:]]*" wanted_field ":[[:space:]]*"
if (line ~ pattern) {
sub(pattern, "", line)
gsub(/^[[:space:]]+|[[:space:]]+$/, "", line)
print line
exit
}
}
' "$SCOPE_PATH"
}
auth_granted=0
if [[ "$(section_field auth status | tr '[:upper:]' '[:lower:]')" == "granted" ]]; then
auth_granted=1
fi
[[ $auth_granted -eq 1 ]] || ISSUES+=("auth.status is not granted")
net_mode="$(section_field network_profile mode | tr -d '\r' | awk '{print $1}')"
if [[ -z "$net_mode" ]]; then
ISSUES+=("network_profile.mode missing")
else
case "$net_mode" in
offline)
if ! printf '%s' "$SCOPE" | grep -Eiq 'sample|offline.?path|本地.?样本|\.apk\b|\.bin\b|\.exe\b'; then
ISSUES+=("network_profile.mode is offline without offline sample cue")
fi
;;
lab_only|authorized_target_only|unrestricted_lab) ;;
*) ISSUES+=("network_profile.mode is unsupported: $net_mode") ;;
esac
fi
has_asset=0
# crude: look for indented list items after assets: that are not []
if printf '%s\n' "$SCOPE" | awk '
BEGIN{inscope=0; inassets=0}
/^##[[:space:]]*in_scope/ {inscope=1; inassets=0; next}
/^##[[:space:]]/ {if(inscope){inscope=0; inassets=0}}
inscope && /-[[:space:]]*assets:/ {inassets=1; next}
inscope && inassets && /^[[:space:]]+-[[:space:]]+/ {
line=$0
sub(/^[[:space:]]+-[[:space:]]+/,"",line)
if (line !~ /^\[/ && length(line)>0) { found=1 }
}
END{ exit(found?0:1) }
'; then
has_asset=1
fi
if [[ $has_asset -eq 0 && "$net_mode" != "offline" ]]; then
ISSUES+=("in_scope.assets appears empty")
fi
ready=0
if [[ "$(section_field signoff ready_for_act | tr '[:upper:]' '[:lower:]')" == "true" ]]; then
ready=1
fi
[[ $ready -eq 1 ]] || ISSUES+=("ready_for_act is not true")
if [[ ${#ISSUES[@]} -eq 0 ]]; then
info "CASE-GUARD OK: $CASE_ROOT"
exit 0
fi
echo "CASE-GUARD NOT READY: $CASE_ROOT"
for i in "${ISSUES[@]}"; do echo " - $i"; done
if [[ $FORCE -eq 1 ]]; then
echo "CASE-GUARD: --force set; continuing with warnings only."
exit 0
fi
echo "Fix scope (or re-run case-init with --auth-granted --target-url ...) or pass --force."
exit 2

View File

@@ -0,0 +1,320 @@
#Requires -Version 5.1
# Initialize work/<case>/ with scope, timeline, workitems (reverse-skill ops contract).
# Bare invocation keeps pending/offline defaults.
# Ready-to-act example:
# powershell -File skills/scripts/case-init.ps1 -Hint "web pentest" -CaseName my-case `
# -AuthGranted -TargetUrl "https://app.example/" -NetworkProfile authorized_target_only
param(
[string] $Hint = '',
[string] $CaseName = '',
[string] $PackageRoot = '',
[AllowEmptyString()]
[string] $ProjectRoot = '',
[switch] $AuthGranted,
[string] $AuthStatus = '',
[string] $AuthBasis = 'own_system',
[string] $EvidenceOfAuth = '',
[string] $TargetUrl = '',
[string[]] $InScopeAssets = @(),
[string] $NetworkProfile = '',
[switch] $ReadyForAct
)
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
. (Join-Path (Join-Path $scriptDir 'lib') 'WorkRoot.ps1')
$requestedProjectRoot = if (-not [string]::IsNullOrWhiteSpace($ProjectRoot)) {
$ProjectRoot
} elseif ($PSBoundParameters.ContainsKey('PackageRoot')) {
$PackageRoot
} else {
''
}
$projectRoot = Resolve-ReverseProjectRoot -RequestedRoot $requestedProjectRoot
if (-not $CaseName) {
$slug = if ($Hint) {
($Hint.ToLowerInvariant() -replace '[^a-z0-9]+', '-').Trim('-')
} else { 'case' }
if ($slug.Length -gt 32) { $slug = $slug.Substring(0, 32).TrimEnd('-') }
$CaseName = '{0}-{1}' -f (Get-Date -Format 'yyyyMMdd-HHmmss'), $slug
}
# CaseName is a directory name, not a path. Keep every case under projectRoot/work.
# The pattern allows localized names but excludes Windows path syntax and names that
# Windows would normalize into a dot segment (for example, '.. ' or 'case.').
if ([string]::IsNullOrWhiteSpace($CaseName) -or
$CaseName -notmatch '^[\p{L}\p{N}][\p{L}\p{N}._ -]{0,79}$' -or
$CaseName -match '[.\s]$') {
throw "Invalid -CaseName '$CaseName'. Use a 1-80 character directory name beginning with a letter or number; path separators, dot segments, trailing dots/spaces, control characters, and wildcard characters are not allowed."
}
if (-not [string]::IsNullOrWhiteSpace($NetworkProfile) -and
$NetworkProfile.Trim().ToLowerInvariant() -notin @('offline', 'lab_only', 'authorized_target_only', 'unrestricted_lab', 'lab', 'authorized', 'auth', 'offline_only')) {
throw "Invalid -NetworkProfile '$NetworkProfile'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)."
}
$workRoot = Join-Path $projectRoot 'work'
$caseRoot = Join-Path $workRoot $CaseName
$dirs = @('evidence', 'notes', 'report')
New-Item -ItemType Directory -Force -Path $caseRoot | Out-Null
foreach ($d in $dirs) {
New-Item -ItemType Directory -Force -Path (Join-Path $caseRoot $d) | Out-Null
}
# Resolve auth / network / assets
# Priority: explicit -AuthStatus (only if bound) > -AuthGranted > pending.
# Never let a stray positional string overwrite AuthGranted.
$authStatusResolved = 'pending'
if ($AuthGranted) { $authStatusResolved = 'granted' }
if ($PSBoundParameters.ContainsKey('AuthStatus') -and -not [string]::IsNullOrWhiteSpace($AuthStatus)) {
$candidate = $AuthStatus.Trim().ToLowerInvariant()
$allowedAuth = @('pending', 'granted', 'denied', 'unknown')
if ($allowedAuth -contains $candidate) {
$authStatusResolved = $candidate
} else {
Write-Host ("WARN: ignoring invalid -AuthStatus '{0}' (allowed: pending|granted|denied|unknown)" -f $AuthStatus) -ForegroundColor Yellow
# if user also passed AuthGranted, keep granted; else stay pending
}
}
$evidenceAuth = if (-not [string]::IsNullOrWhiteSpace($EvidenceOfAuth)) {
$EvidenceOfAuth
} elseif ($AuthGranted -or $authStatusResolved -eq 'granted') {
'cli-flag AuthGranted or AuthStatus=granted'
} else {
'FILL_ME'
}
$assets = New-Object System.Collections.Generic.List[string]
if (-not [string]::IsNullOrWhiteSpace($TargetUrl)) { [void]$assets.Add($TargetUrl.Trim()) }
foreach ($a in @($InScopeAssets)) {
if (-not [string]::IsNullOrWhiteSpace($a) -and -not $assets.Contains($a.Trim())) {
[void]$assets.Add($a.Trim())
}
}
# Also accept host-only tokens that look like domains/IPs from TargetUrl leftovers
if ($assets.Count -eq 0 -and $Hint -match 'https?://([^\s/]+)') {
[void]$assets.Add(('https://{0}/' -f $Matches[1]))
}
$networkMode = 'offline'
if (-not [string]::IsNullOrWhiteSpace($NetworkProfile)) {
$networkMode = $NetworkProfile.Trim()
} elseif ($assets.Count -gt 0 -and $authStatusResolved -eq 'granted') {
# training labs / intentional vulns often use lab_only; default authorized_target_only
$networkMode = 'authorized_target_only'
}
# normalize common aliases
$netAliases = @{
'lab' = 'lab_only'
'authorized' = 'authorized_target_only'
'auth' = 'authorized_target_only'
'offline_only' = 'offline'
}
if ($netAliases.ContainsKey($networkMode.ToLowerInvariant())) {
$networkMode = $netAliases[$networkMode.ToLowerInvariant()]
}
$allowedNetworkModes = @('offline', 'lab_only', 'authorized_target_only', 'unrestricted_lab')
if ($networkMode -notin $allowedNetworkModes) {
throw "Invalid -NetworkProfile '$NetworkProfile'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)."
}
# ready_for_act requires auth granted + assets + non-offline network.
# -ReadyForAct cannot skip auth (would bypass hard gate).
$ready = $false
$netAllowsAct = ($networkMode -ne 'offline' -and -not [string]::IsNullOrWhiteSpace($networkMode))
if ($authStatusResolved -eq 'granted' -and $assets.Count -gt 0 -and $netAllowsAct) {
$ready = $true
} elseif ($ReadyForAct) {
if ($authStatusResolved -ne 'granted') {
Write-Host 'WARN: -ReadyForAct ignored because auth.status is not granted' -ForegroundColor Yellow
} elseif ($assets.Count -eq 0) {
Write-Host 'WARN: -ReadyForAct ignored because in_scope.assets is empty' -ForegroundColor Yellow
} elseif (-not $netAllowsAct) {
Write-Host 'WARN: -ReadyForAct ignored because network_profile is offline/empty' -ForegroundColor Yellow
}
}
# Optional master-route for primary skill
$primary = 'reverse-engineering/SKILL.md'
$primaryId = 'R0'
$routeScript = Join-Path $scriptDir 'master-route.ps1'
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
$scopeRoute = Join-Path $tmp 'route-scope.md'
if (Test-Path $scopeRoute) {
$rt = Get-Content $scopeRoute -Raw -Encoding UTF8
if ($rt -match 'primary_skill:\s*skills/(\S+)') { $primary = $Matches[1] }
if ($rt -match 'primary:\s*(\S+)') { $primaryId = $Matches[1] }
}
} finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
}
$created = Get-Date -Format 'o'
if ($assets.Count -gt 0) {
$assetsBlock = ($assets | ForEach-Object { " - $_" }) -join [Environment]::NewLine
} else {
$assetsBlock = ' []'
}
$checkAuth = if ($authStatusResolved -eq 'granted') { '[x]' } else { '[ ]' }
$checkScope = if ($assets.Count -gt 0) { '[x]' } else { '[ ]' }
$checkNet = if ($networkMode -and $networkMode -ne '') { '[x]' } else { '[ ]' }
$readyStr = if ($ready) { 'true' } else { 'false' }
$timelineNext = if ($ready) {
'open PRIMARY SKILL.md and ACT within scope'
} else {
'fill scope auth + in_scope; set ready_for_act'
}
$timelineSummary = if ($ready) {
'case directory created; scope ready_for_act=true'
} else {
'case directory created; scope pending auth'
}
$scope = @"
# Case Scope
## meta
- case_id: $CaseName
- created: $created
- operator: local
- project_root:REDACTED
- primary_skill: $primary
- primary_id: $primaryId
- lead_role: lead
- specialist_roles: []
- hint: $Hint
## auth
- status: $authStatusResolved
- basis: $AuthBasis
- evidence_of_auth: $evidenceAuth
- MUST NOT proceed if status != granted
## in_scope
- assets:
$assetsBlock
- surfaces: []
- activities: []
## out_of_scope
- assets: []
- activities: [dos, phishing_real_users, unrestricted_exfil]
## network_profile
- mode: $networkMode
- notes: |
offline | lab_only | authorized_target_only | unrestricted_lab
Change mode only after auth.status = granted.
## deliverables
- report: true
- field_journal: true
- diagrams: true
- timeline: true
## constraints
- timebox: {}
- stealth: low
- data_handling: anonymize
## signoff
- ready_for_act: $readyStr
- checklist:
- $checkAuth auth.status = granted
- $checkScope in_scope.assets non-empty OR offline sample path set
- $checkNet network_profile.mode chosen
- [ ] out_of_scope reviewed
- [ ] roles assigned (see skills/ops/role-map.md)
## ops_refs
- skills/ops/scope-contract.md
- skills/ops/evidence-finding-path.md
- skills/ops/role-map.md
- skills/ops/timeline-workitem.md
- skills/ops/IDENTITY.md
"@
$timeline = @"
# Timeline (append-only)
## $created | lead | init
- action: case-init
- command_or_ref: skills/scripts/case-init.ps1
- result_summary: $timelineSummary
- artifacts: [scope.md, workitems.md]
- evidence_ids: []
- next: $timelineNext
"@
$workitems = @"
# Work Items
| ID | title | role | targets | surface | status | evidence | notes |
|----|-------|------|---------|---------|--------|----------|-------|
| WI-001 | Establish scope and auth | lead | case | process | in_progress | | |
## Coverage
- [ ] Recon/analysis complete for in_scope assets
- [ ] Critical/High candidates triaged (or N/A for pure RE)
- [ ] Validated findings have Evidence (E-*)
- [ ] Path documented (attack/call/solve)
- [ ] Timeline continuous across major phases
- [ ] Report via docs-generator
- [ ] field-journal anonymized
## Refs
- skills/ops/timeline-workitem.md
- skills/ops/evidence-finding-path.md
"@
$utf8 = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText((Join-Path $caseRoot 'scope.md'), $scope, $utf8)
[System.IO.File]::WriteAllText((Join-Path $caseRoot 'timeline.md'), $timeline, $utf8)
[System.IO.File]::WriteAllText((Join-Path $caseRoot 'workitems.md'), $workitems, $utf8)
$readmeNext = if ($ready) {
@"
1. Scope is ready_for_act=true (auth granted + in_scope set)
2. Open primary skill: skills/$primary
3. Append ``timeline.md``; update ``workitems.md``
4. Append Evidence: ``skills/scripts/append-evidence.ps1 -CaseRoot <this dir> ...``
5. Promote findings with Evidence chain (skills/ops/evidence-finding-path.md)
6. Report via docs-generator; journal via field-journal
"@
} else {
@"
1. Edit ``scope.md`` — set auth.status=granted and in_scope (or re-run with -AuthGranted -TargetUrl)
2. Set ready_for_act when checklist complete
3. Open primary skill: skills/$primary
4. Append ``timeline.md``; update ``workitems.md``
5. Promote findings with Evidence chain (skills/ops/evidence-finding-path.md)
6. Report via docs-generator; journal via field-journal
"@
}
$readme = @"
# Case $CaseName
$readmeNext
"@
[System.IO.File]::WriteAllText((Join-Path $caseRoot 'README.md'), $readme, $utf8)
Write-Host ("CASE -> {0}" -f $caseRoot) -ForegroundColor Green
Write-Host ("PROJECT -> {0}" -f $projectRoot)
Write-Host ("PRIMARY skill: skills/{0} ({1})" -f $primary, $primaryId)
Write-Host ("auth.status={0} network_profile={1} ready_for_act={2}" -f $authStatusResolved, $networkMode, $readyStr)
if ($ready) {
Write-Host 'NEXT: open PRIMARY SKILL.md and ACT within scope' -ForegroundColor Green
} else {
Write-Host 'NEXT: fill scope.md auth + in_scope; then open PRIMARY SKILL.md'
}

View File

@@ -0,0 +1,343 @@
#!/usr/bin/env bash
# reverse-skill case initializer (bash parity of case-init.ps1 + offline/ctf presets)
# Usage:
# bash skills/scripts/case-init.sh --hint "apk reverse" --case-name demo
# bash skills/scripts/case-init.sh --hint "local sample" --preset offline-sample --sample ./app.apk
# bash skills/scripts/case-init.sh --hint "ctf web" --preset ctf-public --target-url https://chal.example
set -euo pipefail
HINT=""
CASE_NAME=""
PACKAGE_ROOT=""
AUTH_STATUS=""
AUTH_GRANTED=0
AUTH_BASIS="unknown"
EVIDENCE_OF_AUTH=""
TARGET_URL=""
NETWORK_PROFILE=""
SAMPLE=""
PRESET=""
IN_SCOPE_ASSETS=()
while [[ $# -gt 0 ]]; do
case "$1" in
-Hint|--hint) HINT="${2:-}"; shift 2 ;;
-CaseName|--case-name) CASE_NAME="${2:-}"; shift 2 ;;
-PackageRoot|--package-root) PACKAGE_ROOT="${2:-}"; shift 2 ;;
-AuthStatus|--auth-status) AUTH_STATUS="${2:-}"; shift 2 ;;
-AuthGranted|--auth-granted) AUTH_GRANTED=1; shift ;;
-AuthBasis|--auth-basis) AUTH_BASIS="${2:-}"; shift 2 ;;
-EvidenceOfAuth|--evidence-of-auth) EVIDENCE_OF_AUTH="${2:-}"; shift 2 ;;
-TargetUrl|--target-url) TARGET_URL="${2:-}"; shift 2 ;;
-NetworkProfile|--network-profile) NETWORK_PROFILE="${2:-}"; shift 2 ;;
-Sample|--sample|--offline-sample) SAMPLE="${2:-}"; shift 2 ;;
-Preset|--preset) PRESET="${2:-}"; shift 2 ;;
-InScopeAssets|--in-scope-asset) IN_SCOPE_ASSETS+=("${2:-}"); shift 2 ;;
-h|--help) sed -n '2,8p' "$0"; exit 0 ;;
*)
if [[ -z "$HINT" && "$1" != -* ]]; then HINT="$1"; shift
else echo "Unknown arg: $1" >&2; exit 2; fi
;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [[ -z "$PACKAGE_ROOT" ]]; then
PACKAGE_ROOT="$(cd "$SKILLS_ROOT/.." && pwd)"
fi
# Presets: reduce "AI refuses to work" friction for legitimate local/CTF work.
case "$PRESET" in
"" ) ;;
offline-sample|own-sample|local-sample)
AUTH_GRANTED=1
AUTH_STATUS="granted"
AUTH_BASIS="own_system"
NETWORK_PROFILE="${NETWORK_PROFILE:-offline}"
EVIDENCE_OF_AUTH="${EVIDENCE_OF_AUTH:-preset:offline-sample (owner-operated local file)}"
if [[ -n "$SAMPLE" ]]; then IN_SCOPE_ASSETS+=("$SAMPLE"); fi
;;
ctf-public|ctf)
AUTH_GRANTED=1
AUTH_STATUS="granted"
AUTH_BASIS="ctf_public"
NETWORK_PROFILE="${NETWORK_PROFILE:-authorized_target_only}"
EVIDENCE_OF_AUTH="${EVIDENCE_OF_AUTH:-preset:ctf-public}"
;;
own-system|lab-only)
AUTH_GRANTED=1
AUTH_STATUS="granted"
AUTH_BASIS="own_system"
NETWORK_PROFILE="${NETWORK_PROFILE:-lab_only}"
EVIDENCE_OF_AUTH="${EVIDENCE_OF_AUTH:-preset:own-system/lab}"
;;
*)
echo "WARN: unknown preset '$PRESET' (allowed: offline-sample|ctf-public|own-system)" >&2
;;
esac
if [[ -z "$CASE_NAME" ]]; then
slug="$(printf '%s' "$HINT" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/^$/case/' | cut -c1-40)"
CASE_NAME="$(date +%Y%m%d-%H%M%S)-${slug}"
fi
# CaseName is a directory name, never a path. Match the PowerShell guard while
# keeping localized letters available under UTF-8 locales.
case_name_length="$(printf '%s' "$CASE_NAME" | wc -m | tr -d '[:space:]')"
if [[ -z "$CASE_NAME" || "$case_name_length" -gt 80 ||
! "$CASE_NAME" =~ ^[[:alnum:]] ||
"$CASE_NAME" =~ [/:\\*?\"\<\>\|] ||
"$CASE_NAME" =~ [[:cntrl:]] ||
"$CASE_NAME" =~ [\.[:space:]]$ ]]; then
echo "Invalid --case-name '$CASE_NAME'. Use a 1-80 character directory name beginning with a letter or number; path separators, trailing dots/spaces, control characters, and wildcard characters are not allowed." >&2
exit 2
fi
if [[ -n "$NETWORK_PROFILE" ]]; then
network_profile_normalized="$(printf '%s' "$NETWORK_PROFILE" | tr '[:upper:]' '[:lower:]')"
case "$network_profile_normalized" in
offline|lab_only|authorized_target_only|unrestricted_lab|lab|authorized|auth|offline_only) ;;
*)
echo "Invalid --network-profile '$NETWORK_PROFILE'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)." >&2
exit 2
;;
esac
fi
CASE_ROOT="$PACKAGE_ROOT/work/$CASE_NAME"
mkdir -p "$CASE_root:REDACTED" "$CASE_ROOT/notes" "$CASE_ROOT/report"
auth_status_resolved="pending"
if [[ $AUTH_GRANTED -eq 1 ]]; then auth_status_resolved="granted"; fi
if [[ -n "$AUTH_STATUS" ]]; then
candidate="$(printf '%s' "$AUTH_STATUS" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
case "$candidate" in
pending|granted|denied|unknown) auth_status_resolved="$candidate" ;;
*) echo "WARN: ignoring invalid --auth-status '$AUTH_STATUS'" >&2 ;;
esac
fi
if [[ -n "$EVIDENCE_OF_AUTH" ]]; then
evidence_auth="$EVIDENCE_OF_AUTH"
elif [[ $AUTH_GRANTED -eq 1 || "$auth_status_resolved" == "granted" ]]; then
evidence_auth="cli-flag AuthGranted or AuthStatus=granted"
else
evidence_auth="FILL_ME"
fi
ASSETS=()
[[ -n "$TARGET_URL" ]] && ASSETS+=("$TARGET_URL")
[[ -n "$SAMPLE" ]] && ASSETS+=("$SAMPLE")
for a in "${IN_SCOPE_ASSETS[@]:-}"; do
[[ -n "$a" ]] || continue
dup=0
for e in "${ASSETS[@]:-}"; do [[ "$e" == "$a" ]] && dup=1 && break; done
[[ $dup -eq 0 ]] && ASSETS+=("$a")
done
if [[ ${#ASSETS[@]} -eq 0 && "$HINT" =~ https?://([^[:space:]/]+) ]]; then
ASSETS+=("https://${BASH_REMATCH[1]}/")
fi
if [[ -n "$NETWORK_PROFILE" ]]; then
network_mode="$NETWORK_PROFILE"
elif [[ "$auth_status_resolved" == "granted" && ${#ASSETS[@]} -gt 0 && -z "$SAMPLE" ]]; then
# Authorized network targets follow the PowerShell default. Offline is
# reserved for an explicit local sample, never for a URL by accident.
network_mode="authorized_target_only"
else
network_mode="offline"
fi
network_mode="$(printf '%s' "$network_mode" | tr '[:upper:]' '[:lower:]')"
case "$network_mode" in
lab) network_mode="lab_only" ;;
authorized|auth) network_mode="authorized_target_only" ;;
offline_only) network_mode="offline" ;;
offline|lab_only|authorized_target_only|unrestricted_lab) ;;
*)
echo "Invalid --network-profile '$network_mode'. Allowed: offline, lab_only, authorized_target_only, unrestricted_lab (aliases: lab, authorized, auth, offline_only)." >&2
exit 2
;;
esac
# Route PRIMARY via bash master-route
ROUTE_TMP="$(mktemp -d "${TMPDIR:-/tmp}/rs-case-route.XXXXXX")"
primary="reverse-engineering/SKILL.md"
primary_id="R0"
if [[ -f "$SCRIPT_DIR/master-route.sh" ]]; then
set +e
bash "$SCRIPT_DIR/master-route.sh" --hint "$HINT" --out-dir "$ROUTE_TMP" >/dev/null 2>&1
set -e
if [[ -f "$ROUTE_TMP/route-scope.md" ]]; then
rt="$(cat "$ROUTE_TMP/route-scope.md")"
if printf '%s\n' "$rt" | grep -Eq '^[[:space:]]*-[[:space:]]*primary_skill:[[:space:]]*skills/'; then
primary="$(printf '%s\n' "$rt" | grep -E '^[[:space:]]*-[[:space:]]*primary_skill:[[:space:]]*skills/' | head -1 | sed -E 's/.*skills\///' | tr -d '\r')"
fi
if printf '%s\n' "$rt" | grep -Eq '^[[:space:]]*-[[:space:]]*primary:[[:space:]]*'; then
primary_id="$(printf '%s\n' "$rt" | grep -E '^[[:space:]]*-[[:space:]]*primary:[[:space:]]*' | head -1 | sed -E 's/.*primary:[[:space:]]*//' | tr -d '\r')"
fi
fi
fi
rm -rf "$ROUTE_TMP"
created="$(date -Iseconds 2>/dev/null || date +%Y-%m-%dT%H:%M:%S%z)"
if [[ ${#ASSETS[@]} -gt 0 ]]; then
assets_block="$(printf ' - %s\n' "${ASSETS[@]}")"
else
assets_block=" []"
fi
ready=false
if [[ "$auth_status_resolved" == "granted" ]]; then
if [[ "$network_mode" == "offline" ]]; then
if [[ -n "$SAMPLE" && ${#ASSETS[@]} -gt 0 ]]; then ready=true; fi
elif [[ ${#ASSETS[@]} -gt 0 ]]; then
ready=true
fi
fi
check_auth="[ ]"; [[ "$auth_status_resolved" == "granted" ]] && check_auth="[x]"
check_scope="[ ]"; [[ ${#ASSETS[@]} -gt 0 || "$network_mode" == "offline" ]] && check_scope="[x]"
check_net="[ ]"; [[ -n "$network_mode" ]] && check_net="[x]"
ready_str="false"; [[ "$ready" == true ]] && ready_str="true"
if [[ "$ready" == true ]]; then
timeline_next="open PRIMARY SKILL.md and ACT within scope"
timeline_summary="case directory created; scope ready_for_act=true"
else
timeline_next="fill scope auth + in_scope; set ready_for_act"
timeline_summary="case directory created; scope pending auth"
fi
cat > "$CASE_ROOT/scope.md" <<EOF
# Case Scope
## meta
- case_id: $CASE_NAME
- created: $created
- operator: local
- primary_skill: $primary
- primary_id: $primary_id
- lead_role: lead
- specialist_roles: []
- hint: $HINT
- preset: ${PRESET:-none}
## auth
- status: $auth_status_resolved
- basis: $AUTH_BASIS
- evidence_of_auth: $evidence_auth
- MUST NOT proceed if status != granted
## in_scope
- assets:
$assets_block
- surfaces: []
- activities: []
## out_of_scope
- assets: []
- activities: [dos, phishing_real_users, unrestricted_exfil]
## network_profile
- mode: $network_mode
- notes: |
offline | lab_only | authorized_target_only | unrestricted_lab
Change mode only after auth.status = granted.
Presets: offline-sample | ctf-public | own-system
## deliverables
- report: true
- field_journal: true
- diagrams: true
- timeline: true
## constraints
- timebox: {}
- stealth: low
- data_handling: anonymize
## signoff
- ready_for_act: $ready_str
- checklist:
- $check_auth auth.status = granted
- $check_scope in_scope.assets non-empty OR offline sample path set
- $check_net network_profile.mode chosen
- [ ] out_of_scope reviewed
- [ ] roles assigned (see skills/ops/role-map.md)
## ops_refs
- skills/ops/scope-contract.md
- skills/ops/evidence-finding-path.md
- skills/ops/role-map.md
- skills/ops/timeline-workitem.md
- skills/ops/IDENTITY.md
EOF
cat > "$CASE_root:REDACTED.md" <<EOF
# Timeline (append-only)
## $created | lead | init
- action: case-init
- command_or_ref: skills/scripts/case-init.sh
- result_summary: $timeline_summary
- artifacts: [scope.md, workitems.md]
- evidence_ids: []
- next: $timeline_next
EOF
cat > "$CASE_root:REDACTED.md" <<'EOF'
# Work Items
| ID | title | role | targets | surface | status | evidence | notes |
|----|-------|------|---------|---------|--------|----------|-------|
| WI-001 | Establish scope and auth | lead | case | process | in_progress | | |
## Coverage
- [ ] Recon/analysis complete for in_scope assets
- [ ] Critical/High candidates triaged (or N/A for pure RE)
- [ ] Validated findings have Evidence (E-*)
- [ ] Path documented (attack/call/solve)
- [ ] Timeline continuous across major phases
- [ ] Report via docs-generator
- [ ] field-journal anonymized
## Refs
- skills/ops/timeline-workitem.md
- skills/ops/evidence-finding-path.md
EOF
if [[ "$ready" == true ]]; then
cat > "$CASE_ROOT/README.md" <<EOF
# Case $CASE_NAME
1. Scope is ready_for_act=true (auth granted + in_scope set)
2. Open primary skill: skills/$primary
3. Append \`timeline.md\`; update \`workitems.md\`
4. Append Evidence under \`evidence/\`
5. Promote findings with Evidence chain (skills/ops/evidence-finding-path.md)
6. Report via docs-generator; journal via field-journal
EOF
else
cat > "$CASE_ROOT/README.md" <<EOF
# Case $CASE_NAME
1. Edit \`scope.md\` — set auth.status=granted and in_scope
- or re-run: \`bash skills/scripts/case-init.sh --hint "..." --preset offline-sample --sample ./sample.apk\`
- or: \`--preset ctf-public --target-url https://...\`
2. Set ready_for_act when checklist complete
3. Open primary skill: skills/$primary
4. Append \`timeline.md\`; update \`workitems.md\`
5. Promote findings with Evidence chain (skills/ops/evidence-finding-path.md)
6. Report via docs-generator; journal via field-journal
EOF
fi
echo "CASE -> $CASE_ROOT"
echo "PRIMARY skill: skills/$primary ($primary_id)"
echo "auth.status=$auth_status_resolved network_profile=$network_mode ready_for_act=$ready_str"
if [[ "$ready" == true ]]; then
echo "NEXT: open PRIMARY SKILL.md and ACT within scope"
else
echo "NEXT: fill scope.md auth + in_scope; then open PRIMARY SKILL.md"
echo "HINT: try --preset offline-sample|--preset ctf-public|--preset own-system"
fi

View File

@@ -0,0 +1,124 @@
#Requires -Version 5.1
# 生成 skills/INDEX.md:从各模块 SKILL.md 的 frontmatter 提取 name+description,生成导航索引。
# 幂等:重复运行输出一致(CI 用 git diff 校验防 drift)。
# 用法:
# powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/extract-summaries.ps1
# powershell -File skills/scripts/extract-summaries.ps1 -Check # 只校验不写(CI 模式,不一致 exit 1)
param(
[switch] $Check
)
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$indexPath = Join-Path $skillsRoot 'INDEX.md'
# 扫描版本库中的模块 SKILL.md(排除本地 ignored/private trees,保证 clean clone 与开发机幂等)。
$skipDirs = @('ops', 'scripts', 'config', 'tests', 'field-journal', 'references')
$packageRoot = Split-Path -Parent $skillsRoot
$trackedSkillFiles = @()
$git = Get-Command git -ErrorAction SilentlyContinue
if ($git) {
$trackedPaths = @(& $git.Source -C $packageRoot ls-files -- 'skills/**/SKILL.md')
if ($LASTEXITCODE -eq 0) {
$trackedSkillFiles = @($trackedPaths | ForEach-Object {
$fullPath = Join-Path $packageRoot ($_ -replace '/', [IO.Path]::DirectorySeparatorChar)
if (Test-Path -LiteralPath $fullPath -PathType Leaf) { Get-Item -LiteralPath $fullPath }
})
}
}
$candidateSkillFiles = if ($trackedSkillFiles.Count -gt 0) {
$trackedSkillFiles
} else {
@(Get-ChildItem -Path $skillsRoot -Recurse -Filter 'SKILL.md')
}
$skillFiles = $candidateSkillFiles | Where-Object {
$rel = $_.FullName.Substring($skillsRoot.Length + 1)
$rel -ne 'SKILL.md' -and -not ($skipDirs | Where-Object { $rel.StartsWith($_ + '\') -or $rel.StartsWith($_ + '/') })
} | Sort-Object { $_.FullName.Substring($skillsRoot.Length + 1) }
$rows = New-Object System.Collections.ArrayList
foreach ($sf in $skillFiles) {
$rel = $sf.FullName.Substring($skillsRoot.Length + 1)
# 兼容 Windows(\)与 Linux/macOS(/)两种路径分隔符
$dir = $rel.Split(@('\', '/'))[0]
$head = Get-Content -LiteralPath $sf.FullName -TotalCount 15 -Encoding UTF8
$name = ''; $desc = ''; $blockMode = $false; $inFm = $false
foreach ($line in $head) {
if ($line -match '^---') {
if ($inFm) { break } # 第二个 --- 结束 frontmatter
$inFm = $true; continue
}
if (-not $inFm) { continue }
if ($line -match '^name:\s*(.+)$') { $name = $Matches[1].Trim(); continue }
if ($line -match '^description:\s*\|') { $blockMode = $true; continue }
if ($line -match '^description:\s*(.+)$') { $desc = $Matches[1].Trim(); continue }
if ($blockMode) {
# YAML 块:缩进的文本行拼接(取首行,超长截断)
if ($line -match '^\s{2,}(.+)$') {
if (-not $desc) { $desc = $Matches[1].Trim() }
} elseif ($line -match '^\S') { $blockMode = $false }
}
}
if (-not $name) { $name = $dir }
if (-not $desc) { $desc = '(无摘要)' }
if ($desc.Length -gt 160) { $desc = $desc.Substring(0, 157) + '...' }
[void]$rows.Add([pscustomobject]@{
Dir = $dir
Name = $name
Desc = $desc
Path = $rel
})
}
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# reverse-skill 技能导航索引')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('> 本文件由 `skills/scripts/extract-summaries.ps1` 自动生成,**请勿手改**。')
[void]$sb.AppendLine('> 修改摘要请编辑对应模块 `SKILL.md` 的 frontmatter `description`,然后重跑脚本。')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 模块总览')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('| 模块 | 摘要 |')
[void]$sb.AppendLine('|------|------|')
foreach ($r in $rows) {
$esc = $r.Desc -replace '\|', '\|'
[void]$sb.AppendLine(("| [{0}]({1}) | {2} |" -f $r.Name, ($r.Path -replace '\\', '/'), $esc))
}
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 目录树')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('```')
foreach ($r in $rows) {
[void]$sb.AppendLine(("skills/{0}/" -f ($r.Path -replace '\\', '/')))
}
[void]$sb.AppendLine('```')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## 路由')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('PRIMARY 路由由 `skills/config/routing.json`(唯一事实源)驱动,用 `master-route.ps1 -Hint "<任务>"` 分诊。')
[void]$sb.AppendLine('歧义场景读 `skills/routing.md` 全矩阵;CTF 多类型任务走 `CTF-Sandbox-Orchestrator/`。')
$newContent = $sb.ToString()
# 统一 LF 行尾:与 .gitattributes (*.md eol=lf) 保持一致,保证 clone 后 -Check 幂等
$newContent = $newContent -replace "`r`n", "`n"
$utf8 = New-Object System.Text.UTF8Encoding $true
if ($Check) {
if (-not (Test-Path -LiteralPath $indexPath)) {
Write-Host '[CHECK] INDEX.md missing' -ForegroundColor Red
exit 1
}
# 行尾免疫:autocrlf=true 的机器上 worktree 可能是 CRLF,normalize 后再比
$old = ([System.IO.File]::ReadAllText($indexPath)) -replace "`r`n", "`n"
if ($old -eq $newContent) {
Write-Host '[CHECK] INDEX.md up to date' -ForegroundColor Green
exit 0
}
Write-Host '[CHECK] INDEX.md out of date (run extract-summaries.ps1)' -ForegroundColor Red
exit 1
}
[System.IO.File]::WriteAllText($indexPath, $newContent, $utf8)
Write-Host ("INDEX.md regenerated: {0} modules" -f $rows.Count) -ForegroundColor Green

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,33 @@
Set-StrictMode -Version Latest
function Resolve-ReverseProjectRoot {
[CmdletBinding()]
param(
[AllowEmptyString()]
[string]$RequestedRoot = ''
)
$root = if ([string]::IsNullOrWhiteSpace($RequestedRoot)) {
(Get-Location).ProviderPath
} else {
$RequestedRoot
}
if ([string]::IsNullOrWhiteSpace($root)) {
throw 'Unable to resolve the analysis project root.'
}
if (-not (Test-Path -LiteralPath $root)) {
New-Item -ItemType Directory -Force -Path $root | Out-Null
}
if (-not (Test-Path -LiteralPath $root -PathType Container)) {
throw "Project root is not a directory: $root"
}
$item = Get-Item -LiteralPath $root
if ($item.PSProvider.Name -ne 'FileSystem') {
throw "Project root must use the FileSystem provider: $root"
}
return $item.FullName
}

View File

@@ -0,0 +1,174 @@
#Requires -Version 5.1
# reverse-skill PRIMARY router.
# 规则单一事实源:skills/config/routing.json(勿在本脚本内硬编码路由表)。
# CLI 兼容旧版:-Hint / -OutDir;输出 route-scope.md;退出码 0 成功 / 2 配置或技能缺失。
# 读取 UTF-8 BOM 源以保证 Windows PowerShell 5.1 下 CJK 正常。
param(
[string] $Hint = '',
[string] $OutDir = '',
[string] $ProjectRoot = ''
)
$ErrorActionPreference = 'Stop'
# Lowercase for Latin tokens; CJK unchanged by ToLowerInvariant.
$t = if ($Hint) { $Hint.ToLowerInvariant() } else { '' }
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$packageRoot = Split-Path -Parent $skillsRoot
$configPath = Join-Path $skillsRoot 'config\routing.json'
# --- 读取路由配置(单一事实源) ---
if (-not (Test-Path -LiteralPath $configPath)) {
Write-Host ("ERROR: routing config missing: {0}" -f $configPath) -ForegroundColor Red
Write-Host 'Restore skills/config/routing.json (git checkout / git pull) and retry.' -ForegroundColor Yellow
exit 2
}
try {
$cfg = Get-Content -LiteralPath $configPath -Raw -Encoding UTF8 | ConvertFrom-Json
} catch {
Write-Host ("ERROR: routing config is not valid JSON: {0}" -f $_.Exception.Message) -ForegroundColor Red
exit 2
}
# --- 规则匹配:每条 keyword 规则命中则计入候选集(与旧版逐条 if 行为一致) ---
$sel = New-Object System.Collections.Generic.List[string]
foreach ($route in $cfg.routes.PSObject.Properties) {
$id = $route.Name
foreach ($kw in $route.Value.keywords) {
$hit = $false
if ($null -ne $kw.must -and $t -match $kw.must) { $hit = $true }
# mustAll:全部子正则都匹配才成立
if ($hit -and $null -ne $kw.mustAll) {
foreach ($m in $kw.mustAll) {
if ($t -notmatch $m) { $hit = $false; break }
}
}
# exclude:命中则本条规则不触发(防误伤,如 越狱/LLM 语境、域控/攻击链)
if ($hit -and $null -ne $kw.exclude -and $t -match $kw.exclude) { $hit = $false }
if ($hit) { [void]$sel.Add($id) }
}
}
# --- 计分:同 id 多规则命中则加分(与旧版一致) ---
$scores = [ordered]@{}
foreach ($item in $sel) {
if (-not $scores.Contains($item)) { $scores[$item] = 0 }
$scores[$item] = $scores[$item] + 1
}
$uniq = New-Object System.Collections.Generic.List[string]
foreach ($d in $scores.Keys) { [void]$uniq.Add($d) }
# --- priority 自检:路由表与 priority 必须一一对应,防新增路由漏加优先级 ---
$routeIds = @($cfg.routes.PSObject.Properties | ForEach-Object { $_.Name })
$missingInPriority = @($routeIds | Where-Object { $_ -notin @($cfg.priority) })
$extraInPriority = @($cfg.priority | Where-Object { $_ -notin $routeIds })
if ($missingInPriority.Count -gt 0 -or $extraInPriority.Count -gt 0) {
Write-Host 'WARN: routing.json routes/priority mismatch:' -ForegroundColor Yellow
if ($missingInPriority.Count -gt 0) { Write-Host (" routes not in priority: {0}" -f ($missingInPriority -join ', ')) -ForegroundColor Yellow }
if ($extraInPriority.Count -gt 0) { Write-Host (" priority not in routes: {0}" -f ($extraInPriority -join ', ')) -ForegroundColor Yellow }
}
# --- 按 priority 顺序取分数最高者为 PRIMARY(并列时 priority 靠前者胜出) ---
$priority = @($cfg.priority)
$fallbackId = $cfg.meta.fallbackId
$primary = $null
$maxScore = -1
foreach ($p in $priority) {
if ($scores.Contains($p)) {
$score = $scores[$p]
if ($score -gt $maxScore) {
$maxScore = $score
$primary = $p
}
}
}
$notes = New-Object System.Collections.Generic.List[string]
$confidence = 'low'
if ($null -eq $primary) {
$primary = $fallbackId
if ($t) { [void]$notes.Add('No strong keyword hit; open routing.md full matrix') }
else { [void]$notes.Add('Empty hint; provide task text') }
} else {
$confidence = if ($uniq.Count -eq 1) { 'high' } else { 'medium' }
}
# 防御:priority 若含 routes 之外的幽灵 id(配置错误),回退到 fallback 而非崩溃
if ($routeIds -notcontains $primary) {
Write-Host ("WARN: primary id '{0}' not in routes; falling back to {1}" -f $primary, $fallbackId) -ForegroundColor Yellow
$primary = $fallbackId
}
# 复用当前主线的项目工作根解析,避免把路由产物写进 skill 包。
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$packageRoot = Split-Path -Parent $skillsRoot
. (Join-Path (Join-Path $scriptDir 'lib') 'WorkRoot.ps1')
$projectRoot = Resolve-ReverseProjectRoot -RequestedRoot $ProjectRoot
if ([string]::IsNullOrWhiteSpace($OutDir)) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$workRoot = Join-Path $projectRoot 'work'
$OutDir = Join-Path $workRoot ("master-route-{0}" -f $stamp)
}
$primaryPath = $cfg.routes.$primary.skill
$primaryLabel = $cfg.routes.$primary.label
$skillAbs = Join-Path $skillsRoot ($primaryPath -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path -LiteralPath $skillAbs)) {
Write-Host ("ERROR: PRIMARY skill missing: {0}" -f $skillAbs) -ForegroundColor Red
exit 2
}
# --- 输出目录(默认调用方项目 work\master-route-<ts>) ---
if ([string]::IsNullOrWhiteSpace($OutDir)) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
if ($packageRoot -and (Test-Path -LiteralPath $packageRoot)) {
$OutDir = Join-Path (Join-Path $packageRoot 'work') ("master-route-{0}" -f $stamp)
} else {
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
$OutDir = Join-Path (Join-Path $tmpBase 'reverse-skill-route') ("master-route-{0}" -f $stamp)
}
}
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
# --- 写 route-scope.md(格式与旧版一致,下游脚本解析 primary_skill / primary) ---
$sb = New-Object System.Text.StringBuilder
[void]$sb.AppendLine('# reverse-skill Master route (PRIMARY)')
[void]$sb.AppendLine(("- created: {0}" -f (Get-Date -Format 'o')))
[void]$sb.AppendLine(("- package: reverse-skill"))
[void]$sb.AppendLine(("- hint: {0}" -f $Hint))
[void]$sb.AppendLine(("- primary: {0}" -f $primary))
[void]$sb.AppendLine(("- primary_label: {0}" -f $primaryLabel))
[void]$sb.AppendLine(("- primary_skill: skills/{0}" -f $primaryPath))
[void]$sb.AppendLine(("- confidence: {0}" -f $confidence))
[void]$sb.AppendLine(("- project_root: {0}" -f $projectRoot))
$sec = New-Object System.Collections.Generic.List[string]
foreach ($d in $uniq) {
if ($d -ne $primary) { [void]$sec.Add(("skills/{0}" -f $cfg.routes.$d.skill)) }
}
$secText = if ($sec.Count -gt 0) { ($sec -join ', ') } else { '(none)' }
[void]$sb.AppendLine(("- secondary: {0}" -f $secText))
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## MUST open next')
[void]$sb.AppendLine('')
[void]$sb.AppendLine('1. skills/MASTER-ROUTING.md')
[void]$sb.AppendLine(("2. skills/{0}" -f $primaryPath))
[void]$sb.AppendLine('')
[void]$sb.AppendLine('## Notes')
if ($notes.Count -eq 0) { [void]$sb.AppendLine('- (none)') }
foreach ($n in $notes) { [void]$sb.AppendLine(("- {0}" -f $n)) }
# UTF-8 with BOM for route-scope (Windows notepad-friendly)
$utf8 = New-Object System.Text.UTF8Encoding $true
[System.IO.File]::WriteAllText((Join-Path $OutDir 'route-scope.md'), $sb.ToString(), $utf8)
Write-Host ("PRIMARY -> skills/{0}" -f $primaryPath) -ForegroundColor Green
Write-Host ("Label: {0} | confidence: {1}" -f $primaryLabel, $confidence)
foreach ($n in $notes) { Write-Host ("NOTE: {0}" -f $n) -ForegroundColor Yellow }
Write-Host ("Wrote {0}\route-scope.md" -f $OutDir)
Write-Host 'ACTION: Open PRIMARY SKILL.md now and execute ACTION REQUIRED.' -ForegroundColor Yellow

View File

@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# Client-neutral Bash entry for the structured router.
# routing.json is the only route table; this file must not duplicate route rules.
set -euo pipefail
HINT=""
OUT_DIR=""
PROJECT_ROOT=""
while [[ $# -gt 0 ]]; do
case "$1" in
-Hint|-hint|--hint) HINT="${2:-}"; shift 2 ;;
-OutDir|-out-dir|--out-dir) OUT_DIR="${2:-}"; shift 2 ;;
-ProjectRoot|-project-root|--project-root) PROJECT_ROOT="${2:-}"; shift 2 ;;
-h|--help)
echo 'Usage: master-route.sh --hint TEXT [--out-dir DIR] [--project-root DIR]'
exit 0
;;
*)
if [[ -z "$HINT" && "$1" != -* ]]; then HINT="$1"; shift
else echo "Unknown arg: $1" >&2; exit 2; fi
;;
esac
done
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILLS_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PACKAGE_ROOT="$(cd "$SKILLS_ROOT/.." && pwd)"
CONFIG_PATH="$SKILLS_ROOT/config/routing.json"
PYTHON=""
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1; then
PYTHON="$candidate"
break
fi
done
if [[ -z "$PYTHON" ]]; then
echo 'ERROR: Python 3 is required by the Bash structured router.' >&2
exit 2
fi
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "ERROR: routing config missing: $CONFIG_PATH" >&2
exit 2
fi
if [[ -z "$PROJECT_ROOT" ]]; then PROJECT_ROOT="$PACKAGE_ROOT"; fi
if [[ -z "$OUT_DIR" ]]; then
OUT_DIR="$PROJECT_ROOT/work/master-route-$(date +%Y%m%d-%H%M%S)"
fi
"$PYTHON" - "$CONFIG_PATH" "$SKILLS_ROOT" "$PROJECT_ROOT" "$OUT_DIR" "$HINT" <<'PY'
import datetime
import json
import pathlib
import re
import sys
config_path, skills_root_raw, project_root_raw, out_dir_raw, hint = sys.argv[1:]
skills_root = pathlib.Path(skills_root_raw)
project_root = pathlib.Path(project_root_raw).resolve()
out_dir = pathlib.Path(out_dir_raw)
try:
config = json.loads(pathlib.Path(config_path).read_text(encoding="utf-8-sig"))
except (OSError, json.JSONDecodeError) as exc:
print(f"ERROR: routing config is not valid JSON: {exc}", file=sys.stderr)
raise SystemExit(2)
routes = config.get("routes", {})
priority = config.get("priority", [])
fallback = config.get("meta", {}).get("fallbackId", "R0")
route_ids = set(routes)
if set(priority) != route_ids:
missing = sorted(route_ids - set(priority))
extra = sorted(set(priority) - route_ids)
print(f"WARN: routes/priority mismatch; missing={missing}, extra={extra}", file=sys.stderr)
text = hint.lower()
scores = {}
selected_order = []
def matches(pattern):
return bool(re.search(pattern, text, flags=re.IGNORECASE))
try:
for route_id, route in routes.items():
for rule in route.get("keywords", []):
must = rule.get("must")
hit = bool(must and matches(must))
must_all = rule.get("mustAll") or []
if isinstance(must_all, str):
must_all = [must_all]
if hit and any(not matches(pattern) for pattern in must_all):
hit = False
exclude = rule.get("exclude")
if hit and exclude and matches(exclude):
hit = False
if hit:
scores[route_id] = scores.get(route_id, 0) + 1
if route_id not in selected_order:
selected_order.append(route_id)
except re.error as exc:
print(f"ERROR: invalid regex in routing.json: {exc}", file=sys.stderr)
raise SystemExit(2)
primary = None
max_score = -1
for route_id in priority:
score = scores.get(route_id)
if score is not None and score > max_score:
primary = route_id
max_score = score
notes = []
if primary is None:
primary = fallback
confidence = "low"
notes.append("No strong keyword hit; open routing.md full matrix" if text else "Empty hint; provide task text")
else:
confidence = "high" if len(selected_order) == 1 else "medium"
if primary not in routes:
print(f"ERROR: fallback/primary route is missing: {primary}", file=sys.stderr)
raise SystemExit(2)
route = routes[primary]
primary_path = route["skill"]
skill_path = skills_root / pathlib.PurePosixPath(primary_path)
if not skill_path.is_file():
print(f"ERROR: PRIMARY skill missing: {skill_path}", file=sys.stderr)
raise SystemExit(2)
out_dir.mkdir(parents=True, exist_ok=True)
secondary = [f"skills/{routes[item]['skill']}" for item in selected_order if item != primary]
lines = [
"# reverse-skill Master route (PRIMARY)",
f"- created: {datetime.datetime.now(datetime.timezone.utc).astimezone().isoformat()}",
"- package: reverse-skill",
f"- hint: {hint}",
f"- primary: {primary}",
f"- primary_label: {route['label']}",
f"- primary_skill: skills/{primary_path}",
f"- confidence: {confidence}",
f"- project_root: {project_root}",
f"- secondary: {', '.join(secondary) if secondary else '(none)'}",
"",
"## MUST open next",
"",
"1. skills/MASTER-ROUTING.md",
f"2. skills/{primary_path}",
"",
"## Notes",
]
lines.extend(f"- {note}" for note in notes)
if not notes:
lines.append("- (none)")
(out_dir / "route-scope.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"PRIMARY -> skills/{primary_path}")
print(f"Label: {route['label']} | confidence: {confidence}")
for note in notes:
print(f"NOTE: {note}")
print(f"Wrote {out_dir / 'route-scope.md'}")
print("ACTION: Open PRIMARY SKILL.md now and execute ACTION REQUIRED.")
PY

View File

@@ -0,0 +1,178 @@
[CmdletBinding()]
param(
[string]$OutputMarkdown,
[string]$OutputJson
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
[Console]::InputEncoding = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)
$OutputEncoding = [System.Text.UTF8Encoding]::new($false)
if ([string]::IsNullOrWhiteSpace($OutputMarkdown)) {
$OutputMarkdown = Join-Path $PSScriptRoot '..\tool-index.md'
}
if ([string]::IsNullOrWhiteSpace($OutputJson)) {
$OutputJson = Join-Path $PSScriptRoot '..\tool-index.json'
}
. (Join-Path $PSScriptRoot 'lib\ToolDiscovery.ps1')
$scriptRefs = @{
'jadx' = @('apk-reverse/scripts/decode.ps1')
'apktool' = @('apk-reverse/scripts/decode.ps1', 'apk-reverse/scripts/rebuild-sign-install.ps1')
'adb' = @('apk-reverse/scripts/rebuild-sign-install.ps1')
'java' = @('apk-reverse/scripts/decode.ps1')
'apksigner' = @('apk-reverse/scripts/rebuild-sign-install.ps1')
'zipalign' = @('apk-reverse/scripts/rebuild-sign-install.ps1')
'frida' = @('apk-reverse/scripts/frida-run.ps1')
'frida-ps' = @('apk-reverse/scripts/frida-run.ps1')
'r2' = @('radare2/scripts/recon.ps1')
'rabin2' = @('radare2/scripts/recon.ps1')
'rasm2' = @('radare2/SKILL.md')
'radiff2' = @('radare2/SKILL.md')
'rahash2' = @('radare2/SKILL.md')
'rax2' = @('radare2/SKILL.md')
'python' = @('apk-reverse/scripts/frida-run.ps1', 'case-review/scripts/review_case.py')
'pip' = @()
'node' = @('js-reverse/SKILL.md')
'npx' = @('js-reverse/SKILL.md')
'jshookmcp' = @('js-reverse/SKILL.md')
'reqable-mcp' = @('pentest-tools/SKILL.md')
'jeb-pro' = @('apk-reverse/SKILL.md')
'seclists' = @('pentest-tools/SKILL.md')
'pentestswarm' = @('pentest-tools/SKILL.md')
'agent-browser' = @('browser-automation/SKILL.md')
'playwright' = @('browser-automation/SKILL.md', 'browser-automation/scripts/setup.ps1')
'analyzeHeadless' = @('reverse-engineering/SKILL.md')
'proxycat' = @('pentest-tools/SKILL.md')
'nmap' = @('pentest-tools/SKILL.md')
'binwalk' = @('firmware-pentest/SKILL.md')
'yara' = @('malware-analysis/SKILL.md')
'pwntools' = @('reverse-engineering/SKILL.md', 'reverse-engineering/patterns-ctf*.md')
'bkcrack' = @('reverse-engineering/crypto-decode-tools.md', '../CTF-Sandbox-Orchestrator/competition-zip-archive/SKILL.md')
}
$skillsRoot = Split-Path -Parent $PSScriptRoot
$reports = @(Get-ReverseToolReport | Where-Object {
$skill = [string]$_.Skill
$keep = $true
if ($keep -and -not [string]::IsNullOrWhiteSpace($skill)) {
$skillMd = Join-Path $skillsRoot (($skill -replace '/', [IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar + 'SKILL.md')
if (-not (Test-Path -LiteralPath $skillMd)) { $keep = $false }
}
$keep
})
$generatedAt = Get-Date -Format 'yyyy-MM-dd HH:mm:ss K'
$markdownLines = @(
'# 逆向工具索引',
'',
"- 扫描时间: $generatedAt",
'- 路由入口: `SKILL.md` → `routing.md` → 对应子 skill',
'- 说明: 本表由 `skills/scripts/refresh-tool-index.ps1` 自动生成,优先用于 Claude 路由和工具路径确认。',
'- 注意: 对于 jshookmcp 这类 MCP server,`yes` 只表示本机具备通过 node/npx 拉起它的条件,不表示它已经在 MCP 配置里注册并启用。',
'',
'| 工具 | 归属 skill | 作用 | 可用 | 路径 | 版本 | 来源 | 脚本引用 |',
'|---|---|---|---|---|---|---|---|'
)
foreach ($report in $reports) {
$pathText = if ($report.ResolvedPath) { $report.ResolvedPath } else { '—' }
$versionText = if ($report.Version) { $report.Version } else { '—' }
$refs = $scriptRefs[$report.Name]
$refsText = if ($refs -and $refs.Count -gt 0) { ($refs -join '<br>') } else { '—' }
$availableText = if ($report.Available) { 'yes' } else { 'no' }
$escapedPath = $pathText.Replace('|', '\|')
$escapedVersion = $versionText.Replace('|', '\|')
$escapedRefs = $refsText.Replace('|', '\|')
$markdownLines += "| $($report.Name) | $($report.Skill) | $($report.Purpose) | $availableText | $escapedPath | $escapedVersion | $($report.Source) | $escapedRefs |"
}
$markdownContent = ($markdownLines -join [Environment]::NewLine) + [Environment]::NewLine
$markdownContent | Set-Content -LiteralPath $OutputMarkdown -Encoding utf8
# --- Capability status view ---
$capabilityNames = @('jadx', 'apktool', 'jeb-pro', 'frida', 'frida-ps', 'idalib-mcp', 'jshookmcp', 'reqable-mcp', 'anything-analyzer', 'idapro', 'r2', 'rabin2', 'adb', 'agent-browser', 'ghidra-mcp', 'seclists', 'proxycat', 'burpsuite-mcp', 'pentestswarm', 'nmap', 'binwalk', 'yara', 'pwntools', 'bkcrack')
$capabilityRows = @()
foreach ($capName in $capabilityNames) {
$state = Get-ReverseCapabilityState -Name $capName
if ($null -eq $state) { continue }
$toolAvailable = $false
try {
$toolSpec = Resolve-ReverseToolSpec -Name $capName
$toolAvailable = $toolSpec.Available
}
catch {
# Capability exists in bootstrap manifest but not in tool catalog (e.g. MCP-only capabilities)
$toolAvailable = $false
}
$capabilityRows += [pscustomobject]@{
name = $capName
tool_available = $toolAvailable
ready = $state.Ready
mcp_registered = $state.Registered
service_online = $state.ServiceOnline
mcp_http_verified = $state.McpHttpVerified
can_auto_install = $state.CanAutoInstall
bootstrap_kind = $state.BootstrapKind
}
}
# Append capability view to markdown
$markdownCapLines = @(
'',
'---',
'',
'## 能力状态视图 (Capability Status)',
'',
'| 能力 | 工具可用 | Ready | MCP 已注册 | 服务在线 | MCP HTTP | 可自动安装 | 安装方式 |',
'|------|---------|-------|-----------|---------|-----------|---------|'
)
foreach ($cap in $capabilityRows) {
$toolText = if ($cap.tool_available) { '✓' } else { '✗' }
$readyText = if ($cap.ready) { '✓' } else { '✗' }
$mcpText = if ($cap.mcp_registered) { '✓' } else { '—' }
$svcText = if ($cap.service_online) { '✓' } else { '—' }
$mcpHttpText = if ($cap.mcp_http_verified) { '✓' } else { '—' }
$autoText = if ($cap.can_auto_install) { '✓' } else { '✗' }
$kindText = if ($cap.bootstrap_kind) { $cap.bootstrap_kind } else { '—' }
$markdownCapLines += "| $($cap.name) | $toolText | $readyText | $mcpText | $svcText | $mcpHttpText | $autoText | $kindText |"
}
$markdownCapLines += ''
$markdownCapLines += '> ✓ = 是 | ✗ = 否 | — = 不适用或未检测'
$markdownCapLines += ''
$capContent = ($markdownCapLines -join [Environment]::NewLine)
Add-Content -LiteralPath $OutputMarkdown -Value $capContent -Encoding utf8
$jsonRows = foreach ($report in $reports) {
[pscustomobject]@{
name = $report.Name
skill = $report.Skill
purpose = $report.Purpose
available = $report.Available
is_executable = $report.IsExecutable
is_directory = $report.IsDirectory
resolved_path = $report.ResolvedPath
version = $report.Version
source = $report.Source
script_refs = @($scriptRefs[$report.Name])
}
}
$jsonPayload = [pscustomobject]@{
generated_at = $generatedAt
routing_entry = @('SKILL.md', 'routing.md')
tools = $jsonRows
capabilities = $capabilityRows
}
$jsonPayload | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $OutputJson -Encoding utf8
"markdown=$OutputMarkdown"
"json=$OutputJson"
"tools=$($reports.Count)"

View File

@@ -0,0 +1,416 @@
#!/usr/bin/env bash
# refresh-tool-index.sh — generic Linux/macOS tool index refresh
#
# This script is intentionally detection-only. It does not install tools.
# Output defaults:
# skills/tool-index.md
# skills/tool-index.json
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SKILL_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$SKILL_ROOT/.." && pwd)"
OUTPUT_MD="${1:-${SKILL_ROOT}/tool-index.md}"
OUTPUT_JSON="${2:-${SKILL_ROOT}/tool-index.json}"
GENERATED_AT="$(date '+%Y-%m-%d %H:%M:%S %z')"
UNAME_S="$(uname -s 2>/dev/null || echo unknown)"
UNAME_R="$(uname -r 2>/dev/null || echo unknown)"
case "$UNAME_S" in
Darwin) PLATFORM="macos" ;;
Linux) PLATFORM="linux" ;;
*) PLATFORM="unknown" ;;
esac
has_cmd() { command -v "$1" >/dev/null 2>&1; }
cmd_path() { command -v "$1" 2>/dev/null || true; }
run_version() {
local cmd="$1"; shift
if ! has_cmd "$cmd"; then
echo ""
return 0
fi
"$cmd" "$@" 2>&1 | head -n 1 | tr '\n' ' ' | sed 's/[[:space:]]*$//' || true
}
file_exists_any() {
local p
for p in "$@"; do
if [[ -e "$p" ]]; then
echo "$p"
return 0
fi
done
echo ""
}
brew_prefix() {
if has_cmd brew; then
brew --prefix 2>/dev/null || true
fi
}
install_hint() {
local name="$1"
case "$PLATFORM:$name" in
linux:java) echo "apt: sudo apt install openjdk-17-jdk" ;;
linux:node) echo "apt/nvm: sudo apt install nodejs npm; prefer NodeSource or nvm for newer Node" ;;
linux:python3) echo "apt: sudo apt install python3 python3-venv python3-pip pipx" ;;
linux:jadx) echo "GitHub release: download jadx ZIP to ~/tools/jadx" ;;
linux:apktool) echo "apt or jar: sudo apt install apktool; or official apktool.jar" ;;
linux:adb) echo "apt or Android platform-tools: sudo apt install adb" ;;
linux:frida) echo "pipx: pipx install frida-tools" ;;
linux:r2|linux:radare2) echo "GitHub/source preferred; apt if available" ;;
linux:ghidra) echo "GitHub release ZIP or Flatpak; Java required" ;;
linux:burpsuite) echo "manual installer/jar; then load burp-mcp-full jar" ;;
linux:jshookmcp) echo "npx: npx -y @jshookmcp/jshook@0.3.4" ;;
linux:reqable-mcp) echo "npx: npx -y reqable-mcp-server@1.0.1; install Reqable desktop separately" ;;
linux:jeb-pro) echo "manual licensed install: https://www.pnfsoftware.com/jeb/" ;;
linux:anything-analyzer) echo "git clone + pnpm install + pnpm dev" ;;
linux:nuclei) echo "GitHub release or go install; apt may be unavailable" ;;
linux:seclists) echo "git clone https://github.com/danielmiessler/SecLists ~/tools/SecLists" ;;
macos:java) echo "brew: brew install openjdk" ;;
macos:node) echo "brew/nvm: brew install node; or nvm" ;;
macos:python3) echo "brew: brew install python; then pipx/venv" ;;
macos:jadx) echo "brew: brew install jadx" ;;
macos:apktool) echo "brew: brew install apktool" ;;
macos:adb) echo "brew: brew install android-platform-tools" ;;
macos:frida) echo "pipx: pipx install frida-tools" ;;
macos:r2|macos:radare2) echo "brew: brew install radare2" ;;
macos:ghidra) echo "brew: brew install ghidra or brew install --cask ghidra" ;;
macos:burpsuite) echo "brew cask/manual: brew install --cask burp-suite" ;;
macos:jshookmcp) echo "npx: npx -y @jshookmcp/jshook@0.3.4" ;;
macos:reqable-mcp) echo "npx: npx -y reqable-mcp-server@1.0.1; install Reqable desktop separately" ;;
macos:jeb-pro) echo "manual licensed install: https://www.pnfsoftware.com/jeb/" ;;
macos:anything-analyzer) echo "git clone + corepack enable + pnpm install + pnpm dev" ;;
macos:nuclei) echo "brew: brew install nuclei" ;;
macos:seclists) echo "git clone https://github.com/danielmiessler/SecLists ~/tools/SecLists" ;;
linux:binwalk) echo "apt: sudo apt install binwalk" ;;
linux:yara) echo "apt: sudo apt install yara" ;;
linux:pwntools) echo "pipx: pipx install pwntools" ;;
macos:binwalk) echo "brew: brew install binwalk" ;;
macos:yara) echo "brew: brew install yara" ;;
macos:pwntools) echo "pipx: pipx install pwntools" ;;
*) echo "see PLATFORMS.md and docs/platforms/${PLATFORM}.md" ;;
esac
}
# name|skill|purpose|commands|version command args|extra path probes
TOOLS=(
"java|core-runtime|Java runtime for jadx/apktool/Burp/Ghidra|java|java -version|"
"python3|core-runtime|Python runtime for helper scripts and pipx tools|python3|python3 --version|"
"pipx|core-runtime|Isolated Python CLI installer|pipx|pipx --version|"
"node|core-runtime|Node.js runtime for MCP bridges|node|node --version|"
"npm|core-runtime|Node package manager|npm|npm --version|"
"npx|core-runtime|Run npm MCP packages|npx|npx --version|"
"jadx|apk-reverse|APK Java/Kotlin decompiler|jadx|jadx --version|$HOME/tools/jadx/bin/jadx"
"apktool|apk-reverse|APK decode and rebuild|apktool|apktool --version|"
"adb|apk-reverse|Android device bridge|adb|adb version|$HOME/tools/android-platform-tools/platform-tools/adb"
"frida|reverse-engineering|Dynamic instrumentation CLI|frida|frida --version|$HOME/.local/bin/frida"
"frida-ps|reverse-engineering|Frida process listing|frida-ps|frida-ps --version|$HOME/.local/bin/frida-ps"
"r2|radare2|radare2 CLI analysis|r2|r2 -v|"
"rabin2|radare2|Binary metadata extraction|rabin2|rabin2 -v|"
"ghidra|reverse-engineering|Ghidra reverse-engineering suite|ghidraRun|ghidraRun --version|$HOME/tools/ghidra/ghidraRun;/Applications/Ghidra.app"
"idapro|ida-reverse|IDA Pro commercial reverse-engineering suite|idat|idat -v|/opt/idapro/idat;/Applications/IDA Professional.app;/Applications/IDA Free.app"
"burpsuite|burp-mcp|BurpSuite desktop application|burpsuite|burpsuite --version|/Applications/Burp Suite Professional.app;/Applications/Burp Suite Community Edition.app"
"graphviz|diagram-generator|Graphviz diagram rendering|dot|dot -V|"
"plantuml|diagram-generator|PlantUML diagram rendering|plantuml|plantuml -version|"
"nmap|pentest-tools|Network scanner|nmap|nmap --version|"
"sqlmap|pentest-tools|SQL injection testing tool|sqlmap|sqlmap --version|"
"ffuf|pentest-tools|Web fuzzer|ffuf|ffuf -V|"
"hashcat|pentest-tools|Password recovery|hashcat|hashcat --version|"
"nuclei|pentest-tools|Template-based vulnerability scanner|nuclei|nuclei -version|"
"binwalk|firmware-pentest|Firmware extraction and analysis|binwalk|binwalk --version|"
"seclists|pentest-tools|Security wordlists|none|none|$HOME/tools/SecLists;/usr/share/seclists"
"jshookmcp|js-reverse|JS/CDP/Hook MCP runtime via npx|npx|npx --version|"
"reqable-mcp|pentest-tools|Reqable desktop MCP runtime via npx|npx|npx --version|"
"jeb-pro|apk-reverse|Commercial Android/ARM decompiler (manual licensed install)|jeb,jeb_wincon|jeb --version|$HOME/tools/JEB/jeb;$HOME/JEB/jeb;/opt/jeb/jeb"
"anything-analyzer|browser-automation|Browser/HTTP analyzer MCP project|none|none|$HOME/tools/anything-analyzer;$REPO_ROOT/../anything-analyzer"
"burp-mcp-full|burp-mcp|Local Burp MCP extension and stdio bridge|none|none|$REPO_root:REDACTED/mcp-bridge.js"
"binwalk|firmware-pentest|Firmware extraction and analysis|binwalk|binwalk --version|"
"yara|malware-analysis|Malware rule matching engine|yara|yara --version|"
"pwntools|reverse-engineering|CTF pwn exploit development framework|pwn|pwn --version|"
)
records_tmp="$(mktemp)"
trap 'rm -f "$records_tmp"' EXIT
{
echo "# Tool Index"
echo ""
echo "- Generated at: $GENERATED_AT"
echo "- Platform: $PLATFORM ($UNAME_S $UNAME_R)"
echo "- Script: \`skills/scripts/refresh-tool-index.sh\`"
echo "- Note: This script detects tools only. It does not install tools."
echo ""
echo "| Tool | Skill | Purpose | Available | Path | Version | Source | Install hint |"
echo "|---|---|---|---|---|---|---|---|"
} > "$OUTPUT_MD"
for entry in "${TOOLS[@]}"; do
IFS='|' read -r name skill purpose commands version_spec probes <<< "$entry"
available="no"
path=""
version=""
source=""
if [[ "$commands" != "none" ]]; then
IFS=',' read -ra cmd_list <<< "$commands"
for cmd in "${cmd_list[@]}"; do
if has_cmd "$cmd"; then
available="yes"
path="$(cmd_path "$cmd")"
source="command"
break
fi
done
fi
if [[ "$available" == "no" && -n "${probes:-}" ]]; then
IFS=';' read -ra probe_list <<< "$probes"
for probe in "${probe_list[@]}"; do
if [[ -e "$probe" ]]; then
available="yes"
path="$probe"
source="path-probe"
break
fi
done
fi
if [[ "$version_spec" != "none" ]]; then
read -r ver_cmd ver_arg1 ver_arg2 <<< "$version_spec"
if has_cmd "$ver_cmd"; then
version="$(run_version "$ver_cmd" ${ver_arg1:-} ${ver_arg2:-})"
fi
fi
[[ -z "$path" ]] && path="—"
[[ -z "$version" ]] && version="—"
[[ -z "$source" ]] && source="—"
hint="$(install_hint "$name")"
echo "| $name | $skill | $purpose | $available | $path | $version | $source | $hint |" >> "$OUTPUT_MD"
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$name" "$skill" "$purpose" "$available" "$path" "$version" "$source" "$hint" >> "$records_tmp"
done
{
echo ""
echo "---"
echo ""
echo "## Next steps"
echo ""
case "$PLATFORM" in
linux)
echo "- Read \`docs/platforms/linux.md\` for ordinary Linux setup."
echo "- If the host is Kali, read \`kali/README-kali.md\` instead."
;;
macos)
echo "- Read \`docs/platforms/macos.md\` for Homebrew and app-bundle setup."
;;
*)
echo "- Read \`PLATFORMS.md\` and choose the closest platform guide."
;;
esac
echo "- Register MCP servers in your Agent client; tool availability does not imply MCP registration."
} >> "$OUTPUT_MD"
# --- Capability status view -------------------------------------------------
# Parity with skills/scripts/refresh-tool-index.ps1 (the "能力状态视图" block).
# Computed by parsing skills/scripts/bootstrap-manifest.json plus probing the
# local MCP config and each declared servicePort. All logic is contained in the
# Python heredoc below so this script keeps its existing bash dependencies.
MANIFEST_PATH="$SCRIPT_DIR/bootstrap-manifest.json"
MCP_CONFIG_PATH_FOR_CAP="${CLAUDE_MCP_CONFIG:-$HOME/.claude/mcp.json}"
CAP_RECORDS_TMP="$(mktemp)"
# Replace earlier single-file trap with one that also cleans this capability tmp file.
trap 'rm -f "$records_tmp" "$CAP_RECORDS_TMP"' EXIT
if [[ -f "$MANIFEST_PATH" ]]; then
# Pass tool availability info (collected earlier) so the capability view can
# reflect the same "tool ready" judgement as the tool table above.
python3 - "$MANIFEST_PATH" "$MCP_CONFIG_PATH_FOR_CAP" "$records_tmp" "$CAP_RECORDS_TMP" <<'PY'
import json, pathlib, socket, sys, urllib.request
manifest_path, mcp_config_path, tool_records_path, out_path = sys.argv[1:5]
# Load capability definitions
try:
manifest = json.loads(pathlib.Path(manifest_path).read_text(encoding='utf-8'))
except Exception:
manifest = {'capabilities': []}
capabilities = manifest.get('capabilities', [])
# Load currently registered MCP server names
registered_names = set()
try:
mcp_data = json.loads(pathlib.Path(mcp_config_path).read_text(encoding='utf-8'))
registered_names = set(mcp_data.get('mcpServers', {}).keys())
except Exception:
pass
# Load tool availability from the table we already wrote (best-effort match by name)
tool_available = {}
try:
with open(tool_records_path, encoding='utf-8') as f:
for line in f:
parts = line.rstrip('\n').split('\t')
if len(parts) >= 4:
tool_available[parts[0]] = (parts[3] == 'yes')
except Exception:
pass
def tcp_open(port, timeout=1.0):
try:
s = socket.socket(); s.settimeout(timeout)
s.connect(('127.0.0.1', int(port))); s.close()
return True
except Exception:
return False
def mcp_http_handshake(port, timeout=3):
try:
body = json.dumps({"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}).encode()
req = urllib.request.Request(
f"http://127.0.0.1:{int(port)}/mcp",
data=body,
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(req, timeout=timeout) as resp:
return resp.status == 200
except Exception:
return False
rows = []
for cap in capabilities:
name = cap.get('name')
if not name:
continue
bootstrap_kind = cap.get('bootstrapKind', '')
can_auto_install = bool(cap.get('canAutoInstall', False))
mcp_names = cap.get('mcpNames') or []
service_port = cap.get('servicePort')
verification_mode = cap.get('verificationMode', '')
registered = any(n in registered_names for n in mcp_names) if mcp_names else False
service_online = False
mcp_http_verified = False
if service_port:
service_online = tcp_open(service_port)
if service_online:
mcp_http_verified = mcp_http_handshake(service_port)
tool_ready = bool(tool_available.get(name, False))
if mcp_names:
if verification_mode == 'service-and-registration':
ready = registered and service_online
elif verification_mode == 'service-or-registration':
ready = registered or service_online
elif bootstrap_kind == 'npm-mcp':
ready = registered and tool_ready
else:
ready = registered or tool_ready
else:
ready = tool_ready
rows.append({
'name': name,
'tool_available': tool_ready,
'ready': ready,
'mcp_registered': registered if mcp_names else None,
'service_online': service_online if service_port else None,
'mcp_http_verified': mcp_http_verified if service_port else None,
'can_auto_install': can_auto_install,
'bootstrap_kind': bootstrap_kind,
})
with open(out_path, 'w', encoding='utf-8') as f:
json.dump(rows, f, ensure_ascii=False)
PY
# Append the markdown capability table (mirrors the headings used in the ps1 version)
{
echo ""
echo "---"
echo ""
echo "## 能力状态视图 (Capability Status)"
echo ""
echo "| 能力 | 工具可用 | Ready | MCP 已注册 | 服务在线 | MCP HTTP | 可自动安装 | 安装方式 |"
echo "|------|---------|-------|-----------|---------|----------|-----------|---------|"
} >> "$OUTPUT_MD"
python3 - "$CAP_RECORDS_TMP" "$OUTPUT_MD" <<'PY'
import json, sys
rows = json.loads(open(sys.argv[1], encoding='utf-8').read())
def yn(v): # True->✓, False->✗
return '✓' if v else '✗'
def opt(v): # True->✓, False->—, None->—
if v is True: return '✓'
if v is False: return '—'
return '—'
with open(sys.argv[2], 'a', encoding='utf-8') as out:
for r in rows:
out.write(
f"| {r['name']} | {yn(r['tool_available'])} | {yn(r['ready'])} | "
f"{opt(r['mcp_registered'])} | {opt(r['service_online'])} | "
f"{opt(r['mcp_http_verified'])} | {yn(r['can_auto_install'])} | "
f"{r['bootstrap_kind'] or '—'} |\n"
)
out.write("\n> ✓ = 是 | ✗ = 否 | — = 不适用或未检测\n\n")
PY
else
CAP_RECORDS_TMP=""
fi
# --- end capability status view ---------------------------------------------
python3 - "$records_tmp" "$OUTPUT_JSON" "$GENERATED_AT" "$PLATFORM" "$UNAME_S $UNAME_R" "${CAP_RECORDS_TMP:-}" <<'PY'
import json, sys
records_path, output_json, generated_at, platform, uname, cap_records_path = sys.argv[1:7]
tools=[]
with open(records_path, encoding='utf-8') as f:
for line in f:
name, skill, purpose, available, path, version, source, hint = line.rstrip('\n').split('\t')
tools.append({
'name': name,
'skill': skill,
'purpose': purpose,
'available': available == 'yes',
'path': None if path == '—' else path,
'version': None if version == '—' else version,
'source': None if source == '—' else source,
'install_hint': hint,
})
capabilities = []
if cap_records_path:
try:
with open(cap_records_path, encoding='utf-8') as f:
capabilities = json.load(f)
except Exception:
capabilities = []
with open(output_json, 'w', encoding='utf-8') as f:
json.dump({
'generated_at': generated_at,
'platform': platform,
'uname': uname,
'script': 'skills/scripts/refresh-tool-index.sh',
'tools': tools,
'capabilities': capabilities,
}, f, ensure_ascii=False, indent=2)
PY
echo "✅ Tool index refreshed"
echo " markdown=$OUTPUT_MD"
echo " json=$OUTPUT_JSON"
echo " platform=$PLATFORM"
echo " tools=${#TOOLS[@]}"

View File

@@ -0,0 +1,77 @@
<#
.SYNOPSIS
Scan text/markdown for un-anonymized sensitive info (IP/email/phone/JWT/API keys/tokens).
.DESCRIPTION
Companion to skills/field-journal/anonymization.md placeholder rules.
Default behavior: exit 1 when findings exist (CI gate).
Use -ReportOnly to just report without failing.
.PARAMETER Path
File or directory to scan (default: skills/field-journal).
Directory -> recursive *.md/*.txt/*.json; File -> that file only.
.PARAMETER ReportOnly
Report findings but do not set a failing exit code.
#>
param(
[string]$Path = "skills/field-journal",
[switch]$ReportOnly
)
$ErrorActionPreference = 'Stop'
# Allowed sample domains (documentation examples are not leaks)
$allowedDomains = @('example.com','example.org','example.net','example.edu','example.test','test','localhost','local','invalid')
$patterns = @(
@{ Name = 'Public IPv4'; Regex = '\b(?!(?:10\.|127\.|172\.(?:1[6-9]|2[0-9]|3[01])\.|192\.168\.|169\.254\.|100\.100\.|198\.51\.100\.|203\.0\.113\.|192\.0\.2\.|0\.0\.0\.|224\.|25[0-5]\.))(?:\d{1,3}\.){3}\d{1,3}\b' }
@{ Name = 'Email'; Regex = '\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b' }
@{ Name = 'CN mobile'; Regex = '\b1[3-9][0-9]{9}\b' }
@{ Name = 'JWT'; Regex = 'eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}' }
@{ Name = 'AWS Access Key'; Regex = '\bAKIA[0-9A-Z]{16}\b' }
@{ Name = 'OpenAI key'; Regex = '\bsk-(?:proj-)?[A-Za-z0-9]{20,}\b' }
@{ Name = 'GitHub token'; Regex = '\bgh[pousr]_[A-Za-z0-9]{20,}\b' }
@{ Name = 'npm token'; Regex = '\bnpm_[A-Za-z0-9]{30,}\b' }
@{ Name = 'Slack token'; Regex = '\bxox[baprs]-[A-Za-z0-9-]{10,}\b' }
@{ Name = 'Google API key'; Regex = '\bAIza[A-Za-z0-9_-]{35}\b' }
@{ Name = 'Stripe live key'; Regex = '\b(?:sk|rk)_live_[A-Za-z0-9]{20,}\b' }
)
function Test-EmailAllowed {
param([string]$Match)
$domain = ($Match -split '@')[-1]
foreach ($d in $allowedDomains) {
if ($domain -eq $d -or $domain.EndsWith('.' + $d)) { return $true }
}
return $false
}
$targets = @()
if (Test-Path $Path -PathType Container) {
$targets = Get-ChildItem -Path $Path -Recurse -File -Include *.md,*.txt,*.json | Sort-Object FullName
} elseif (Test-Path $Path -PathType Leaf) {
$targets = @(Get-Item $Path)
} else {
Write-Error "Path not found: $Path"
exit 2
}
$findings = 0
foreach ($t in $targets) {
$lines = Get-Content -Path $t.FullName -Encoding UTF8
for ($i = 0; $i -lt $lines.Count; $i++) {
$line = $lines[$i]
foreach ($p in $patterns) {
foreach ($m in [regex]::Matches($line, $p.Regex)) {
if ($p.Name -eq 'Email' -and (Test-EmailAllowed $m.Value)) { continue }
$findings++
$kind = if ($ReportOnly) { 'warning' } else { 'error' }
Write-Host "::$kind file=$($t.FullName),line=$($i + 1)::$($p.Name): $($m.Value)"
}
}
}
}
Write-Host "scan-leaks: scanned $($targets.Count) files, $findings finding(s)"
if ($findings -gt 0 -and -not $ReportOnly) { exit 1 }

View File

@@ -0,0 +1,183 @@
#Requires -Version 5.1
# reverse-skill smoke entrypoint: verify + script parse + master-route sample matrix.
# Usage:
# powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/smoke.ps1
# powershell -File skills/scripts/smoke.ps1 -LogDir C:\path\to\logs
param(
[string] $LogDir = '',
[string] $PackageRoot = ''
)
$ErrorActionPreference = 'Continue'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
if ([string]::IsNullOrWhiteSpace($LogDir)) {
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
$LogDir = Join-Path $tmpBase ("rs-smoke-{0}" -f $stamp)
}
New-Item -ItemType Directory -Path $LogDir -Force | Out-Null
$fail = New-Object System.Collections.Generic.List[string]
function Ok([string] $m) { Write-Host ("[OK] {0}" -f $m) -ForegroundColor Green }
function Bad([string] $m) {
Write-Host ("[FAIL] {0}" -f $m) -ForegroundColor Red
[void]$fail.Add($m)
}
Write-Host ("=== reverse-skill smoke | LogDir={0} ===" -f $LogDir)
# --- 1) routing coherence ---
$verify = Join-Path $scriptDir 'verify-routing-coherence.ps1'
if (-not (Test-Path -LiteralPath $verify)) {
Bad 'verify-routing-coherence.ps1 missing'
$verifyExit = 1
} else {
$vLog = Join-Path $LogDir '01-verify.txt'
& powershell -NoProfile -ExecutionPolicy Bypass -File $verify 2>&1 | Tee-Object -FilePath $vLog | Out-Null
$verifyExit = $LASTEXITCODE
if ($verifyExit -eq 0) { Ok 'verify-routing-coherence exit 0' } else { Bad ("verify-routing-coherence exit {0}" -f $verifyExit) }
}
# --- 2) parse key scripts ---
$scripts = @(
'verify-routing-coherence.ps1',
'master-route.ps1',
'case-init.ps1',
'lib\WorkRoot.ps1',
'bootstrap-reverse.ps1',
'refresh-tool-index.ps1',
'smoke.ps1',
'append-evidence.ps1',
'case-guard.ps1',
'test-routing.ps1',
'extract-summaries.ps1'
)
$parseOk = 0
$parseFail = 0
$parseLog = New-Object System.Collections.Generic.List[string]
foreach ($name in $scripts) {
$p = Join-Path $scriptDir $name
if (-not (Test-Path -LiteralPath $p)) {
# append-evidence is required for P0; others should exist
if ($name -eq 'append-evidence.ps1' -or $name -eq 'smoke.ps1') {
Bad ("script missing: {0}" -f $name)
$parseFail++
[void]$parseLog.Add("MISSING $name")
}
continue
}
$errs = $null
$tokens = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile($p, [ref]$tokens, [ref]$errs)
if ($errs -and $errs.Count -gt 0) {
Bad ("parse fail {0}: {1}" -f $name, $errs[0].Message)
$parseFail++
[void]$parseLog.Add("FAIL $name $($errs[0].Message)")
} else {
Ok ("parse {0}" -f $name)
$parseOk++
[void]$parseLog.Add("OK $name")
}
}
$parseLog -join [Environment]::NewLine | Set-Content (Join-Path $LogDir '02-parse.txt') -Encoding UTF8
# --- 3) master-route sample matrix ---
$mr = Join-Path $scriptDir 'master-route.ps1'
$cases = @(
@{ Name = 'apk'; Hint = 'decompile APK with jadx apktool smali'; Expect = 'apk-reverse' },
@{ Name = 'js'; Hint = 'js reverse frontend sign jshook encrypted param'; Expect = 'js-reverse' },
@{ Name = 'ida'; Hint = 'IDA decompile PE binary disassemble'; Expect = 'ida-reverse' },
@{ Name = 'pentest'; Hint = 'nmap nuclei sqlmap ffuf pentest bug bounty'; Expect = 'pentest-tools' },
@{ Name = 'llm'; Hint = 'LLM prompt inject jailbreak agent security garak'; Expect = 'llm-security' },
@{ Name = 'zh-apk'; Hint = '安卓 APK 加固 反编译'; Expect = 'apk-reverse' },
@{ Name = 'zh-pentest'; Hint = '渗透测试 端口扫描 SQL注入'; Expect = 'pentest-tools' },
@{ Name = 'zh-js'; Hint = '前端签名 JS逆向 加密参数'; Expect = 'js-reverse' },
@{ Name = 'evidence'; Hint = 'case review evidence chain traceability'; Expect = 'case-review' }
)
$routeOk = 0
$routeFail = 0
$routeSummary = New-Object System.Collections.Generic.List[string]
if (-not (Test-Path -LiteralPath $mr)) {
Bad 'master-route.ps1 missing'
} else {
foreach ($c in $cases) {
$outFile = Join-Path $LogDir ("route-{0}.txt" -f $c.Name)
$raw = & powershell -NoProfile -ExecutionPolicy Bypass -File $mr -Hint $c.Hint 2>&1 | Out-String
$raw | Set-Content -Path $outFile -Encoding UTF8
if ($raw -match [regex]::Escape($c.Expect)) {
Ok ("route {0} -> {1}" -f $c.Name, $c.Expect)
$routeOk++
[void]$routeSummary.Add("PASS $($c.Name)")
} else {
Bad ("route {0} expect {1}" -f $c.Name, $c.Expect)
$routeFail++
[void]$routeSummary.Add("FAIL $($c.Name)")
}
}
}
$routeSummary -join [Environment]::NewLine | Set-Content (Join-Path $LogDir '03-route-summary.txt') -Encoding UTF8
# --- 4) Evidence ID immutability ---
$appendEvidence = Join-Path $scriptDir 'append-evidence.ps1'
$evidenceCase = Join-Path $LogDir 'evidence-immutability'
if (-not (Test-Path -LiteralPath $appendEvidence)) {
Bad 'append-evidence.ps1 missing for immutability check'
} else {
New-Item -ItemType Directory -Path $evidenceCase -Force | Out-Null
& powershell -NoProfile -ExecutionPolicy Bypass -File $appendEvidence `
-CaseRoot $evidenceCase `
-Id 'E-IMMUTABLE' `
-Title 'first write' `
-ReproCommand 'echo first' 2>&1 | Out-Null
$firstEvidenceExit = $LASTEXITCODE
if ($firstEvidenceExit -ne 0) {
Bad ("initial Evidence append exit {0}" -f $firstEvidenceExit)
} else {
$evidencePath = Join-Path $evidenceCase 'evidence\E-IMMUTABLE.md'
$indexPath = Join-Path $evidenceCase 'evidence\INDEX.md'
$beforeEvidence = (Get-FileHash -LiteralPath $evidencePath -Algorithm SHA256).Hash
$beforeIndex = (Get-FileHash -LiteralPath $indexPath -Algorithm SHA256).Hash
& powershell -NoProfile -ExecutionPolicy Bypass -File $appendEvidence `
-CaseRoot $evidenceCase `
-Id 'E-IMMUTABLE' `
-Title 'second write' `
-ReproCommand 'echo second' 2>&1 | Out-Null
$duplicateEvidenceExit = $LASTEXITCODE
$afterEvidence = (Get-FileHash -LiteralPath $evidencePath -Algorithm SHA256).Hash
$afterIndex = (Get-FileHash -LiteralPath $indexPath -Algorithm SHA256).Hash
if ($duplicateEvidenceExit -eq 0) {
Bad 'duplicate Evidence ID was accepted'
} elseif ($beforeEvidence -ne $afterEvidence) {
Bad 'existing Evidence changed after duplicate append'
} elseif ($beforeIndex -ne $afterIndex) {
Bad 'Evidence index changed after duplicate append'
} else {
Ok 'duplicate Evidence ID rejected without mutation'
}
}
}
# --- summary ---
$summary = @(
"VERIFY_EXIT=$verifyExit",
"PARSE ok=$parseOk fail=$parseFail",
"ROUTE ok=$routeOk fail=$routeFail / $($cases.Count)",
"FAIL_COUNT=$($fail.Count)",
"LogDir=$LogDir"
)
$summary -join [Environment]::NewLine | Set-Content (Join-Path $LogDir 'SUMMARY.txt') -Encoding UTF8
Write-Host '=== SMOKE SUMMARY ==='
$summary | ForEach-Object { Write-Host $_ }
if ($fail.Count -gt 0) {
$fail | Set-Content (Join-Path $LogDir 'failures.txt') -Encoding UTF8
Write-Host 'OVERALL: FAIL' -ForegroundColor Red
exit 1
}
Write-Host 'OVERALL: ALL PASS' -ForegroundColor Green
exit 0

View File

@@ -0,0 +1,405 @@
#Requires -Version 5.1
# In-repo test: drives real smoke / case-init / append-evidence entrypoints.
# Usage:
# powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/test-p0-friction.ps1
# powershell -File skills/scripts/test-p0-friction.ps1 -ScratchDir $env:TEMP\my-scratch
param(
[string] $ScratchDir = '',
[string] $PackageRoot = ''
)
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
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'))
}
New-Item -ItemType Directory -Path $ScratchDir -Force | Out-Null
$fail = New-Object System.Collections.Generic.List[string]
function Ok($m) { Write-Host "[OK] $m" -ForegroundColor Green }
function Bad($m) { Write-Host "[FAIL] $m" -ForegroundColor Red; [void]$fail.Add($m) }
Write-Host "ScratchDir=$ScratchDir"
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 |
Tee-Object -FilePath $smokeLog | Out-Null
$smokeExit = $LASTEXITCODE
if ($smokeExit -eq 0) { Ok 'smoke exit 0' } else { Bad "smoke exit $smokeExit" }
$smokeText = Get-Content $smokeLog -Raw -Encoding UTF8
if ($smokeText -match 'verify-routing-coherence|VERIFY_EXIT=0|ALL PASS') { Ok 'smoke log has verify/pass signal' } else { Bad 'smoke log missing verify/pass signal' }
if ($smokeText -match 'route apk|parse master-route|parse case-init') { Ok 'smoke log has parse/route activity' } else { Bad 'smoke log missing parse/route activity' }
# 2) case-init ready-to-act
$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 `
-Hint 'web pentest nmap nuclei' `
-CaseName $caseName `
-PackageRoot $PackageRoot `
-AuthGranted `
-TargetUrl 'https://app.example.invalid/' `
-NetworkProfile 'authorized_target_only' 2>&1 |
Tee-Object -FilePath $ciLog | Out-Null
$caseRoot = Join-Path $PackageRoot ("work\{0}" -f $caseName)
$scopePath = Join-Path $caseRoot 'scope.md'
if (-not (Test-Path $scopePath)) { Bad "scope.md missing at $scopePath" }
else {
$scope = Get-Content $scopePath -Raw -Encoding UTF8
$scope | Set-Content (Join-Path $ScratchDir 'scope.md') -Encoding UTF8
if ($scope -match 'status:\s*granted') { Ok 'scope auth granted' } else { Bad 'scope auth not granted' }
if ($scope -match 'app\.example\.invalid') { Ok 'scope in_scope has target' } else { Bad 'scope missing target asset' }
if ($scope -match 'mode:\s*authorized_target_only') { Ok 'scope network authorized_target_only' } else { Bad 'scope network not authorized_target_only' }
if ($scope -match 'ready_for_act:\s*true') { Ok 'scope ready_for_act true' } else { Bad 'scope ready_for_act not true' }
}
# 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
$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'
} else {
Bad 'bare case-init defaults changed unexpectedly'
}
# 4) append-evidence
$evCase = Join-Path $ScratchDir 'case-ev'
New-Item -ItemType Directory -Path (Join-Path $evCase 'evidence') -Force | Out-Null
# minimal case tree
'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 `
-CaseRoot $evCase `
-Id 'E-001' `
-Title 'Smoke evidence item' `
-ReproCommand 'curl -sI https://app.example.invalid/' `
-Severity info `
-Status observed `
-RawExcerpt 'HTTP/1.1 200 OK' 2>&1 |
Tee-Object -FilePath $aeLog | Out-Null
$evFile = Join-Path $evCase 'evidence\E-001.md'
if (-not (Test-Path $evFile)) { Bad 'E-001.md not written' }
else {
$ev = Get-Content $evFile -Raw -Encoding UTF8
$ev | Set-Content (Join-Path $ScratchDir 'E-001.md') -Encoding UTF8
if ($ev -match 'title:\s*Smoke evidence item') { Ok 'evidence title' } else { Bad 'evidence title missing' }
if ($ev -match 'repro_command:') { Ok 'evidence repro_command' } else { Bad 'evidence repro_command missing' }
if ($ev -match 'curl -sI') { Ok 'evidence repro body' } else { Bad 'evidence repro body missing' }
}
$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 `
-CaseRoot $evCase `
-Id 'E-002' `
-Title 'Hashed evidence item' `
-ReproCommand 'Get-FileHash evidence\fixture.bin -Algorithm SHA256' `
-ArtifactPath 'evidence\fixture.bin' `
-Severity info `
-Status observed 2>&1 | Out-Null
$hashedEvidence = Join-Path $evCase 'evidence\E-002.md'
if (-not (Test-Path $hashedEvidence)) { Bad 'E-002.md not written' }
else {
$hashedText = Get-Content $hashedEvidence -Raw -Encoding UTF8
$expectedHash = (Get-FileHash -LiteralPath $artifact -Algorithm SHA256).Hash.ToLowerInvariant()
if ($hashedText -match [regex]::Escape('sha256:' + $expectedHash)) { Ok 'evidence SHA-256 recorded' } else { Bad 'evidence SHA-256 missing or incorrect' }
if ($hashedText -match '(?m)^- artifact_path:\s*evidence/fixture\.bin\s*$') { Ok 'evidence artifact path recorded' } else { Bad 'evidence artifact path missing' }
}
# 5) recon-pipeline topics present
$recon = Join-Path $skillsRoot 'pentest-tools\references\recon-pipeline.md'
$rt = Get-Content $recon -Raw -Encoding UTF8
foreach ($topic in @('Origin:', 'Referer:', 'eth0', 'append-evidence', 'Access-Control', 'nmap -sT')) {
# looser checks
}
if ($rt -match 'Origin:' -and $rt -match 'Referer:') { Ok 'recon-pipeline browser Origin/Referer' } else { Bad 'recon-pipeline missing Origin/Referer' }
if ($rt -match 'eth0' -or $rt -match 'nmap -sT') { Ok 'recon-pipeline Windows nmap note' } else { Bad 'recon-pipeline missing nmap note' }
if ($rt -match 'append-evidence') { Ok 'recon-pipeline Evidence append' } else { Bad 'recon-pipeline missing append-evidence' }
# 7) verify alone
$vLog = Join-Path $ScratchDir 'verify.log'
& powershell -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" }
# 8) Chinese route samples (P1)
$mr = Join-Path $scriptDir 'master-route.ps1'
$zhCases = @(
@{ Hint = '安卓 APK 加固 反编译'; Expect = 'apk-reverse' },
@{ Hint = '渗透测试 端口扫描 SQL注入'; Expect = 'pentest-tools' },
@{ Hint = '前端签名 JS逆向'; Expect = 'js-reverse' }
)
foreach ($zc in $zhCases) {
$raw = & powershell -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
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
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
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
# Note: -ProjectRoot is passed explicitly to prevent the -InScopeAssets array
# 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 `
-Hint 'gin juice lab pentest' `
-CaseName $labName `
-PackageRoot $PackageRoot `
-ProjectRoot $PackageRoot `
-AuthGranted `
-AuthBasis 'public_training_lab' `
-EvidenceOfAuth 'PortSwigger intentionally vulnerable public site example' `
-TargetUrl 'https://ginandjuice.shop/' `
-NetworkProfile 'lab_only' `
-InScopeAssets @('https://ginandjuice.shop/', 'ginandjuice.shop') 2>&1 | Out-Null
$labScopePath = Join-Path $PackageRoot ("work\{0}\scope.md" -f $labName)
$labScope = Get-Content $labScopePath -Raw -Encoding UTF8
$labScope | Set-Content (Join-Path $ScratchDir 'scope-lab.md') -Encoding UTF8
if ($labScope -match 'status:\s*granted') { Ok 'lab AuthGranted stays granted' } else { Bad 'lab auth not granted (AuthStatus clobber?)' }
if ($labScope -match 'ready_for_act:\s*true') { Ok 'lab ready_for_act true with lab_only' } else { Bad 'lab ready_for_act false' }
if ($labScope -match 'mode:\s*lab_only') { Ok 'lab network lab_only' } else { Bad 'lab network wrong' }
if ($labScope -match 'ginandjuice\.shop') { Ok 'lab in_scope has target' } else { Bad 'lab missing target asset' }
# 10b) garbage AuthStatus must not override AuthGranted
$junkName = 'p0-junk-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $ci `
-Hint 'web pentest lab' `
-CaseName $junkName `
-PackageRoot $PackageRoot `
-AuthGranted `
-AuthStatus 'demo.owasp-juice.shop' `
-TargetUrl 'https://example.invalid/' `
-NetworkProfile 'lab_only' 2>&1 | Tee-Object -FilePath (Join-Path $ScratchDir 'case-init-junk-auth.log') | Out-Null
$junkScope = Get-Content (Join-Path $PackageRoot ("work\{0}\scope.md" -f $junkName)) -Raw -Encoding UTF8
$junkScope | Set-Content (Join-Path $ScratchDir 'scope-junk-auth.md') -Encoding UTF8
if ($junkScope -match 'status:\s*granted' -and $junkScope -notmatch 'status:\s*demo\.owasp') {
Ok 'junk AuthStatus ignored; remains granted'
} else {
Bad 'junk AuthStatus clobbered granted'
}
# 11) append-evidence: special-char excerpt via -File + -RawExcerptFile (real nested CLI path)
$ae2 = Join-Path $ScratchDir 'case-ev'
New-Item -ItemType Directory -Path (Join-Path $ae2 'evidence') -Force | Out-Null
'x' | Set-Content (Join-Path $ae2 'scope.md') -Encoding UTF8
$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 `
-CaseRoot $ae2 `
-Id 'E-XML' `
-Title 'Stock API error sample' `
-ReproCommand 'curl -s -X POST https://example/stock --data-binary @x.xml' `
-RawExcerptFile $excerptFile `
-Severity medium `
-Status observed 2>&1 |
Tee-Object -FilePath (Join-Path $ScratchDir 'append-evidence.log') | Out-Null
if ($LASTEXITCODE -ne 0) { Bad "append-evidence -File exit $LASTEXITCODE" }
$evXml = Join-Path $ae2 'evidence\E-XML.md'
if (Test-Path $evXml) {
$ex = Get-Content $evXml -Raw -Encoding UTF8
$ex | Set-Content (Join-Path $ScratchDir 'E-XML.md') -Encoding UTF8
if ($ex -match '(?m)^- title:\s*Stock API error sample\s*$' -and $ex -match 'repro_command:') {
Ok 'evidence has title+repro'
} else { Bad 'evidence missing title/repro' }
# Assert specifically inside raw_excerpt block (not title)
$excerptBlockOk = $false
if ($ex -match '(?ms)^- raw_excerpt:\s*\|\r?\n(?<block>.*?)(?=\r?\n- |\z)') {
$block = $Matches['block']
if ($block -match 'XML parsing error' -and $block -match 'Entities are not allowed' -and $block -match '"') {
$excerptBlockOk = $true
}
}
if ($excerptBlockOk) { Ok 'raw_excerpt block preserves special payload' } else { Bad 'raw_excerpt block lost special payload' }
if ($ex -match '(?m)^- location:\s*n/a\s*$') { Ok 'location remains n/a' } else { Bad 'location polluted by arg split' }
if ($ex -match '(?m)^- source_type:\s*command\s*$') { Ok 'source_type remains command' } else { Bad 'source_type polluted by arg split' }
} else { Bad 'E-XML.md not written' }
# 11b) broken multi-word -RawExcerpt under -File must fail loud (not silently corrupt fields)
$ae3 = Join-Path $ScratchDir 'case-ev-broken'
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"
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
$play = Join-Path $skillsRoot 'pentest-tools\references\client-side-lab-playbook.md'
$recon = Join-Path $skillsRoot 'pentest-tools\references\recon-pipeline.md'
if (Test-Path $play) {
$pt = Get-Content $play -Raw -Encoding UTF8
if ($pt -match 'globoff' -and $pt -match 'innerHTML' -and $pt -match 'agent-browser' -and $pt -match 'observed') {
Ok 'client-side-lab-playbook topics'
} else { Bad 'client-side-lab-playbook missing topics' }
} else { Bad 'client-side-lab-playbook missing' }
if (Test-Path $recon) {
$rt = Get-Content $recon -Raw -Encoding UTF8
if ($rt -match 'Origin:' -and $rt -match 'Referer:' -and $rt -match 'globoff' -and ($rt -match 'nmap -sT' -or $rt -match 'eth0')) {
Ok 'recon-pipeline topic families'
} else { Bad 'recon-pipeline missing topic family' }
} else { Bad 'recon-pipeline missing' }
# 13) ReadyForAct alone must NOT mark ready without auth/assets
$forceName = 'p0-forceonly-' + (Get-Date -Format 'HHmmss')
& powershell -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') {
Ok 'ReadyForAct alone does not bypass auth'
} else {
Bad 'ReadyForAct alone incorrectly set ready/granted'
}
# 14) case-guard must not treat ops_refs paths as in_scope assets
$ghostCase = Join-Path $ScratchDir 'case-guard-ghost-asset'
New-Item -ItemType Directory -Force -Path $ghostCase | Out-Null
$ghostScope = @'
# Case Scope
## auth
- status: granted
## in_scope
- assets:
[]
## network_profile
- mode: lab_only
## signoff
- ready_for_act: true
## ops_refs
- skills/ops/scope-contract.md
- 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
if ($LASTEXITCODE -eq 2) { Ok 'case-guard rejects empty assets despite ops_refs URLs' }
else { Bad "case-guard should fail empty assets; exit=$LASTEXITCODE" }
# 14a) Auth/signoff fields outside their contract sections must not satisfy gate.
$sectionCase = Join-Path $ScratchDir 'case-guard-section-scope'
New-Item -ItemType Directory -Force -Path $sectionCase | Out-Null
$sectionScope = @'
# Case Scope
## auth
- status: pending
## in_scope
- assets:
- https://example.test/
## network_profile
- mode: authorized_target_only
## signoff
- ready_for_act: false
## notes
- status: granted
- 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
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" }
$networkCase = Join-Path $ScratchDir 'case-guard-invalid-network'
New-Item -ItemType Directory -Force -Path $networkCase | Out-Null
$networkScope = @'
# Case Scope
## auth
- status: granted
## in_scope
- assets:
- https://example.test/
## network_profile
- mode: internet
## signoff
- 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
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 @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ci,
'-Hint', 'authorized web review',
'-CaseName', $invalidNetworkName,
'-PackageRoot', $PackageRoot,
'-AuthGranted',
'-TargetUrl', 'https://example.test/',
'-NetworkProfile', 'internet'
) -Wait -PassThru -WindowStyle Hidden
$invalidNetworkRoot = Join-Path $PackageRoot ("work\{0}" -f $invalidNetworkName)
if ($invalidNetworkProcess.ExitCode -ne 0 -and -not (Test-Path -LiteralPath $invalidNetworkRoot)) {
Ok 'case-init rejects unsupported network profile before writing case'
} else {
Bad 'case-init accepted unsupported network profile or wrote a partial case'
}
# 14b) CaseName must remain a single safe directory name under work/.
$invalidCaseNames = @('..\case-escape', '../case-escape', 'case/name', 'case:name', '.. ', 'case.')
$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 @(
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $ci,
'-CaseName', $invalidCaseName, '-PackageRoot', $PackageRoot
) -Wait -PassThru -NoNewWindow
$exitCode = $process.ExitCode
$afterCases = @(Get-ChildItem -LiteralPath $workRoot -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name)
$newCases = @($afterCases | Where-Object { $_ -notin $beforeCases })
if ($exitCode -ne 0 -and $newCases.Count -eq 0) {
Ok "case-init rejects unsafe CaseName '$invalidCaseName'"
} else {
Bad "case-init accepted or wrote unsafe CaseName '$invalidCaseName'"
foreach ($newCase in $newCases) {
Remove-Item -LiteralPath (Join-Path $workRoot $newCase) -Recurse -Force -ErrorAction SilentlyContinue
}
}
}
# 13) copy smoke primary log alias for harness
$smokeLogs = Join-Path $ScratchDir 'smoke-logs'
if (Test-Path (Join-Path $smokeLogs 'SUMMARY.txt')) {
Copy-Item (Join-Path $smokeLogs 'SUMMARY.txt') (Join-Path $ScratchDir 'smoke-summary.txt') -Force
}
if (Test-Path $smokeLog) {
# already smoke.log
}
# OPT summary
$opt = @(
"entrypoints: smoke.ps1, case-init.ps1, append-evidence.ps1, case-guard.ps1, case-review/review_case.py, master-route.ps1, verify-routing-coherence.ps1, test-p0-friction.ps1",
"docs: recon-pipeline.md (Origin/Referer, nmap -sT/eth0, globoff, append-evidence); client-side-lab-playbook.md (innerHTML sink, agent-browser, observed vs validated, PP)",
"FAIL_COUNT=$($fail.Count)",
"ScratchDir=$ScratchDir"
)
$opt -join [Environment]::NewLine | Set-Content (Join-Path $ScratchDir 'OPT-SUMMARY.txt') -Encoding UTF8
$summary = "FAIL_COUNT=$($fail.Count)`nScratchDir=$ScratchDir"
$summary | Set-Content (Join-Path $ScratchDir 'TEST-SUMMARY.txt') -Encoding UTF8
Write-Host '=== TEST-P0 SUMMARY ==='
Write-Host $summary
if ($fail.Count -gt 0) {
$fail | Set-Content (Join-Path $ScratchDir 'failures.txt') -Encoding UTF8
exit 1
}
Write-Host 'OVERALL: ALL PASS' -ForegroundColor Green
exit 0

View File

@@ -0,0 +1,95 @@
#Requires -Version 5.1
# reverse-skill 路由回归测试器:跑 routing-benchmark.json 全部用例,比对 PRIMARY 期望。
# 用法:
# powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/test-routing.ps1
# powershell -File skills/scripts/test-routing.ps1 -Quick # 只跑 quick 最小回归集
# powershell -File skills/scripts/test-routing.ps1 -Benchmark <path> -LogDir <dir>
# 退出码:0 全绿 / 1 有用例失败(failures 详情写入 LogDir)
param(
[string] $Benchmark = '',
[switch] $Quick,
[string] $LogDir = '',
[string] $PackageRoot = ''
)
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
if (-not $PackageRoot) { $PackageRoot = Split-Path -Parent $skillsRoot }
if ([string]::IsNullOrWhiteSpace($Benchmark)) {
$Benchmark = Join-Path $skillsRoot 'tests\routing-benchmark.json'
}
if (-not (Test-Path -LiteralPath $Benchmark)) {
Write-Host ("ERROR: benchmark not found: {0}" -f $Benchmark) -ForegroundColor Red
exit 2
}
$bm = Get-Content -LiteralPath $Benchmark -Raw -Encoding UTF8 | ConvertFrom-Json
# $env:TEMP 在 Linux/macOS runner 上可能未设置,统一用 GetTempPath fallback
$tmpBase = if ($env:TEMP) { $env:TEMP } else { [System.IO.Path]::GetTempPath() }
if ([string]::IsNullOrWhiteSpace($LogDir)) {
$LogDir = Join-Path $tmpBase ("rs-routing-test-{0}" -f (Get-Date -Format 'yyyyMMdd-HHmmss'))
}
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
$masterRoute = Join-Path $scriptDir 'master-route.ps1'
$cases = @($bm.cases)
if ($Quick) { $cases = @($cases | Where-Object { $_.quick }) }
$fail = New-Object System.Collections.Generic.List[string]
$pass = 0
$failCount = 0
$detail = New-Object System.Collections.Generic.List[string]
Write-Host ("=== test-routing | {0} cases (quick={1}) ===" -f $cases.Count, [bool]$Quick)
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
$scope = Join-Path $tmp 'route-scope.md'
if (Test-Path -LiteralPath $scope) {
$text = Get-Content -LiteralPath $scope -Raw -Encoding UTF8
if ($text -match 'primary:\s*(\S+)') { $got = $Matches[1] }
}
} catch {
$got = 'EXC:' + $_.Exception.Message
} finally {
Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue
}
if ($got -eq $c.expect) {
$pass++
[void]$detail.Add(("PASS {0} -> {1}" -f $c.expect, $c.hint))
} else {
$failCount++
$msg = ("FAIL hint='{0}' expect={1} got={2}" -f $c.hint, $c.expect, $got)
Write-Host ("[FAIL] {0}" -f $msg) -ForegroundColor Red
[void]$fail.Add($msg)
[void]$detail.Add($msg)
}
}
$detail -join [Environment]::NewLine | Set-Content -LiteralPath (Join-Path $LogDir 'cases.txt') -Encoding UTF8
$summary = @(
"TOTAL=$($cases.Count)",
"PASS=$pass",
"FAIL=$failCount",
"QUICK=$([bool]$Quick)",
"LogDir=$LogDir"
)
$summary -join [Environment]::NewLine | Set-Content -LiteralPath (Join-Path $LogDir 'SUMMARY.txt') -Encoding UTF8
Write-Host ('=== ROUTING TEST SUMMARY ===')
$summary | ForEach-Object { Write-Host $_ }
if ($failCount -gt 0) {
$fail | Set-Content -LiteralPath (Join-Path $LogDir 'failures.txt') -Encoding UTF8
Write-Host "OVERALL: FAIL ($failCount)" -ForegroundColor Red
exit 1
}
Write-Host "OVERALL: ALL PASS ($pass)" -ForegroundColor Green
exit 0

View File

@@ -0,0 +1,76 @@
#!/usr/bin/env bash
# Run the shared routing benchmark through the Bash structured router.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BENCHMARK="$SCRIPT_DIR/../tests/routing-benchmark.json"
ROUTER="$SCRIPT_DIR/master-route.sh"
PYTHON=""
for candidate in python3 python; do
if command -v "$candidate" >/dev/null 2>&1; then
PYTHON="$candidate"
break
fi
done
if [[ -z "$PYTHON" ]]; then
echo 'ERROR: Python 3 is required by the Bash routing benchmark runner.' >&2
exit 2
fi
"$PYTHON" - "$BENCHMARK" "$ROUTER" <<'PY'
import json
import pathlib
import re
import subprocess
import sys
import tempfile
benchmark_path = pathlib.Path(sys.argv[1])
router_path = pathlib.Path(sys.argv[2])
benchmark = json.loads(benchmark_path.read_text(encoding="utf-8-sig"))
cases = benchmark["cases"]
failures = []
passed = 0
print(f"=== Bash routing benchmark | {len(cases)} cases ===")
with tempfile.TemporaryDirectory(prefix="rs-routing-bash-") as scratch:
root = pathlib.Path(scratch)
for index, case in enumerate(cases):
hint = case["hint"]
expected = case["expect"]
out_dir = root / str(index)
result = subprocess.run(
["bash", str(router_path), "--hint", hint, "--out-dir", str(out_dir)],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
)
got = "ERR"
scope_path = out_dir / "route-scope.md"
if result.returncode == 0 and scope_path.is_file():
match = re.search(
r"(?m)^- primary:[ \t]*(\S+)[ \t]*$",
scope_path.read_text(encoding="utf-8"),
)
if match:
got = match.group(1)
if got == expected:
passed += 1
else:
failures.append(
f"hint={hint!r} expect={expected} got={got} exit={result.returncode}"
)
print(f"TOTAL={len(cases)} PASS={passed} FAIL={len(failures)}")
if failures:
for failure in failures:
print(f"[FAIL] {failure}", file=sys.stderr)
raise SystemExit(1)
print(f"OVERALL: ALL PASS ({passed})")
PY

View File

@@ -0,0 +1,38 @@
#Requires -Version 5.1
param([string] $WorkflowPath = '')
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($WorkflowPath)) {
$WorkflowPath = Join-Path (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) '.github\workflows\auto-merge-journal.yml'
}
$text = Get-Content -LiteralPath $WorkflowPath -Raw -Encoding UTF8
$failures = New-Object System.Collections.Generic.List[string]
function Check([bool] $Condition, [string] $Message) {
if ($Condition) {
Write-Host "[OK] $Message" -ForegroundColor Green
} else {
Write-Host "[FAIL] $Message" -ForegroundColor Red
[void]$failures.Add($Message)
}
}
Check ($text -match '(?m)^\s+PR_NUMBER:\s*\$\{\{\s*github\.event\.pull_request\.number\s*\}\}') 'PR number is passed through the environment'
Check ($text -match '(?m)^\s+PR_TITLE:\s*\$\{\{\s*github\.event\.pull_request\.title\s*\}\}') 'PR title is passed through the environment'
Check ($text -match '(?m)^\s+PR_SUBJECT:\s*\$\{\{\s*format\(') 'PR subject is constructed as environment data'
Check ($text -match '(?ms)gh pr merge "\$PR_NUMBER".*--subject "\$PR_SUBJECT"') 'merge command quotes the PR number and subject'
Check ($text -notmatch '(?m)--subject[^\r\n]*github\.event\.pull_request\.title') 'PR title is not interpolated into the merge command'
Check ($text -notmatch '(?m)gh pr (?:diff|merge|comment)\s+\$\{\{') 'GitHub CLI commands do not embed event expressions'
$maliciousTitle = '$(Write-Host injected) `' + [Environment]::NewLine + '"quoted"; Get-ChildItem > should-not-run'
$subject = '[field-journal] ' + $maliciousTitle
Check ($subject -eq ('[field-journal] ' + $maliciousTitle)) 'shell metacharacters remain plain subject data'
if ($failures.Count -gt 0) {
exit 1
}
Write-Host 'WORKFLOW TITLE SAFETY CHECKS PASSED' -ForegroundColor Green
exit 0

View File

@@ -0,0 +1,125 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Regenerate docs/assets/star-history.svg from GitHub stargazers API.
.DESCRIPTION
Uses git credential manager token (or GH_TOKEN / GITHUB_TOKEN env).
Requires repo admin/collaborator access (GitHub July 2026 stargazer restriction).
Writes UTF-8 SVG without BOM. Does not commit.
.EXAMPLE
powershell -NoProfile -ExecutionPolicy Bypass -File skills/scripts/update-star-history.ps1
#>
param(
[string]$Owner = 'zhaoxuya520',
[string]$Repo = 'reverse-skill',
[string]$OutFile = ''
)
$ErrorActionPreference = 'Stop'
function Get-GitHubToken {
if ($env:GH_TOKEN) { return $env:GH_TOKEN }
if ($env:GITHUB_TOKEN) { return $env:GITHUB_TOKEN }
$input = "protocol=https`nhost=github.com`n`n"
$cred = $input | git credential fill 2>$null
$tok = ($cred | Where-Object { $_ -match '^password=' }) -replace '^password=', ''
if ([string]::IsNullOrWhiteSpace($tok)) {
throw 'No GitHub token. Set GH_TOKEN or use git credential manager (owner/collaborator required).'
}
return $tok
}
$token = Get-GitHubToken
$headers = @{
Authorization = "Bearer $token"
Accept = 'application/vnd.github.v3.star+json'
'User-Agent' = 'reverse-skill-star-history'
}
$all = New-Object System.Collections.Generic.List[object]
$page = 1
while ($true) {
$url = "https://api.github.com/repos/$Owner/$Repo/stargazers?per_page=100&page=$page"
$resp = Invoke-RestMethod -Uri $url -Headers $headers -Method Get
if (-not $resp -or @($resp).Count -eq 0) { break }
foreach ($s in @($resp)) { [void]$all.Add($s) }
Write-Host ("page={0} batch={1} total={2}" -f $page, @($resp).Count, $all.Count)
if (@($resp).Count -lt 100) { break }
$page++
if ($page -gt 500) { break }
}
if ($all.Count -eq 0) {
$repoInfo = Invoke-RestMethod -Uri "https://api.github.com/repos/$Owner/$Repo" -Headers @{
Authorization = "Bearer $token"; 'User-Agent' = 'reverse-skill-star-history'
}
$series = @([pscustomobject]@{ Date = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd'); Stars = [int]$repoInfo.stargazers_count })
}
else {
$byDay = @{}
foreach ($s in $all) {
if (-not $s.starred_at) { continue }
$day = ([datetime]$s.starred_at).ToUniversalTime().ToString('yyyy-MM-dd')
if (-not $byDay.ContainsKey($day)) { $byDay[$day] = 0 }
$byDay[$day]++
}
$cum = 0
$series = foreach ($d in ($byDay.Keys | Sort-Object)) {
$cum += $byDay[$d]
[pscustomobject]@{ Date = $d; Stars = $cum }
}
}
$scriptDir = $PSScriptRoot
$packageRoot = Split-Path (Split-Path $scriptDir -Parent) -Parent
if (-not $OutFile) {
$OutFile = Join-Path $packageRoot 'docs\assets\star-history.svg'
}
New-Item -ItemType Directory -Force -Path (Split-Path $OutFile -Parent) | Out-Null
$w = 800; $h = 400
$padL = 56; $padR = 24; $padT = 36; $padB = 48
$plotW = $w - $padL - $padR
$plotH = $h - $padT - $padB
$maxY = [Math]::Max(10, [Math]::Ceiling((($series | Measure-Object Stars -Maximum).Maximum) * 1.1))
$n = @($series).Count
$pts = for ($i = 0; $i -lt $n; $i++) {
$x = if ($n -eq 1) { $padL + $plotW / 2 } else { $padL + ($plotW * $i / ($n - 1)) }
$y = $padT + $plotH * (1 - ($series[$i].Stars / $maxY))
'{0:F1},{1:F1}' -f $x, $y
}
$poly = ($pts -join ' ')
$firstDate = $series[0].Date
$lastDate = $series[-1].Date
$maxStars = $series[-1].Stars
$gen = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm') + ' UTC'
$ticks = ''
for ($t = 0; $t -le 4; $t++) {
$val = [int]($maxY * $t / 4)
$y = $padT + $plotH * (1 - ($val / $maxY))
$ticks += " <line x1=`"$padL`" y1=`"$('{0:F1}' -f $y)`" x2=`"$($w - $padR)`" y2=`"$('{0:F1}' -f $y)`" stroke=`"#e5e7eb`" stroke-width=`"1`"/>`n"
$ticks += " <text x=`"$($padL - 8)`" y=`"$('{0:F1}' -f ($y + 4))`" text-anchor=`"end`" font-size=`"11`" fill=`"#6b7280`" font-family=`"system-ui,sans-serif`">$val</text>`n"
}
$fullName = "$Owner/$Repo"
$svg = @"
<svg xmlns="http://www.w3.org/2000/svg" width="$w" height="$h" viewBox="0 0 $w $h" role="img" aria-label="GitHub Star History for $fullName">
<title>Star History — $fullName</title>
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="$($w / 2)" y="22" text-anchor="middle" font-size="15" font-weight="600" fill="#111827" font-family="system-ui,sans-serif">Star History — $fullName</text>
<text x="$($w / 2)" y="$($h - 10)" text-anchor="middle" font-size="10" fill="#9ca3af" font-family="system-ui,sans-serif">generated $gen · $maxStars stars · $firstDate → $lastDate</text>
$ticks
<line x1="$padL" y1="$padT" x2="$padL" y2="$($padT + $plotH)" stroke="#9ca3af" stroke-width="1.5"/>
<line x1="$padL" y1="$($padT + $plotH)" x2="$($w - $padR)" y2="$($padT + $plotH)" stroke="#9ca3af" stroke-width="1.5"/>
<polyline fill="none" stroke="#2563eb" stroke-width="2.5" stroke-linejoin="round" stroke-linecap="round" points="$poly"/>
<text x="$padL" y="$($padT + $plotH + 28)" font-size="11" fill="#6b7280" font-family="system-ui,sans-serif">$firstDate</text>
<text x="$($w - $padR)" y="$($padT + $plotH + 28)" text-anchor="end" font-size="11" fill="#6b7280" font-family="system-ui,sans-serif">$lastDate</text>
<text x="$($w - $padR)" y="$($padT + 14)" text-anchor="end" font-size="12" fill="#2563eb" font-family="system-ui,sans-serif" font-weight="600">$maxStars ★</text>
</svg>
"@
[System.IO.File]::WriteAllText($OutFile, $svg, [System.Text.UTF8Encoding]::new($false))
Write-Host ("Wrote {0} ({1} stars, {2} points)" -f $OutFile, $maxStars, $n) -ForegroundColor Green

View File

@@ -0,0 +1,86 @@
<#
.SYNOPSIS
Verify doc fact tables (capability lists, MCP ports, Burp tool count)
against source-of-truth (bootstrap-manifest.json / McpHttpServer.java).
.DESCRIPTION
P1-4 guard: RULES.md / RULES_zh.md / skills/SKILL.md fact tables must not
drift from the manifest and the Burp extension source. Exit 1 on mismatch.
#>
$ErrorActionPreference = 'Stop'
$Root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$fail = 0
function Check([string]$Name, [bool]$Ok, [string]$Detail) {
if ($Ok) { Write-Host "OK $Name" } else { $script:fail++; Write-Host "FAIL $Name : $Detail" }
}
# --- Source of truth: manifest ---
$manifestPath = Join-Path $Root 'skills\scripts\bootstrap-manifest.json'
$manifest = Get-Content $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$capNames = @($manifest.capabilities | ForEach-Object { $_.name } | Sort-Object)
# --- Source of truth: Burp getToolList() ---
$javaPath = Join-Path $Root 'burp-mcp-full\src\main\java\com\burpmcp\McpHttpServer.java'
$java = Get-Content $javaPath -Raw
$def = [regex]::Match($java, 'String\s+getToolList\s*\([^)]*\)\s*\{')
if (-not $def.Success) { throw 'getToolList() not found in McpHttpServer.java' }
$endIdx = $java.IndexOf("`n }", $def.Index)
$body = $java.Substring($def.Index, $endIdx - $def.Index)
$burpTools = @([regex]::Matches($body, '\\"([a-z_0-9]+)\\"') | ForEach-Object { $_.Groups[1].Value } | Sort-Object -Unique)
$burpCount = $burpTools.Count
Write-Host "source-of-truth: $($capNames.Count) capabilities, Burp getToolList = $burpCount tools"
# --- Doc helpers ---
function Get-ListLine([string]$Text, [string]$Marker) {
$lines = $Text -split "`r?`n"
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i] -match [regex]::Escape($Marker)) {
$inline = [regex]::Match($lines[$i], '):(.+?)\s*$')
if ($inline.Success) { return $inline.Groups[1].Value }
if ($i + 1 -lt $lines.Count) { return $lines[$i + 1] }
}
}
return ''
}
function Test-ListHasAll([string]$ListText, [string[]]$Names) {
$tokens = @($ListText -split '[、,]+' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($tokens.Count -ne $Names.Count) { return $false }
foreach ($n in $Names) { if ($tokens -notcontains $n) { return $false } }
return $true
}
$rulesEn = Get-Content (Join-Path $Root 'RULES.md') -Raw -Encoding UTF8
$rulesZh = Get-Content (Join-Path $Root 'RULES_zh.md') -Raw -Encoding UTF8
$skillMd = Get-Content (Join-Path $Root 'skills\SKILL.md') -Raw -Encoding UTF8
$enList = Get-ListLine $rulesEn 'Supported capability names'
$zhList = Get-ListLine $rulesZh '支持的能力名'
$skList = Get-ListLine $skillMd '支持的能力'
Check 'RULES.md capability list' (Test-ListHasAll $enList $capNames) "expected $($capNames.Count) capabilities: $($capNames -join ', ')"
Check 'RULES_zh.md capability list' (Test-ListHasAll $zhList $capNames) "expected $($capNames.Count) capabilities"
Check 'SKILL.md capability list' (Test-ListHasAll $skList $capNames) "expected $($capNames.Count) capabilities"
Check 'RULES_zh.md count text' ($rulesZh.Contains("共 $($capNames.Count) 项")) "expected 共 $($capNames.Count) 项"
# --- MCP ports from manifest servicePort / servicePortRange ---
foreach ($c in $manifest.capabilities) {
if (-not $c.PSObject.Properties['servicePort']) { continue }
$port = [string]$c.servicePort
Check "RULES.md port $($c.name)=$port" ($rulesEn.Contains($port)) "missing port $port for $($c.name)"
Check "RULES_zh.md port $($c.name)=$port" ($rulesZh.Contains($port)) "missing port $port for $($c.name)"
if ($c.PSObject.Properties['servicePortRange']) {
$range = @($c.servicePortRange) -join '-'
Check "RULES.md range $($c.name)=$range" ($rulesEn.Contains($range)) "missing range $range for $($c.name)"
Check "RULES_zh.md range $($c.name)=$range" ($rulesZh.Contains($range)) "missing range $range for $($c.name)"
}
}
# --- Burp tool count ---
Check 'RULES.md burp tool count' ($rulesEn.Contains("$burpCount-tool")) "expected '$burpCount-tool' in RULES.md"
Check 'RULES_zh.md burp tool count' ($rulesZh.Contains("$burpCount 工具全控制")) "expected '$burpCount 工具全控制' in RULES_zh.md"
Write-Host "verify-doc-facts: $fail failure(s)"
if ($fail -gt 0) { exit 1 }

View File

@@ -0,0 +1,424 @@
#Requires -Version 5.1
# reverse-skill routing + ops contract gates (skill-router only; no host platform runtime)
param([string] $ScratchDir = '')
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
if (-not $scriptDir) { $scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path }
$skillsRoot = Split-Path -Parent $scriptDir
$packageRoot = Split-Path -Parent $skillsRoot
$masterRoute = Join-Path $scriptDir 'master-route.ps1'
$caseInit = Join-Path $scriptDir 'case-init.ps1'
$masterDoc = Join-Path $skillsRoot 'MASTER-ROUTING.md'
$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'))
}
New-Item -ItemType Directory -Force -Path $ScratchDir | Out-Null
$fail = New-Object System.Collections.Generic.List[string]
function Ok($m) { Write-Host "[OK] $m" -ForegroundColor Green }
function Bad($m) { Write-Host "[FAIL] $m" -ForegroundColor Red; [void]$fail.Add($m) }
# --- 新事实源/产物检查(routing.json / benchmark / INDEX) ---
$routingJson = Join-Path $skillsRoot 'config/routing.json'
if (Test-Path -LiteralPath $routingJson) {
$rj = Get-Content -LiteralPath $routingJson -Raw -Encoding UTF8 | ConvertFrom-Json
$rjRoutes = @($rj.routes.PSObject.Properties)
if ($rjRoutes.Count -ge 30) { Ok "routing.json routes=$($rjRoutes.Count)" } else { Bad 'routing.json route count suspicious (<30)' }
$badRoute = @($rjRoutes | Where-Object { -not $_.Value.label -or -not $_.Value.skill -or -not $_.Value.keywords })
if ($badRoute.Count -eq 0) { Ok 'routing.json: all routes have label/skill/keywords' } else { Bad "routing.json routes missing fields: $($badRoute.Name -join ',')" }
$missingRouteSkills = @($rjRoutes | Where-Object {
-not (Test-Path -LiteralPath (Join-Path $skillsRoot ($_.Value.skill -replace '/', [IO.Path]::DirectorySeparatorChar)) -PathType Leaf)
})
if ($missingRouteSkills.Count -eq 0) { Ok 'routing.json: all route skills exist' } else { Bad "routing.json missing skill files: $($missingRouteSkills.Name -join ',')" }
$git = Get-Command git -ErrorAction SilentlyContinue
if ($git) {
$trackedSkills = @(& $git.Source -C $packageRoot ls-files -- 'skills/**/SKILL.md')
if ($LASTEXITCODE -eq 0) {
$untrackedRouteSkills = @($rjRoutes | Where-Object { ('skills/' + $_.Value.skill) -notin $trackedSkills })
if ($untrackedRouteSkills.Count -eq 0) { Ok 'routing.json: all route skills are tracked' } else { Bad "routing.json references untracked skills: $($untrackedRouteSkills.Name -join ',')" }
}
}
$routeIds = @($rjRoutes | ForEach-Object { $_.Name })
$missingPrio = @($routeIds | Where-Object { $_ -notin @($rj.priority) })
$extraPrio = @($rj.priority | Where-Object { $_ -notin $routeIds })
if ($missingPrio.Count -eq 0 -and $extraPrio.Count -eq 0) { Ok 'routing.json priority covers all routes (1:1)' } else { Bad "routing.json priority mismatch: missing=$($missingPrio -join ',') extra=$($extraPrio -join ',')" }
} else {
Bad 'skills/config/routing.json missing (single source of truth)'
}
$benchJson = Join-Path $skillsRoot 'tests/routing-benchmark.json'
if (Test-Path -LiteralPath $benchJson) {
$bj = Get-Content -LiteralPath $benchJson -Raw -Encoding UTF8 | ConvertFrom-Json
$bjCases = @($bj.cases)
if ($bjCases.Count -ge 100) { Ok "benchmark cases=$($bjCases.Count)" } else { Bad "benchmark cases < 100 ($($bjCases.Count))" }
$badExpect = @($bjCases | Where-Object { $_.expect -notmatch '^R\d+$' })
if ($badExpect.Count -eq 0) { Ok 'benchmark expect ids well-formed' } else { Bad "benchmark bad expect: $($badExpect.Count)" }
# benchmark expect 必须存在于 routing.json(防 benchmark 引用已删除的路由)
if (Test-Path -LiteralPath $routingJson) {
$rjIds = @($rjRoutes | ForEach-Object { $_.Name })
$ghostExpect = @($bjCases | Where-Object { $_.expect -notin $rjIds })
if ($ghostExpect.Count -eq 0) { Ok 'benchmark expects all exist in routing.json' } else { Bad "benchmark ghost expects: $(($ghostExpect | Select-Object -First 5).expect -join ',')" }
}
} else {
Bad 'skills/tests/routing-benchmark.json missing'
}
if (Test-Path -LiteralPath (Join-Path $skillsRoot 'INDEX.md')) { Ok 'INDEX.md present (generated)' } else { Bad 'INDEX.md missing (run extract-summaries.ps1)' }
# master-route.ps1 不得回退到硬编码路由表(防绕过 routing.json)
$mrText = Get-Content -LiteralPath (Join-Path $scriptDir 'master-route.ps1') -Raw -Encoding UTF8
if ($mrText -match '\$map\s*=\s*\[ordered\]' -or $mrText -match "R1'\s*=\s*'apk-reverse") {
Bad 'master-route.ps1 contains hardcoded routing table (must read routing.json)'
} else {
Ok 'master-route.ps1 has no hardcoded routing table'
}
# --- ops artifacts exist ---
$opsFiles = @(
'ops/IDENTITY.md',
'ops/scope-contract.md',
'ops/evidence-finding-path.md',
'ops/role-map.md',
'ops/timeline-workitem.md',
'ops/sandbox-profile.md',
'ops/skill-supply-chain.md',
'ops/README.md',
'references/community-security-skills.md',
'references/domain-coverage-map.md',
'attack-chain\references\lifecycle-checklist.md',
'reverse-engineering/references\re-agent-workflow.md',
'pentest-tools/references\recon-pipeline.md',
'MASTER-ROUTING.md',
'scripts\master-route.ps1',
'scripts\case-init.ps1',
'scripts\lib\WorkRoot.ps1',
'case-review/SKILL.md',
'case-review/scripts/review_case.py',
'docs-generator/references\security-report-templates.md',
'docs-generator/references\vendor-report-rules.md',
'field-journal/_template.md'
)
$indexLines = New-Object System.Collections.Generic.List[string]
foreach ($rel in $opsFiles) {
$p = Join-Path $skillsRoot $rel
if (Test-Path -LiteralPath $p) {
Ok "artifact $rel"
[void]$indexLines.Add("OK $rel")
} else {
Bad "missing $rel"
[void]$indexLines.Add("MISS $rel")
}
}
$indexLines | Set-Content -LiteralPath (Join-Path $ScratchDir 'artifacts-index.txt') -Encoding UTF8
# --- links from hubs (skills + RULES single source) ---
foreach ($hub in @('MASTER-ROUTING.md', 'SKILL.md', 'routing.md')) {
$t = Get-Content (Join-Path $skillsRoot $hub) -Raw -Encoding UTF8
if ($t -match 'ops/scope-contract|ops\\scope-contract|case-init') { Ok "hub link scope in $hub" }
else { Bad "hub $hub missing scope/case-init link" }
if ($t -match 'ops/IDENTITY|IDENTITY\.md') { Ok "hub identity $hub" }
else { Bad "hub $hub missing IDENTITY" }
}
# research deposits must be reachable from hubs
$hubAll = ''
foreach ($hub in @('MASTER-ROUTING.md', 'SKILL.md', 'ops/README.md', 'routing.md')) {
$hp = Join-Path $skillsRoot $hub
if (Test-Path $hp) { $hubAll += (Get-Content $hp -Raw -Encoding UTF8) }
}
foreach ($n in @('community-security-skills', 'skill-supply-chain', 're-agent-workflow', 'recon-pipeline')) {
if ($hubAll -match [regex]::Escape($n)) { Ok "hub surfaces $n" }
else { Bad "hub missing surface for $n" }
}
# RULES.md / RULES_zh.md MUST gate case-init/scope before ACT (injection + CRITICAL + chain)
$rulesEn = Join-Path $packageRoot 'RULES.md'
$rulesZh = Join-Path $packageRoot 'RULES_zh.md'
foreach ($rp in @($rulesEn, $rulesZh)) {
$name = Split-Path $rp -Leaf
if (-not (Test-Path -LiteralPath $rp)) { Bad "missing $name"; continue }
$rt = Get-Content -LiteralPath $rp -Raw -Encoding UTF8
if ($rt -match 'case-init' -and ($rt -match 'scope-contract|scope\.md|network_profile')) {
Ok "$name has case-init/scope gate"
} else {
Bad "$name missing case-init/scope/network_profile gate"
}
# Compact or CRITICAL must not jump routing→ACT without scope
if ($rt -match 'auth\.status\s*=\s*granted|auth.status=granted|未就绪禁止|MUST NOT ACT against targets|禁止对目标 ACT') {
Ok "$name has auth hard gate language"
} else {
Bad "$name missing auth hard-gate language"
}
# Post-trigger / 行为链: case-init before ACT pattern
if ($rt -match '(?s)case-init.{0,400}ACT|scope\.md.{0,400}ACT|scope-contract.{0,400}ACT') {
Ok "$name orders scope before ACT (nearby)"
} else {
Bad "$name does not place scope/case-init before ACT"
}
}
# --- template required headings ---
$fieldLog = New-Object System.Collections.Generic.List[string]
function Assert-Fields([string]$path, [string[]]$needles) {
$t = Get-Content $path -Raw -Encoding UTF8
foreach ($n in $needles) {
if ($t -match [regex]::Escape($n)) {
Ok "field '$n' in $(Split-Path $path -Leaf)"
[void]$fieldLog.Add("OK $n @ $path")
} else {
Bad "field '$n' missing in $path"
[void]$fieldLog.Add("MISS $n @ $path")
}
}
}
Assert-Fields (Join-Path $skillsRoot 'ops/scope-contract.md') @('auth', 'in_scope', 'out_of_scope', 'network_profile', 'deliverables')
Assert-Fields (Join-Path $skillsRoot 'ops/evidence-finding-path.md') @('Evidence', 'Finding', 'Path', 'repro_command', 'evidence_ids')
Assert-Fields (Join-Path $skillsRoot 'ops/timeline-workitem.md') @('timeline.md', 'workitems.md', 'Coverage')
Assert-Fields (Join-Path $skillsRoot 'ops/role-map.md') @('lead', 'cie', 'cpe', 'cre', 'Handoff')
Assert-Fields (Join-Path $skillsRoot 'ops/skill-supply-chain.md') @('AST10', 'MCP', 'bootstrap', 'MUST')
Assert-Fields (Join-Path $skillsRoot 'references/community-security-skills.md') @('trailofbits', 'agentskills.io', 'MUST', '2026-07')
Assert-Fields (Join-Path $skillsRoot 'reverse-engineering/references\re-agent-workflow.md') @('Triage', 'Static', 'Dynamic', 'Synthesis')
Assert-Fields (Join-Path $skillsRoot 'pentest-tools/references\recon-pipeline.md') @('auth.status', 'network_profile', 'Evidence', 'nuclei')
Assert-Fields (Join-Path $skillsRoot 'docs-generator/references\security-report-templates.md') @('Evidence Chain', 'Findings', 'Path')
Assert-Fields (Join-Path $skillsRoot 'field-journal/_template.md') @('Scope', 'Evidence', 'Finding')
Assert-Fields (Join-Path $skillsRoot 'case-review/SKILL.md') @('ACTION REQUIRED', 'review_case.py', 'Evidence Graph Review')
$vendorRulesPath = Join-Path $skillsRoot 'docs-generator/references\vendor-report-rules.md'
$vendorRulesText = Get-Content $vendorRulesPath -Raw -Encoding UTF8
Assert-Fields (Join-Path $skillsRoot 'docs-generator/SKILL.md') @('vendor-report-rules.md', 'flavor = null', '不强制 IOC/ATT&CK')
Assert-Fields $vendorRulesPath @('flavor = null', 'explicit_malware')
if ($vendorRulesText -match '(?m)逆向工程报告\s*\|\s*默认\s*`malware`') {
Bad 'vendor rules default generic reverse engineering to malware flavor'
} else {
Ok 'vendor rules keep generic reverse engineering flavor-neutral'
}
if ($vendorRulesText -match '先确认 scope 并保全' -and $vendorRulesText -match '不得在证据保全前直接删除文件') {
Ok 'malware remediation preserves evidence before destructive actions'
} else {
Bad 'malware remediation does not require evidence preservation before destructive actions'
}
if ($vendorRulesText -match '(?m)JS/Web 签名逆向报告\s*\|[^\r\n]*malware') {
Bad 'vendor rules route JS signature reports through malware flavor'
} else {
Ok 'vendor rules keep JS signature reports flavor-neutral'
}
Assert-Fields $vendorRulesPath @('skills/ops/evidence-finding-path.md', '来源证据', 'securelist.com/updated-mata', 'www.huorong.cn')
$fieldLog | Set-Content -LiteralPath (Join-Path $ScratchDir 'template-fields.txt') -Encoding UTF8
# --- role map skills exist for primary rows ---
$roleDoc = Get-Content (Join-Path $skillsRoot 'ops/role-map.md') -Raw -Encoding UTF8
foreach ($sk in @('attack-chain', 'pentest-tools', 'ida-reverse', 'docs-generator', 'llm-security')) {
if ($roleDoc -match [regex]::Escape($sk)) { Ok "role-map mentions $sk" } else { Bad "role-map missing $sk" }
}
# --- master-route cases ---
$cases = @(
@{ N = 'dsl'; H = 'dsl vm reverse fireye'; Id = 'R4'; Sub = 'reverse-engineering/dsl-vm-reverse/SKILL.md' },
@{ N = 'apk'; H = 'apk jadx smali reverse'; Id = 'R1'; Sub = 'apk-reverse/SKILL.md' },
@{ N = 'malware'; H = 'malware yara sample analysis'; Id = 'R9'; Sub = 'malware-analysis/SKILL.md' },
@{ N = 'pentest'; H = 'nmap nuclei pentest sqlmap'; Id = 'R11'; Sub = 'pentest-tools/SKILL.md' },
@{ N = 'attack'; H = 'full pentest attack chain from external'; Id = 'R10'; Sub = 'attack-chain/SKILL.md' },
@{ N = 'protocol'; H = 'protobuf custom protocol reverse pcap'; Id = 'R21'; Sub = 'protocol-reverse/SKILL.md' },
@{ N = 'ghidra'; H = 'ghidra headless decompile'; Id = 'R22'; Sub = 'ghidra-reverse/SKILL.md' },
@{ N = 'cloud'; H = 'kubernetes k8s container escape'; Id = 'R23'; Sub = 'cloud-k8s/SKILL.md' },
@{ N = 'ad'; H = 'bloodhound kerberoast active directory'; Id = 'R24'; Sub = 'windows-ad/SKILL.md' },
@{ N = 'forensics'; H = 'volatility memory dump forensics'; Id = 'R25'; Sub = 'digital-forensics/SKILL.md' },
@{ N = 'codeaudit'; H = 'semgrep code audit sast'; Id = 'R26'; Sub = 'code-audit/SKILL.md' },
@{ N = 'hunt'; H = 'threat hunting detection engineering'; Id = 'R27'; Sub = 'threat-hunting/SKILL.md' },
@{ N = 'ot'; H = 'scada plc modbus industrial control'; Id = 'R28'; Sub = 'ot-ics/SKILL.md' },
@{ N = 'wifi'; H = 'wifi aircrack wireless pentest'; Id = 'R29'; Sub = 'wifi-wireless/SKILL.md' },
@{ N = 'extension'; H = 'chrome extension crx reverse'; Id = 'R30'; Sub = 'browser-extension-reverse/SKILL.md' },
@{ N = 'macos'; H = 'macos mach-o codesign reverse'; Id = 'R31'; Sub = 'macos-reverse/SKILL.md' },
@{ N = 'thick'; H = 'thick client electron desktop client'; Id = 'R32'; Sub = 'thick-client/SKILL.md' },
@{ N = 'gorust'; H = 'golang stripped go binary reverse'; Id = 'R33'; Sub = 'go-rust-reverse/SKILL.md' },
@{ N = 'hw'; H = 'uart jtag hardware debug pads'; Id = 'R34'; Sub = 'hardware-security/SKILL.md' },
@{ N = 'db'; H = 'database security mysql postgres redis'; Id = 'R35'; Sub = 'database-security/SKILL.md' },
@{ N = 'email'; H = 'phishing spf dkim dmarc email security'; Id = 'R36'; Sub = 'email-security/SKILL.md' },
@{ N = 'sso'; H = 'saml oidc sso federation'; Id = 'R37'; Sub = 'identity-federation/SKILL.md' },
@{ N = 'sdr'; H = 'sdr hackrf gnu radio rf'; Id = 'R38'; Sub = 'radio-sdr/SKILL.md' }
)
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 | 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 }
$text = Get-Content $scope -Raw -Encoding UTF8
if ($text -notmatch ("primary: {0}" -f [regex]::Escape($c.Id))) { Bad "$($c.N) id want $($c.Id)" } else { Ok "$($c.N) -> $($c.Id)" }
$abs = Join-Path $skillsRoot ($c.Sub -replace '/', [IO.Path]::DirectorySeparatorChar)
if (-not (Test-Path $abs)) { Bad "missing $($c.Sub)" } else { Ok "exists $($c.Sub)" }
}
# default outdir under work
$def = & powershell -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 `
-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)
if ($projectRouteDirs.Count -eq 1 -and (Test-Path (Join-Path $projectRouteDirs[0].FullName 'route-scope.md'))) {
Ok 'explicit ProjectRoot keeps route artifacts in analysis project'
} else {
Bad 'explicit ProjectRoot did not receive route artifacts'
}
$defaultProjectRoot = Join-Path $ScratchDir 'default-analysis-project'
New-Item -ItemType Directory -Force -Path $defaultProjectRoot | Out-Null
$previousLocation = Get-Location
try {
Set-Location -LiteralPath $defaultProjectRoot
$defaultProjectRoute = & powershell -NoProfile -ExecutionPolicy Bypass -File $masterRoute `
-Hint 'radare2 analyze' 2>&1 | Out-String
} finally {
Set-Location -LiteralPath $previousLocation
}
$defaultProjectWork = Join-Path $defaultProjectRoot 'work'
$defaultProjectRoutes = @(Get-ChildItem -LiteralPath $defaultProjectWork -Directory -Filter 'master-route-*' -ErrorAction SilentlyContinue)
if ($defaultProjectRoutes.Count -eq 1 -and (Test-Path (Join-Path $defaultProjectRoutes[0].FullName 'route-scope.md'))) {
Ok 'default route artifacts follow the caller project'
} else {
Bad 'default route artifacts did not follow the caller project'
}
# 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 | 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')) {
$fp = Join-Path $caseRoot $f
if (Test-Path $fp) { Ok "case-init $f" } else { Bad "case-init missing $f" }
}
if (Test-Path (Join-Path $caseRoot 'scope.md')) {
$sc = Get-Content (Join-Path $caseRoot 'scope.md') -Raw -Encoding UTF8
foreach ($k in @('auth', 'network_profile', 'in_scope', 'ready_for_act')) {
if ($sc -match $k) { Ok "case scope has $k" } else { Bad "case scope missing $k" }
}
}
$projectCaseName = 'verify-project-root-' + (Get-Date -Format 'HHmmss')
& powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit `
-Hint 'apk jadx reverse' -CaseName $projectCaseName -PackageRoot $packageRoot `
-ProjectRoot $projectRoot 2>&1 | Out-Null
$projectCaseRoot = Join-Path $projectWork $projectCaseName
if ((Test-Path (Join-Path $projectCaseRoot 'scope.md')) -and
(Test-Path (Join-Path $projectCaseRoot 'timeline.md')) -and
(Test-Path (Join-Path $projectCaseRoot 'workitems.md'))) {
Ok 'explicit ProjectRoot keeps case artifacts in analysis project'
} else {
Bad 'explicit ProjectRoot did not receive case artifacts'
}
$defaultCaseName = 'verify-default-project-' + (Get-Date -Format 'HHmmss')
try {
Set-Location -LiteralPath $defaultProjectRoot
& powershell -NoProfile -ExecutionPolicy Bypass -File $caseInit `
-Hint 'apk jadx reverse' -CaseName $defaultCaseName 2>&1 | Out-Null
} finally {
Set-Location -LiteralPath $previousLocation
}
$defaultCaseRoot = Join-Path (Join-Path $defaultProjectRoot 'work') $defaultCaseName
if ((Test-Path (Join-Path $defaultCaseRoot 'scope.md')) -and
(Test-Path (Join-Path $defaultCaseRoot 'timeline.md')) -and
(Test-Path (Join-Path $defaultCaseRoot 'workitems.md'))) {
Ok 'default case artifacts follow the caller project'
} else {
Bad 'default case artifacts did not follow the caller project'
}
# ghost dsl
foreach ($rel in @('SKILL.md', 'routing.md', 'MASTER-ROUTING.md', 'scripts\master-route.ps1')) {
$p = Join-Path $skillsRoot $rel
if (-not (Test-Path $p)) { continue }
$t = Get-Content $p -Raw -Encoding UTF8
if ($t -match '`dsl-vm-reverse/' -and $t -notmatch 'reverse-engineering/dsl-vm-reverse') {
Bad "ghost dsl path in $rel"
}
}
Ok 'ghost dsl scan done'
# refresh-tool-index parses
$e = $null
[void][System.Management.Automation.Language.Parser]::ParseFile((Join-Path $scriptDir 'refresh-tool-index.ps1'), [ref]$null, [ref]$e)
if ($e -and $e.Count -gt 0) { Bad ("refresh-tool-index parse: {0}" -f $e[0]) } else { Ok 'refresh-tool-index parses' }
# --- bootstrap-manifest parity (skills vs kali) ---
$skillsManifest = Join-Path $scriptDir 'bootstrap-manifest.json'
$kaliManifest = Join-Path $packageRoot 'kali/scripts/bootstrap-manifest.json'
$skillsCaps = @()
if (Test-Path -LiteralPath $skillsManifest) {
$sm = Get-Content -LiteralPath $skillsManifest -Raw -Encoding UTF8 | ConvertFrom-Json
$skillsCaps = @($sm.capabilities | ForEach-Object { $_.name })
if ($skillsCaps.Count -ge 10) { Ok "skills manifest $($skillsCaps.Count) capabilities" } else { Bad 'skills manifest capability count suspicious' }
} else {
Bad 'skills bootstrap-manifest.json missing'
}
if (Test-Path -LiteralPath $kaliManifest) {
$km = Get-Content -LiteralPath $kaliManifest -Raw -Encoding UTF8 | ConvertFrom-Json
$kaliCaps = @($km.capabilities | ForEach-Object { $_.name })
foreach ($missing in ($skillsCaps | Where-Object { $_ -notin $kaliCaps })) {
Bad "kali manifest missing capability: $missing"
}
foreach ($missing in ($kaliCaps | Where-Object { $_ -notin $skillsCaps })) {
Ok "kali-only capability: $missing"
}
} else {
Bad 'kali bootstrap-manifest.json missing'
}
# --- supply-chain pin gate: auto-install download sources MUST be pinned ---
# 统一判定:pinnedVersion / pinnedCommit / pinPolicy 三选一;
# github-release-* 额外接受 assetSha256 / preferApiDigest(GitHub 官方发布资产哈希)。
$pinKinds = @('pip-package', 'npm-mcp', 'npm-global', 'go-install', 'git-clone')
foreach ($mf in @($skillsManifest, $kaliManifest)) {
if (-not (Test-Path -LiteralPath $mf)) { continue }
$mn = Split-Path $mf -Leaf
$mc = Get-Content -LiteralPath $mf -Raw -Encoding UTF8 | ConvertFrom-Json
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 }
'local-http-mcp' { $hasPin = $true } # 本地服务,不下载
'winget-package' { $hasPin = $hasPin } # winget-latest 属于 pinPolicy
'apt-package' { $hasPin = $true } # 发行版仓库自带(Kali 侧)
'docker-image' { $hasPin = $true } # fallback 通道
'manual' { $hasPin = $true } # 手工安装
default { $hasPin = $hasPin }
}
if (-not $hasPin) {
Bad "unpinned auto-install capability: $($cap.name) in $mn ($($cap.bootstrapKind))"
} else {
Ok "pinned $($cap.name) in $mn"
}
}
}
# identity: no FastAPI/React requirement in ops IDENTITY
$id = Get-Content (Join-Path $skillsRoot 'ops/IDENTITY.md') -Raw -Encoding UTF8
if ($id -match '不是|不做|NOT|not a Z3r0|FastAPI|React') { Ok 'identity distinguishes platform' } else { Bad 'identity weak' }
if ($id -match 'tool-index|bootstrap|field-journal|路由') { Ok 'identity keeps reverse-skill DNA' } else { Bad 'identity missing DNA' }
$idCheck = @()
$idCheck += "HEAD packageRoot=$packageRoot"
$idCheck += "fastapi-in-ops-deps=false"
$idCheck -join [Environment]::NewLine | Set-Content (Join-Path $ScratchDir 'identity-check.txt') -Encoding UTF8
Ok 'identity-check written'
Write-Host "Scratch=$ScratchDir"
if ($fail.Count -gt 0) {
Write-Host ("FAILED {0}" -f $fail.Count) -ForegroundColor Red
$fail | ForEach-Object { Write-Host " - $_" }
$fail | Set-Content (Join-Path $ScratchDir 'failures.txt') -Encoding UTF8
exit 1
}
Write-Host 'ALL ROUTING COHERENCE CHECKS PASSED' -ForegroundColor Green
'ALL ROUTING COHERENCE CHECKS PASSED' | Set-Content (Join-Path $ScratchDir 'verify.txt') -Encoding UTF8
exit 0