reverse-skills-max: initial public release (restored, redacted, hardened)
This commit is contained in:
200
reverse-skill-repo/skills/scripts/append-evidence.ps1
Normal file
200
reverse-skill-repo/skills/scripts/append-evidence.ps1
Normal 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
|
||||
328
reverse-skill-repo/skills/scripts/bootstrap-manifest.json
Normal file
328
reverse-skill-repo/skills/scripts/bootstrap-manifest.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
1233
reverse-skill-repo/skills/scripts/bootstrap-reverse.ps1
Normal file
1233
reverse-skill-repo/skills/scripts/bootstrap-reverse.ps1
Normal file
File diff suppressed because it is too large
Load Diff
808
reverse-skill-repo/skills/scripts/bootstrap-reverse.sh
Normal file
808
reverse-skill-repo/skills/scripts/bootstrap-reverse.sh
Normal 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"
|
||||
102
reverse-skill-repo/skills/scripts/case-guard.ps1
Normal file
102
reverse-skill-repo/skills/scripts/case-guard.ps1
Normal 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
|
||||
128
reverse-skill-repo/skills/scripts/case-guard.sh
Normal file
128
reverse-skill-repo/skills/scripts/case-guard.sh
Normal 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
|
||||
320
reverse-skill-repo/skills/scripts/case-init.ps1
Normal file
320
reverse-skill-repo/skills/scripts/case-init.ps1
Normal 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'
|
||||
}
|
||||
343
reverse-skill-repo/skills/scripts/case-init.sh
Normal file
343
reverse-skill-repo/skills/scripts/case-init.sh
Normal 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
|
||||
124
reverse-skill-repo/skills/scripts/extract-summaries.ps1
Normal file
124
reverse-skill-repo/skills/scripts/extract-summaries.ps1
Normal 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
|
||||
1040
reverse-skill-repo/skills/scripts/lib/ToolDiscovery.ps1
Normal file
1040
reverse-skill-repo/skills/scripts/lib/ToolDiscovery.ps1
Normal file
File diff suppressed because it is too large
Load Diff
33
reverse-skill-repo/skills/scripts/lib/WorkRoot.ps1
Normal file
33
reverse-skill-repo/skills/scripts/lib/WorkRoot.ps1
Normal 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
|
||||
}
|
||||
174
reverse-skill-repo/skills/scripts/master-route.ps1
Normal file
174
reverse-skill-repo/skills/scripts/master-route.ps1
Normal 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
|
||||
166
reverse-skill-repo/skills/scripts/master-route.sh
Normal file
166
reverse-skill-repo/skills/scripts/master-route.sh
Normal 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
|
||||
178
reverse-skill-repo/skills/scripts/refresh-tool-index.ps1
Normal file
178
reverse-skill-repo/skills/scripts/refresh-tool-index.ps1
Normal 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)"
|
||||
|
||||
416
reverse-skill-repo/skills/scripts/refresh-tool-index.sh
Normal file
416
reverse-skill-repo/skills/scripts/refresh-tool-index.sh
Normal 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[@]}"
|
||||
77
reverse-skill-repo/skills/scripts/scan-leaks.ps1
Normal file
77
reverse-skill-repo/skills/scripts/scan-leaks.ps1
Normal 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 }
|
||||
183
reverse-skill-repo/skills/scripts/smoke.ps1
Normal file
183
reverse-skill-repo/skills/scripts/smoke.ps1
Normal 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
|
||||
405
reverse-skill-repo/skills/scripts/test-p0-friction.ps1
Normal file
405
reverse-skill-repo/skills/scripts/test-p0-friction.ps1
Normal 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
|
||||
95
reverse-skill-repo/skills/scripts/test-routing.ps1
Normal file
95
reverse-skill-repo/skills/scripts/test-routing.ps1
Normal 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
|
||||
76
reverse-skill-repo/skills/scripts/test-routing.sh
Normal file
76
reverse-skill-repo/skills/scripts/test-routing.sh
Normal 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
|
||||
@@ -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
|
||||
125
reverse-skill-repo/skills/scripts/update-star-history.ps1
Normal file
125
reverse-skill-repo/skills/scripts/update-star-history.ps1
Normal 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
|
||||
86
reverse-skill-repo/skills/scripts/verify-doc-facts.ps1
Normal file
86
reverse-skill-repo/skills/scripts/verify-doc-facts.ps1
Normal 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 }
|
||||
424
reverse-skill-repo/skills/scripts/verify-routing-coherence.ps1
Normal file
424
reverse-skill-repo/skills/scripts/verify-routing-coherence.ps1
Normal 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
|
||||
Reference in New Issue
Block a user