diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b7dcb7..cdd883d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ Versioning follows [Semantic Versioning](https://semver.org/). ### Added +- `case-review/`: read-only Evidence Graph Review with scope, timeline, work item, Finding, Path, and optional SHA-256 fixity checks - Domain skills R21–R27, R29–R30: `protocol-reverse`, `ghidra-reverse`, `cloud-k8s`, `windows-ad`, `digital-forensics`, `code-audit`, `threat-hunting`, `wifi-wireless`, `browser-extension-reverse` - High-quality skills R28, R31–R38: `ot-ics`, `macos-reverse`, `thick-client`, `go-rust-reverse`, `hardware-security`, `database-security`, `email-security`, `identity-federation`, `radio-sdr` - Wired into `MASTER-ROUTING.md`, `master-route.ps1`, routing tables, domain map, role-map, coherence tests diff --git a/README.md b/README.md index dea137b..50efe9b 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,7 @@ Platform-specific docs: | Malware / YARA | `skills/malware-analysis/` | | Penetration testing / scanning | `skills/pentest-tools/` | | Attack chain / red-team orchestration | `skills/attack-chain/` | +| Case evidence review / report handoff | `skills/case-review/` | | CTF competition | `CTF-Sandbox-Orchestrator/` (40+ sub-skills) | | Firmware / IoT | `skills/firmware-pentest/` | | Patch diff / N-day | `skills/patch-diff-exploit/` | @@ -161,6 +162,7 @@ Platform-specific docs: | [skills/tool-index.md](skills/tool-index.md) | Local tool status (auto-generated) | | [skills/scripts/master-route.ps1](skills/scripts/master-route.ps1) | One-shot PRIMARY triage | | [skills/scripts/case-init.ps1](skills/scripts/case-init.ps1) | Case dir: scope / timeline / workitems | +| [skills/case-review/](skills/case-review/) | Read-only Evidence graph review and artifact fixity checks | | [skills/ops/](skills/ops/) | Scope, Evidence chain, roles, timeline (skill-router form) | ### Repository layout @@ -248,4 +250,4 @@ Special thanks to the OLLVM deobfuscation ecosystem contributors and everyone wh - **Email:** [24781737@qq.com](mailto:24781737@qq.com) - **Discord:** -[reverse-skill](https://discord.gg/TECd3bMRR) \ No newline at end of file +[reverse-skill](https://discord.gg/TECd3bMRR) diff --git a/README_AI.md b/README_AI.md index f1e59d0..7176020 100644 --- a/README_AI.md +++ b/README_AI.md @@ -41,7 +41,8 @@ AI Community: https://linux.do 7. **Ops gate (MUST):** `powershell -File skills/scripts/case-init.ps1 -Hint ""` (or hand-write `work//scope.md` per `skills/ops/scope-contract.md`). Set `auth.status=granted` + `network_profile` before any target ACT. Evidence chain: `skills/ops/evidence-finding-path.md`. Roles: `skills/ops/role-map.md`. Identity: `skills/ops/IDENTITY.md`. 8. Open PRIMARY SKILL.md → execute ACTION REQUIRED. Append timeline/workitems under the case dir. -9. Continue the behavior chain → report via docs-generator + field-journal. +9. Before report handoff, run `python3 skills/case-review/scripts/review_case.py work/ --verify-hashes --strict` and resolve every error. +10. Continue the behavior chain → report via docs-generator + field-journal. ``` ### Platform deployment routing table diff --git a/README_zh.md b/README_zh.md index 44c3e57..56cdb85 100644 --- a/README_zh.md +++ b/README_zh.md @@ -134,6 +134,7 @@ git clone https://github.com/zhaoxuya520/reverse-skill.git | 恶意软件 / YARA | `skills/malware-analysis/` | | 渗透测试 / 漏洞扫描 | `skills/pentest-tools/` | | 攻击链 / 红队编排 | `skills/attack-chain/` | +| Case 证据审查 / 报告交接 | `skills/case-review/` | | CTF 竞赛 | `CTF-Sandbox-Orchestrator/` (40+ 子技能) | | 固件 / IoT | `skills/firmware-pentest/` | | 补丁差分 / N-day | `skills/patch-diff-exploit/` | @@ -157,6 +158,7 @@ git clone https://github.com/zhaoxuya520/reverse-skill.git | [skills/tool-index.md](skills/tool-index.md) | 本机工具索引(自动生成) | | [skills/scripts/master-route.ps1](skills/scripts/master-route.ps1) | 一键分诊 | | [skills/scripts/case-init.ps1](skills/scripts/case-init.ps1) | 作战 case 目录(scope/timeline) | +| [skills/case-review/](skills/case-review/) | 只读 Evidence 图审查与 artifact fixity 校验 | | [skills/ops/](skills/ops/) | Scope / 证据链 / 角色 / 时间线 / skill 供应链安全 | | [skills/references/community-security-skills.md](skills/references/community-security-skills.md) | 社区安全 skill 生态对照(借鉴不并库) | @@ -246,4 +248,4 @@ git clone https://github.com/zhaoxuya520/reverse-skill.git ## 联系方式 -- **邮箱**:[24781737@qq.com](mailto:24781737@qq.com) \ No newline at end of file +- **邮箱**:[24781737@qq.com](mailto:24781737@qq.com) diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a27c8ad..0d7add5 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -26,7 +26,8 @@ flowchart TD Execute --> TaskDone{任务完成?} TaskDone -->|否| Execute - TaskDone -->|是| GenReport[调用 docs-generator
生成报告 + 图表] + TaskDone -->|是| ReviewCase[调用 case-review
校验证据图] + ReviewCase --> GenReport[调用 docs-generator
生成报告 + 图表] GenReport --> WriteJournal[回写 field-journal
经验沉淀] WriteJournal --> UpdateIndex[更新索引/路由/manifest] @@ -76,6 +77,7 @@ flowchart LR subgraph 输出层 Docs[docs-generator
报告生成] Diagram[diagram-generator
图表生成] + Review[case-review
Evidence graph audit] Journal[field-journal
自动进化] end @@ -99,7 +101,8 @@ flowchart LR Bootstrap --> Discovery --> ToolIndex - APK & IDA & R2 & Pentest & JS -->|任务完成| Docs + APK & IDA & R2 & Pentest & JS -->|任务完成| Review + Review --> Docs Docs --> Diagram Docs --> Journal ``` diff --git a/docs/PACKAGE-SECURITY-AUDIT.md b/docs/PACKAGE-SECURITY-AUDIT.md index 8531948..8b96d8f 100644 --- a/docs/PACKAGE-SECURITY-AUDIT.md +++ b/docs/PACKAGE-SECURITY-AUDIT.md @@ -95,6 +95,7 @@ skills/radare2/scripts/* skills/ida-reverse/scripts/* skills/browser-automation/scripts/* skills/diagram-generator/scripts/*.py +skills/case-review/scripts/*.py kali/scripts/* burp-mcp-full/mcp-bridge.js (+ Java 扩展源) ``` diff --git a/skills/MASTER-ROUTING.md b/skills/MASTER-ROUTING.md index 3d67b42..b98c38a 100644 --- a/skills/MASTER-ROUTING.md +++ b/skills/MASTER-ROUTING.md @@ -27,6 +27,7 @@ powershell -File skills\scripts\smoke.ps1 powershell -File skills\scripts\case-guard.ps1 -CaseRoot work\my-case # Evidence 追加 powershell -File skills\scripts\append-evidence.ps1 -CaseRoot work\my-case -Id E-001 -Title "..." -ReproCommand "..." +python3 skills/case-review/scripts/review_case.py work/ --verify-hashes --strict ``` ## 作战契约(ops) @@ -36,6 +37,7 @@ powershell -File skills\scripts\append-evidence.ps1 -CaseRoot work\my-case -Id E | `ops/IDENTITY.md` | 我们是路由包,不是 Z3r0 平台 | | `ops/scope-contract.md` | 启动门槛 | | `ops/evidence-finding-path.md` | 证据链 | +| `case-review/SKILL.md` | Evidence 图审查与报告交接 | | `ops/role-map.md` | 角色→skill | | `ops/timeline-workitem.md` | 时间线与覆盖 | | `ops/sandbox-profile.md` | 工具对照 | @@ -69,6 +71,7 @@ powershell -File skills\scripts\append-evidence.ps1 -CaseRoot work\my-case -Id E | **R19** | 浏览器/桌面自动化 | `browser-automation/` | | **R20** | 报告 / writeup | `docs-generator/` | | **R39** | 图表 / Mermaid / Graphviz / PlantUML / 架构图 | `diagram-generator/` | +| **R40** | Case / Evidence 图审查 | `case-review/` | | **R21** | 协议 / Protobuf / PCAP 协议 | `protocol-reverse/` | | **R22** | Ghidra / 开源反编译 | `ghidra-reverse/` | | **R23** | 云 / 容器 / K8s | `cloud-k8s/` | diff --git a/skills/SKILL.md b/skills/SKILL.md index eedef2e..db9dfa3 100644 --- a/skills/SKILL.md +++ b/skills/SKILL.md @@ -39,6 +39,7 @@ description: Routes reverse engineering, exploitation, penetration testing, malw | **radare2 分析** | `radare2/` | CLI 二进制侦察、反汇编、patch:r2 / rabin2 / rasm2 / radiff2 | | **CTF 竞赛全栈** | `../CTF-Sandbox-Orchestrator/` | 40+ 子技能:Web/逆向/Pwn/云/容器/AD/取证/隐写/移动端/密码学,由总控统一编排 | | **技术文档编写** | `docs-generator/` | 任务完成后自动生成逆向报告、渗透报告、CTF writeup、签名逆向报告 | +| **Evidence 图审查** | `case-review/` | 校验 scope、Evidence→Finding→Path 可追溯性、workitems、timeline 与 artifact hash | | **浏览器与桌面自动化** | `browser-automation/` | 浏览器操作(Playwright)+ Windows 桌面应用操作(OpenReverse UIA/CUA)+ 网络观察 | | **跨版本符号迁移** | `binary-diff/` | 有旧版符号迁移到新版、缺 PDB 推导、程序更新后批量迁移函数名 | | **N-day 补丁差分→利用** | `patch-diff-exploit/` | 从厂商补丁定位漏洞点、写 PoC、N-day 武器化(与 binary-diff 分工:本 skill 偏攻击侧) | diff --git a/skills/case-review/SKILL.md b/skills/case-review/SKILL.md new file mode 100644 index 0000000..38dcf89 --- /dev/null +++ b/skills/case-review/SKILL.md @@ -0,0 +1,157 @@ +--- +name: case-review +description: Reviews a reverse-skill case package for scope readiness, Evidence to Finding to Path traceability, work item coverage, timeline references, and optional artifact hash integrity before report handoff. +--- + +# Evidence Graph Review + +Use this skill when a reverse engineering, forensics, CTF, or authorized security case needs a defensible handoff. It audits the existing `work//` package without changing the case or touching a target. + +## Scope + +This skill covers: + +- Scope metadata and target-activity readiness +- Evidence record structure and reproducibility fields +- References from work items and timeline entries to Evidence +- Structured Findings and Paths in report Markdown +- Optional SHA-256 verification for case-local artifacts +- A Markdown or JSON review result for a report handoff + +It MUST NOT perform reconnaissance, exploitation, dynamic instrumentation, or target changes. Those actions belong to the routed analysis skill and require the case scope gate. + +## ACTION REQUIRED + +1. `NOW`: read `../field-journal/precedent-reverse.md` and confirm that this is a review of an existing authorized case package. +2. `NOW`: confirm the case path and choose read-only review mode. +3. `NEXT`: read `../tool-index.md`; this skill uses only Python 3 standard library and does not require bootstrap. +4. `NEXT`: run `python3 scripts/review_case.py --format markdown`. +5. `ACT`: resolve every error, then rerun the review before claiming a handoff is complete. + +## Tool dependencies + +| Tool | Required | Purpose | Auto-bootstrap | +|------|----------|---------|---------------| +| Python 3.9+ | Yes | Runs the read-only case review script | No, use the platform Python installation | + +No network access or third-party package is required. + +## Workflow + +### Phase 1: Intake + +Run the review against the existing case directory: + +```bash +python3 skills/case-review/scripts/review_case.py work/ --format markdown +``` + +Confirm that `scope.md`, `timeline.md`, `workitems.md`, and `evidence/` are present. A non-strict review reports scope warnings while a strict review treats warnings as handoff blockers. + +## 建议下一步(选一个编号) + +1. 修复 scope.md 中的授权、范围或 network_profile 字段 +2. 继续检查 Evidence 记录的可复现命令和来源 +3. 导出当前 review 结果并附到阶段性报告 +4. 换 JSON 输出接入 CI 或其他审查工具 +5. 暂停,先确认审查范围 + +### Phase 2: Traceability + +Review the checks for: + +- Evidence IDs that do not exist +- Findings without `evidence_ids` +- Paths without an allowed `path_type` or Evidence reference +- Work items and timeline entries pointing to unknown Evidence +- Unlinked Evidence records +- Validated Findings with low confidence + +An offline observation may use `repro_command: n/a` only when its `notes` field explicitly documents the offline limitation. + +Use JSON when another tool needs stable fields: + +```bash +python3 skills/case-review/scripts/review_case.py work/ --format json +``` + +## 建议下一步(选一个编号) + +1. 补写缺失的 Evidence,并保留原始命令 +2. 将候选 Finding 绑定到 Evidence 后重新审查 +3. 为调用链或攻击链补充 P-id 和 Path 步骤 +4. 生成 Markdown handoff summary +5. 换回 PRIMARY skill 继续分析 + +### Phase 3: Fixity verification + +When an Evidence record contains both `content_hash` and `artifact_path`, verify the case-local artifact: + +```bash +python3 skills/case-review/scripts/review_case.py work/ --verify-hashes --strict +``` + +The script accepts `sha256:<64 hex characters>` and checks that the artifact remains inside the case root. A hash mismatch is a hard failure. + +The PowerShell Evidence helper can record a hash while appending a record: + +```powershell +powershell -File skills/scripts/append-evidence.ps1 -CaseRoot work\ -Id E-001 -Title "Sample hash" -ReproCommand "sha256sum evidence/sample.bin" -ArtifactPath "evidence\sample.bin" +``` + +## 建议下一步(选一个编号) + +1. 修复 hash mismatch 或替换已污染的工作副本 +2. 为未固定的原始文件补充 SHA-256 和 artifact_path +3. 继续进入报告生成阶段 +4. 导出 JSON 结果供 CI 保存 +5. 暂停并请求人工复核 + +### Phase 4: Handoff + +Use strict mode before a final report or specialist handoff: + +```bash +python3 skills/case-review/scripts/review_case.py work/ --strict --format markdown > work//report/case-review.md +``` + +The command is read-only with respect to the case unless shell redirection is explicitly used to save its output. The review is not legal advice and does not replace organizational evidence handling procedures. + +## 建议下一步(选一个编号) + +1. 将通过的 review 结果交给 `docs-generator/` 生成正式报告 +2. 回到 PRIMARY skill 补齐新的分析证据 +3. 归档 Markdown 和 JSON review 结果 +4. 暂停并请求人工复核 + +## Language behavior contract + +- Internal reasoning, tool selection, and phase control: English. +- User-visible messages, section labels, reports, and next-step menus: Chinese unless the user requests another language. +- Default bilingual labels place Chinese first and English second, separated by `/`. + +## Bootstrap boundary + +This skill has no third-party dependency. If Python 3 is unavailable, the only allowed recovery action is the repository bootstrap path when a Python capability is registered for the current platform. If no such capability is registered, stop and report the missing runtime. Do not guess executable paths, download packages, or perform a manual install from inside this skill. + +## Routing context + +**Upstream entry**: any reverse, forensics, CTF, or authorized security skill that has produced a case package. + +**Downstream exit**: `docs-generator/` for a formal report, or the original PRIMARY skill when the graph is incomplete. + +**Related modules**: `ops/evidence-finding-path.md`, `ops/timeline-workitem.md`, `digital-forensics/`, `reverse-engineering/`, and `docs-generator/`. + +## References + +- [NIST SP 800-86: Guide to Integrating Forensic Techniques into Incident Response](https://csrc.nist.gov/pubs/sp/800/86/final) +- [SWGDE Best Practices for Computer Forensic Acquisitions](https://www.swgde.org/documents/published-complete-listing/17-f-002-2-1/) +- [SWGDE Best Practices for Archiving Digital and Multimedia Evidence](https://www.swgde.org/documents/published-complete-listing/19-f-003-best-practices-for-archiving-digital-and-multimedia-evidence/) + +## 任务完成自检 + +- [ ] 我是否审查了 scope.md、timeline.md、workitems.md 和 evidence/? +- [ ] 所有 Finding 是否引用了现存 Evidence? +- [ ] 所有 Path 是否包含合法 path_type 和 Evidence 引用? +- [ ] 是否执行了 hash verification,或记录了未执行原因? +- [ ] 是否以 strict 模式重新运行并保存了 review 结果? diff --git a/skills/case-review/scripts/review_case.py b/skills/case-review/scripts/review_case.py new file mode 100644 index 0000000..dc25d74 --- /dev/null +++ b/skills/case-review/scripts/review_case.py @@ -0,0 +1,451 @@ +import argparse +import hashlib +import json +import os +import re +import sys +from datetime import datetime, timezone +from pathlib import Path + + +EVIDENCE_ID = re.compile(r"\bE-[A-Za-z0-9][A-Za-z0-9_-]*\b") +WORKITEM_ID = re.compile(r"\bWI-[A-Za-z0-9][A-Za-z0-9_-]*\b") +FINDING_ID = re.compile(r"\bF-[A-Za-z0-9][A-Za-z0-9_-]*\b") +PATH_ID = re.compile(r"\bP-[A-Za-z0-9][A-Za-z0-9_-]*\b") +FIELD_LINE = re.compile(r"^\s*-\s+([A-Za-z0-9_]+):\s*(.*)$") +SECTION_HEADING = re.compile(r"^#{2,6}\s+(.+?)\s*$") +EVIDENCE_HEADING = re.compile(r"^###\s+(E-[A-Za-z0-9][A-Za-z0-9_-]*)\b", re.MULTILINE) +FINDING_HEADING = re.compile(r"^###\s+(F-[A-Za-z0-9][A-Za-z0-9_-]*)\b", re.MULTILINE) +PATH_HEADING = re.compile(r"^###\s+(P-[A-Za-z0-9][A-Za-z0-9_-]*)\b", re.MULTILINE) + +SEVERITIES = {"critical", "high", "medium", "low", "info", "n/a", "n/a_re"} +EVIDENCE_STATUSES = {"observed", "candidate", "validated", "false_positive", "accepted_risk"} +WORKITEM_STATUSES = {"pending", "in_progress", "blocked", "done", "cancelled"} +PATH_TYPES = {"attack", "callflow", "solve"} +NETWORK_MODES = {"offline", "lab_only", "authorized_target_only", "unrestricted_lab"} + + +def field_value(text, name): + lines = text.splitlines() + for index, line in enumerate(lines): + match = FIELD_LINE.match(line) + if not match or match.group(1) != name: + continue + value = match.group(2).strip() + if value != "|": + return value + block = [] + for continuation in lines[index + 1:]: + if FIELD_LINE.match(continuation): + break + if SECTION_HEADING.match(continuation): + break + if continuation.strip(): + block.append(continuation.strip()) + return "\n".join(block).strip() + return "" + + +def section_text(text, title): + pattern = re.compile( + r"(?ms)^##\s+" + re.escape(title) + r"\s*$\n?(.*?)(?=^##\s|\Z)" + ) + match = pattern.search(text) + return match.group(1) if match else "" + + +def ids_in(value, pattern): + return sorted(set(pattern.findall(value or ""))) + + +def issue(issues, level, code, message, path=""): + issues.append({"level": level, "code": code, "message": message, "path": path}) + + +def relative_path(root, path): + try: + return str(path.resolve().relative_to(root.resolve())) + except ValueError: + return str(path) + + +def split_table_row(line): + stripped = line.strip() + if not stripped.startswith("|") or not stripped.endswith("|"): + return [] + return [cell.strip() for cell in stripped[1:-1].split("|")] + + +def parse_workitems(root, issues): + path = root / "workitems.md" + records = {} + references = [] + if not path.is_file(): + issue(issues, "error", "workitems.missing", "workitems.md is missing", "workitems.md") + return records, references + + for line_number, line in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1): + cells = split_table_row(line) + if len(cells) < 8 or not WORKITEM_ID.fullmatch(cells[0]): + continue + workitem_id = cells[0] + status = cells[5].lower() + if status not in WORKITEM_STATUSES: + issue( + issues, + "error", + "workitem.status", + "unsupported work item status: " + status, + "workitems.md:" + str(line_number), + ) + evidence_ids = ids_in(cells[6], EVIDENCE_ID) + records[workitem_id] = {"status": status, "evidence_ids": evidence_ids} + references.extend((evidence_id, "workitems.md:" + str(line_number)) for evidence_id in evidence_ids) + + if not records: + issue(issues, "warning", "workitems.empty", "no work item rows were found", "workitems.md") + return records, references + + +def parse_timeline(root, issues): + path = root / "timeline.md" + references = [] + events = 0 + if not path.is_file(): + issue(issues, "error", "timeline.missing", "timeline.md is missing", "timeline.md") + return events, references + + lines = path.read_text(encoding="utf-8-sig").splitlines() + for index, line in enumerate(lines): + if not line.startswith("## ") or "|" not in line: + continue + events += 1 + end = len(lines) + for next_index in range(index + 1, len(lines)): + if lines[next_index].startswith("## "): + end = next_index + break + body = "\n".join(lines[index:end]) + evidence_ids = ids_in(field_value(body, "evidence_ids"), EVIDENCE_ID) + references.extend((evidence_id, "timeline.md:" + str(index + 1)) for evidence_id in evidence_ids) + + if events == 0: + issue(issues, "warning", "timeline.empty", "no append-only timeline events were found", "timeline.md") + return events, references + + +def parse_scope(root, issues, strict): + path = root / "scope.md" + result = {"auth_status": "", "network_mode": "", "ready_for_act": "", "assets": []} + if not path.is_file(): + issue(issues, "error", "scope.missing", "scope.md is missing", "scope.md") + return result + + text = path.read_text(encoding="utf-8-sig") + auth = section_text(text, "auth") + scope_section = section_text(text, "in_scope") + network = section_text(text, "network_profile") + signoff = section_text(text, "signoff") + result["auth_status"] = field_value(auth, "status").lower() + result["network_mode"] = field_value(network, "mode").lower() + result["ready_for_act"] = field_value(signoff, "ready_for_act").lower() + + assets_match = re.search(r"(?ms)^\s*-\s+assets:\s*\n(?P.*?)(?=^\s*-\s+[A-Za-z0-9_]+:|\Z)", scope_section) + if assets_match: + result["assets"] = [ + line.strip()[2:].strip() + for line in assets_match.group("body").splitlines() + if re.match(r"^\s+-\s+\S+", line) and line.strip()[2:].strip() != "[]" + ] + + if not result["auth_status"]: + issue(issues, "error", "scope.auth_missing", "auth.status is missing", "scope.md") + elif result["auth_status"] not in {"pending", "granted", "denied", "unknown"}: + issue(issues, "error", "scope.auth_invalid", "unsupported auth.status: " + result["auth_status"], "scope.md") + + if not result["network_mode"]: + issue(issues, "error", "scope.network_missing", "network_profile.mode is missing", "scope.md") + elif result["network_mode"] not in NETWORK_MODES: + issue(issues, "error", "scope.network_invalid", "unsupported network mode: " + result["network_mode"], "scope.md") + + if not result["ready_for_act"]: + issue(issues, "error", "scope.ready_missing", "signoff.ready_for_act is missing", "scope.md") + elif result["ready_for_act"] not in {"true", "false"}: + issue(issues, "error", "scope.ready_invalid", "ready_for_act must be true or false", "scope.md") + + if result["network_mode"] != "offline" and not result["assets"]: + issue(issues, "error", "scope.assets_missing", "in_scope.assets is empty for a network case", "scope.md") + + if result["auth_status"] != "granted" or result["ready_for_act"] != "true": + message = "scope is not ready for target ACT" + if strict and result["network_mode"] != "offline": + issue(issues, "error", "scope.not_ready", message, "scope.md") + else: + issue(issues, "warning", "scope.not_ready", message, "scope.md") + return result + + +def report_sections(text, heading_pattern): + matches = list(heading_pattern.finditer(text)) + sections = [] + for index, match in enumerate(matches): + end = len(text) + for next_match in re.finditer(r"(?m)^#{1,3}\s+", text[match.end():]): + end = match.end() + next_match.start() + break + sections.append((match.group(1), text[match.start():end])) + return sections + + +def parse_reports(root, issues): + finding_records = [] + path_records = [] + references = [] + report_root = root / "report" + report_files = sorted(report_root.rglob("*.md")) if report_root.is_dir() else [] + if not report_files: + issue(issues, "warning", "report.missing", "no Markdown report was found", "report") + return finding_records, path_records, references + + for report_path in report_files: + text = report_path.read_text(encoding="utf-8-sig") + for finding_id, body in report_sections(text, FINDING_HEADING): + status = field_value(body, "status").lower() + confidence = field_value(body, "confidence").lower() + evidence_ids = ids_in(field_value(body, "evidence_ids"), EVIDENCE_ID) + required = ("severity", "evidence_ids", "confidence", "location", "status") + for field in required: + if not field_value(body, field): + issue(issues, "error", "finding.field_missing", finding_id + " is missing " + field, relative_path(root, report_path)) + if status not in {"candidate", "validated", "false_positive", "accepted_risk"}: + issue(issues, "error", "finding.status", finding_id + " has unsupported status", relative_path(root, report_path)) + if status == "validated" and confidence == "low": + issue(issues, "error", "finding.confidence", finding_id + " is validated with low confidence", relative_path(root, report_path)) + if not evidence_ids: + issue(issues, "error", "finding.evidence_missing", finding_id + " has no evidence_ids", relative_path(root, report_path)) + references.extend((evidence_id, relative_path(root, report_path)) for evidence_id in evidence_ids) + finding_records.append({"id": finding_id, "status": status, "evidence_ids": evidence_ids, "path": relative_path(root, report_path)}) + + for path_id, body in report_sections(text, PATH_HEADING): + path_type = field_value(body, "path_type").lower() + evidence_ids = ids_in(body, EVIDENCE_ID) + if path_type not in PATH_TYPES: + issue(issues, "error", "path.type", path_id + " has unsupported path_type", relative_path(root, report_path)) + if not evidence_ids: + issue(issues, "error", "path.evidence_missing", path_id + " has no evidence reference", relative_path(root, report_path)) + references.extend((evidence_id, relative_path(root, report_path)) for evidence_id in evidence_ids) + path_records.append({"id": path_id, "path_type": path_type, "evidence_ids": evidence_ids, "path": relative_path(root, report_path)}) + return finding_records, path_records, references + + +def normalize_hash(value): + normalized = value.strip().lower() + if normalized.startswith("sha256:"): + normalized = normalized[7:] + if re.fullmatch(r"[0-9a-f]{64}", normalized): + return normalized + return "" + + +def is_within(root, path): + try: + return os.path.commonpath([str(root), str(path)]) == str(root) + except ValueError: + return False + + +def sha256_file(path): + digest = hashlib.sha256() + with path.open("rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def verify_artifact(root, artifact_path, expected_hash, issues, display_path): + if not artifact_path: + issue(issues, "warning", "artifact.path_missing", "content_hash is recorded without artifact_path", display_path) + return + candidate = Path(artifact_path) + if not candidate.is_absolute(): + candidate = root / candidate + candidate = candidate.resolve() + if not is_within(root.resolve(), candidate): + issue(issues, "warning", "artifact.outside_case", "artifact_path points outside the case root", display_path) + return + if not candidate.is_file(): + issue(issues, "error", "artifact.missing", "artifact_path does not exist", display_path) + return + digest = sha256_file(candidate) + if digest != expected_hash: + issue(issues, "error", "artifact.hash_mismatch", "artifact SHA-256 does not match content_hash", display_path) + + +def parse_evidence(root, workitems, issues, verify_hashes): + evidence_root = root / "evidence" + records = {} + references = [] + if not evidence_root.is_dir(): + issue(issues, "error", "evidence.dir_missing", "evidence directory is missing", "evidence") + return records, references + + evidence_files = sorted(path for path in evidence_root.glob("E-*.md") if path.name != "INDEX.md") + if not evidence_files: + issue(issues, "warning", "evidence.empty", "no Evidence records were found", "evidence") + + for path in evidence_files: + evidence_id = path.stem + text = path.read_text(encoding="utf-8-sig") + heading = EVIDENCE_HEADING.search(text) + if not heading: + issue(issues, "error", "evidence.heading_missing", "Evidence record has no matching heading", relative_path(root, path)) + elif heading.group(1) != evidence_id: + issue(issues, "error", "evidence.heading_mismatch", "Evidence heading does not match its filename", relative_path(root, path)) + + severity = field_value(text, "severity").lower() + status = field_value(text, "status").lower() + repro_command = field_value(text, "repro_command") + notes = field_value(text, "notes").lower() + linked_workitems = ids_in(field_value(text, "linked_workitem"), WORKITEM_ID) + content_hash_value = field_value(text, "content_hash") + artifact_path = field_value(text, "artifact_path") + if severity not in SEVERITIES: + issue(issues, "error", "evidence.severity", "unsupported severity: " + severity, relative_path(root, path)) + if status not in EVIDENCE_STATUSES: + issue(issues, "error", "evidence.status", "unsupported status: " + status, relative_path(root, path)) + offline_note = any(marker in notes for marker in ("offline", "离线", "not applicable")) + if not repro_command or (repro_command.lower() in {"n/a", "n/a_re"} and not offline_note): + issue(issues, "error", "evidence.repro_missing", "Evidence requires a reproducible command or documented offline limitation", relative_path(root, path)) + expected_hash = normalize_hash(content_hash_value) if content_hash_value.lower() != "n/a" else "" + if content_hash_value.lower() != "n/a" and not expected_hash: + issue(issues, "error", "evidence.hash_invalid", "content_hash must be SHA-256", relative_path(root, path)) + if verify_hashes and expected_hash: + verify_artifact(root, artifact_path, expected_hash, issues, relative_path(root, path)) + for workitem_id in linked_workitems: + if workitem_id not in workitems: + issue(issues, "error", "evidence.workitem_missing", "linked work item does not exist: " + workitem_id, relative_path(root, path)) + records[evidence_id] = {"severity": severity, "status": status, "artifact_path": artifact_path} + + return records, references + + +def build_traceability(evidence_ids, references): + graph = {evidence_id: {"workitems": 0, "timeline": 0, "reports": 0} for evidence_id in sorted(evidence_ids)} + for evidence_id, source in references: + if evidence_id not in graph: + continue + if source.startswith("workitems.md:"): + graph[evidence_id]["workitems"] += 1 + elif source.startswith("timeline.md:"): + graph[evidence_id]["timeline"] += 1 + else: + graph[evidence_id]["reports"] += 1 + return graph + + +def review_case(case_root, strict=False, verify_hashes=False): + root = Path(case_root).expanduser().resolve() + issues = [] + if not root.is_dir(): + issue(issues, "error", "case.missing", "case root does not exist", str(root)) + return { + "status": "FAIL", + "case_root": str(root), + "generated_at": datetime.now(timezone.utc).isoformat(), + "summary": {"errors": 1, "warnings": 0, "evidence": 0, "workitems": 0, "timeline_events": 0, "findings": 0, "paths": 0}, + "issues": issues, + "traceability": {}, + } + + workitems, workitem_refs = parse_workitems(root, issues) + timeline_events, timeline_refs = parse_timeline(root, issues) + scope = parse_scope(root, issues, strict) + evidence, evidence_refs = parse_evidence(root, workitems, issues, verify_hashes) + findings, paths, report_refs = parse_reports(root, issues) + references = workitem_refs + timeline_refs + evidence_refs + report_refs + + for evidence_id, source in references: + if evidence_id not in evidence: + issue(issues, "error", "reference.unknown_evidence", "reference points to missing Evidence: " + evidence_id, source) + + traceability = build_traceability(evidence, references) + for evidence_id, links in traceability.items(): + if not any(links.values()): + issue(issues, "warning", "evidence.unlinked", "Evidence is not referenced by a work item, timeline, or report", "evidence/" + evidence_id + ".md") + + errors = sum(1 for item in issues if item["level"] == "error") + warnings = sum(1 for item in issues if item["level"] == "warning") + status = "FAIL" if errors or (strict and warnings) else "WARN" if warnings else "PASS" + return { + "status": status, + "case_root": str(root), + "generated_at": datetime.now(timezone.utc).isoformat(), + "scope": scope, + "summary": { + "errors": errors, + "warnings": warnings, + "evidence": len(evidence), + "workitems": len(workitems), + "timeline_events": timeline_events, + "findings": len(findings), + "paths": len(paths), + }, + "issues": issues, + "traceability": traceability, + } + + +def render_markdown(report): + summary = report["summary"] + lines = [ + "# Case review", + "", + "- status: " + report["status"], + "- case_root: " + report["case_root"], + "- generated_at: " + report["generated_at"], + "", + "## Summary", + "", + "| Metric | Value |", + "|---|---:|", + ] + for key in ("errors", "warnings", "evidence", "workitems", "timeline_events", "findings", "paths"): + lines.append("| " + key + " | " + str(summary[key]) + " |") + + lines.extend(["", "## Checks", "", "| Level | Code | Location | Detail |", "|---|---|---|---|"]) + if report["issues"]: + for item in report["issues"]: + location = item["path"].replace("|", "\\|") + detail = item["message"].replace("|", "\\|") + lines.append("| " + item["level"] + " | " + item["code"] + " | " + location + " | " + detail + " |") + else: + lines.append("| pass | none | n/a | No review issues found |") + + lines.extend(["", "## Traceability", "", "| Evidence | Work items | Timeline | Reports |", "|---|---:|---:|---:|"]) + if report["traceability"]: + for evidence_id, links in report["traceability"].items(): + lines.append( + "| " + evidence_id + " | " + str(links["workitems"]) + " | " + str(links["timeline"]) + " | " + str(links["reports"]) + " |" + ) + else: + lines.append("| n/a | 0 | 0 | 0 |") + return "\n".join(lines) + "\n" + + +def main(argv=None): + parser = argparse.ArgumentParser(description="Review a reverse-skill case for scope and Evidence graph integrity") + parser.add_argument("case_root", help="Path to a work/ directory") + parser.add_argument("--format", choices=("markdown", "json"), default="markdown") + parser.add_argument("--strict", action="store_true", help="Treat warnings as a failed review") + parser.add_argument("--verify-hashes", action="store_true", help="Verify SHA-256 content_hash values against artifact_path") + args = parser.parse_args(argv) + report = review_case(args.case_root, strict=args.strict, verify_hashes=args.verify_hashes) + if args.format == "json": + print(json.dumps(report, ensure_ascii=False, indent=2)) + else: + print(render_markdown(report), end="") + return 1 if report["status"] == "FAIL" else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/case-review/tests/test_review_case.py b/skills/case-review/tests/test_review_case.py new file mode 100644 index 0000000..0023e3d --- /dev/null +++ b/skills/case-review/tests/test_review_case.py @@ -0,0 +1,170 @@ +import hashlib +import importlib.util +import tempfile +import unittest +from pathlib import Path + + +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "review_case.py" +SPEC = importlib.util.spec_from_file_location("review_case", SCRIPT_PATH) +REVIEW_CASE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(REVIEW_CASE) + + +class ReviewCaseTests(unittest.TestCase): + def write_case(self, root, include_report=True): + evidence_dir = root / "evidence" + report_dir = root / "report" + evidence_dir.mkdir(parents=True) + if include_report: + report_dir.mkdir(parents=True) + + artifact = evidence_dir / "sample.bin" + artifact.write_bytes(b"reverse-skill case review fixture") + digest = hashlib.sha256(artifact.read_bytes()).hexdigest() + + (root / "scope.md").write_text( + """# Case Scope + +## auth +- status: granted +- basis: lab_only + +## in_scope +- assets: + - sample.bin + +## network_profile +- mode: offline + +## signoff +- ready_for_act: true +""", + encoding="utf-8", + ) + (root / "workitems.md").write_text( + """# Work Items + +| ID | title | role | targets | surface | status | evidence | notes | +|----|-------|------|---------|---------|--------|----------|-------| +| WI-001 | Recover sample behavior | cre | sample.bin | binary | done | E-001 | | +""", + encoding="utf-8", + ) + (root / "timeline.md").write_text( + """# Timeline (append-only) + +## 2026-08-02T00:00:00Z | cre | static +- action: inspect local sample +- evidence_ids: [E-001] +""", + encoding="utf-8", + ) + (evidence_dir / "E-001.md").write_text( + """### E-001 +- title: Sample hash +- severity: info +- status: observed +- content_hash: sha256:%s +- artifact_path: evidence/sample.bin +- linked_workitem: WI-001 +- repro_command: sha256sum evidence/sample.bin +- raw_excerpt: | + fixture +""" % digest, + encoding="utf-8", + ) + if include_report: + (report_dir / "analysis.md").write_text( + """### F-001 +- title: Sample behavior recovered +- severity: info +- category: reverse_algo +- status: validated +- evidence_ids: [E-001] +- location: sample.bin:0x10 +- impact: n/a +- confidence: high +- repro_steps: | + 1. Run the fixture command. +- remediation: n/a + +### P-001 +- title: Static recovery path +- path_type: callflow +- start: sample.bin +- goal: recovered behavior +- steps: | + 1. Hash the sample with E-001. +- residual_risks: n/a +""", + encoding="utf-8", + ) + + def test_valid_case_passes_with_hash_verification(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.write_case(root) + result = REVIEW_CASE.review_case(root, verify_hashes=True) + self.assertEqual(result["status"], "PASS") + self.assertEqual(result["summary"]["evidence"], 1) + self.assertEqual(result["summary"]["findings"], 1) + self.assertEqual(result["summary"]["paths"], 1) + + def test_unknown_report_evidence_fails(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.write_case(root) + report = root / "report" / "analysis.md" + report.write_text(report.read_text(encoding="utf-8").replace("E-001", "E-999"), encoding="utf-8") + result = REVIEW_CASE.review_case(root) + self.assertEqual(result["status"], "FAIL") + self.assertTrue(any(item["code"] == "reference.unknown_evidence" for item in result["issues"])) + + def test_hash_mismatch_fails(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.write_case(root) + (root / "evidence" / "sample.bin").write_bytes(b"changed fixture") + result = REVIEW_CASE.review_case(root, verify_hashes=True) + self.assertEqual(result["status"], "FAIL") + self.assertTrue(any(item["code"] == "artifact.hash_mismatch" for item in result["issues"])) + + def test_markdown_output_contains_traceability_table(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.write_case(root) + result = REVIEW_CASE.review_case(root) + output = REVIEW_CASE.render_markdown(result) + self.assertIn("## Traceability", output) + self.assertIn("| E-001 | 1 | 1 | 2 |", output) + + def test_documented_offline_evidence_does_not_require_command(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.write_case(root) + (root / "evidence" / "E-002.md").write_text( + """### E-002 +- title: Offline observation +- severity: info +- status: observed +- content_hash: n/a +- repro_command: n/a +- notes: offline-only observation +""", + encoding="utf-8", + ) + result = REVIEW_CASE.review_case(root) + self.assertFalse(any(item["code"] == "evidence.repro_missing" for item in result["issues"])) + + def test_strict_mode_fails_when_report_is_missing(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + self.write_case(root, include_report=False) + result = REVIEW_CASE.review_case(root, strict=True) + self.assertEqual(result["status"], "FAIL") + self.assertTrue(any(item["code"] == "report.missing" for item in result["issues"])) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/ops/README.md b/skills/ops/README.md index 2008a87..796bb6a 100644 --- a/skills/ops/README.md +++ b/skills/ops/README.md @@ -11,6 +11,7 @@ Z3r0 启发、**本包形态**实现: | [timeline-workitem.md](timeline-workitem.md) | 时间线与覆盖 | | [sandbox-profile.md](sandbox-profile.md) | 工具对照 | | [skill-supply-chain.md](skill-supply-chain.md) | Agent Skill/MCP 供应链安全(AST10 精简) | +| [case-review/](../case-review/) | Evidence 图完整性审查与报告交接 | 相关 references(非孤儿,从本 hub / MASTER / SKILL 可达): @@ -20,5 +21,6 @@ Z3r0 启发、**本包形态**实现: - `../reverse-engineering/references/re-agent-workflow.md` — RE 四阶段 - `../pentest-tools/references/recon-pipeline.md` — 授权侦察 + Evidence 门 -脚本:`../scripts/case-init.ps1` -校验:`../scripts/verify-routing-coherence.ps1`(含 ops 契约检查) +- 脚本:`../scripts/case-init.ps1` +- 校验:`../scripts/verify-routing-coherence.ps1`(含 ops 契约检查) +- 审查:`../case-review/scripts/review_case.py`(只读 Evidence 图检查) diff --git a/skills/ops/evidence-finding-path.md b/skills/ops/evidence-finding-path.md index 3dc2632..0b05dde 100644 --- a/skills/ops/evidence-finding-path.md +++ b/skills/ops/evidence-finding-path.md @@ -14,6 +14,7 @@ - source_type: command | screenshot | file | log | memory | network | manual - source_ref: {path or command id} - content_hash: {sha256 of artifact if file, else n/a} +- artifact_path: {relative path under case root when content_hash is recorded, else n/a} - repro_command: | {exact command} - raw_excerpt: | @@ -31,6 +32,14 @@ powershell -File skills/scripts/append-evidence.ps1 -CaseRoot work/ ` -Id E-001 -Title "..." -ReproCommand "..." -Severity info -Status observed ``` +When the evidence is a case-local file, pass `-ArtifactPath` to record a SHA-256 fixity value and a relative artifact path. Review the complete case graph before handoff: + +```bash +python3 skills/case-review/scripts/review_case.py work/ --verify-hashes --strict +``` + +The review is read-only and checks scope fields, Evidence records, work item and timeline references, structured Findings, Paths, and artifact hash matches. + ## 2. Finding(安全/逆向结论) ```markdown diff --git a/skills/routing.md b/skills/routing.md index da83914..319c106 100644 --- a/skills/routing.md +++ b/skills/routing.md @@ -27,6 +27,7 @@ Route tasks to the most appropriate skill module by target type, user intent, an | macOS / iOS | `reverse-engineering/platforms.md` — Mach-O/ObjC/Swift | `mobile-reverse/` for iOS-specific | | Game (Unity) | `reverse-engineering/` — engine reverse, anti-cheat, IL2CPP/Mono (see seed-014) | `ida-reverse/` deep analysis | | Memory dump / PCAP | `digital-forensics/` — memory/timeline/PCAP IR | `protocol-reverse/` for protocol recovery | +| Existing case package / evidence handoff | `case-review/`: Evidence graph and fixity review | `docs-generator/` for final report writing | | Custom protocol / Protobuf / gRPC | `protocol-reverse/` | `js-reverse/` if pure browser WS crypto | | Cloud / Container / K8s | `cloud-k8s/` | CTF: `../CTF-Sandbox-Orchestrator/competition-agent-cloud/` | | Windows AD / Kerberos / AD CS | `windows-ad/` | multi-stage: `attack-chain/` | @@ -92,6 +93,7 @@ Route tasks to the most appropriate skill module by target type, user intent, an | "patch environment / Node reproduce" | `js-reverse/references/env-patching.md` | | "CTF challenge / competition reverse" | `reverse-engineering/patterns-ctf*.md` | | "write report / documentation" | `docs-generator/` — technical documentation | +| "review case / evidence chain / traceability" | `case-review/`: read-only Evidence Graph Review | | "write writeup" | `docs-generator/` — CTF writeup template | | "open webpage / browser automation / fill form" | `browser-automation/SKILL.md` — Playwright | | "crawl page / screenshot / auto login" | `browser-automation/SKILL.md` | @@ -212,6 +214,7 @@ Do NOT force the user to repeatedly confirm "this is CTF/local." Carry the CTF/l | Frida | `reverse-engineering/tools-dynamic.md` | | GDB / GEF / pwndbg / rr | `reverse-engineering/tools.md` | | Ghidra (headless) | `reverse-engineering/tools.md` + Ghidra MCP | +| Python 3 standard library | `case-review/`: read-only case evidence graph review | | angr / Qiling / Unicorn | `reverse-engineering/tools-dynamic.md` | | D-810 / d810-ng | `reverse-engineering/references/ollvm-deobfuscation.md` — IDA Pro 反混淆插件,OLLVM/Tigress/Hodur/Approov + Z3 SMT | | obpo-plugin | `reverse-engineering/references/ollvm-deobfuscation.md` — Hex-Rays microcode 云插件,效果最强 | diff --git a/skills/routing_zh.md b/skills/routing_zh.md index 51d6b88..bc65243 100644 --- a/skills/routing_zh.md +++ b/skills/routing_zh.md @@ -25,6 +25,7 @@ | WASM / Python 字节码 / .NET | `reverse-engineering/languages.md` | 按具体语言查对应章节 | | macOS / iOS | `reverse-engineering/platforms.md` — Mach-O/ObjC/Swift | — | | 内存转储 / PCAP | `reverse-engineering/platforms.md` | `reverse-engineering/patterns*.md` | +| 已有 case / 证据交接审查 | `case-review/SKILL.md`: Evidence 图与 fixity 校验 | `docs-generator/`: 最终报告 | | 密码学 / 加解密算法 | `reverse-engineering/patterns*.md` — 密码学模式 | `js-reverse/`(如果是前端加密) | | 协议逆向 / 自定义协议 | `reverse-engineering/platforms.md` — 网络协议 | `js-reverse/`(如果是 WebSocket/HTTP) | | Go / Rust 二进制 | `reverse-engineering/languages-compiled.md` + `go-reverse.md` | `ida-reverse/` 或 `radare2/` | @@ -64,6 +65,7 @@ | "补环境/Node 复现" | `js-reverse/references/env-patching.md` | | "CTF 题/竞赛逆向" | `reverse-engineering/patterns-ctf*.md` | | "写报告/写文档/出报告" | `docs-generator/` — 技术文档编写 | +| "审查 case / 证据链 / 可追溯性" | `case-review/SKILL.md`: 只读 Evidence 图审查 | | "写 writeup" | `docs-generator/` — CTF writeup 模板 | | "打开网页/浏览器自动化/填表" | `browser-automation/SKILL.md` — Playwright 浏览器操作 | | "爬取页面/截图/自动化登录" | `browser-automation/SKILL.md` — 浏览器自动化 | @@ -210,6 +212,7 @@ | Frida | `reverse-engineering/tools-dynamic.md` | | GDB / rr(通用调试) | `reverse-engineering/tools.md` | | Ghidra (headless) | `reverse-engineering/tools.md` + Ghidra MCP(免费 IDA 替代,可通过 bootstrap 自动注册) | +| Python 3 标准库 | `case-review/`: 只读 case Evidence 图审查 | | angr / Qiling / Unicorn | `reverse-engineering/tools-dynamic.md` | | BinDiff / Diaphora | `reverse-engineering/tools-advanced.md` | | anything-analyzer MCP | 端口 23816 的 MCP 服务器(浏览器+HTTP 捕获+AI 分析) | diff --git a/skills/scripts/append-evidence.ps1 b/skills/scripts/append-evidence.ps1 index 65ab1be..e9b52cc 100644 --- a/skills/scripts/append-evidence.ps1 +++ b/skills/scripts/append-evidence.ps1 @@ -34,7 +34,8 @@ param( [string] $Location = '', [string] $Notes = '', [string] $NotesFile = '', - [string] $ReproCommandFile = '' + [string] $ReproCommandFile = '', + [string] $ArtifactPath = '' ) $ErrorActionPreference = 'Stop' @@ -84,6 +85,27 @@ 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: $ArtifactPath" + } + $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: $ArtifactPath" + } + $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 @@ -117,7 +139,8 @@ $body = @" - observed_at: $observed - source_type: $src - source_ref: append-evidence.ps1 -- content_hash: n/a +- content_hash: $contentHash +- artifact_path: $artifactRef - severity: $sev - status: $st - location: $loc diff --git a/skills/scripts/master-route.ps1 b/skills/scripts/master-route.ps1 index 5e49829..4b275a3 100644 --- a/skills/scripts/master-route.ps1 +++ b/skills/scripts/master-route.ps1 @@ -32,6 +32,7 @@ $map = [ordered]@{ 'R19' = 'browser-automation/SKILL.md' 'R20' = 'docs-generator/SKILL.md' 'R39' = 'diagram-generator/SKILL.md' + 'R40' = 'case-review/SKILL.md' 'R21' = 'protocol-reverse/SKILL.md' 'R22' = 'ghidra-reverse/SKILL.md' 'R23' = 'cloud-k8s/SKILL.md' @@ -75,6 +76,7 @@ $labels = [ordered]@{ 'R19' = 'Browser / desktop automation' 'R20' = 'Docs generator' 'R39' = 'Diagram generation' + 'R40' = 'Case evidence review' 'R21' = 'Protocol reverse' 'R22' = 'Ghidra reverse' 'R23' = 'Cloud / K8s' @@ -124,6 +126,7 @@ if ($t -match 'edr|av.?bypass|syscall|amsi|etw.?patch|hell.?s.?gate|免杀|反 if ($t -match 'playwright|browser.?auto|desktop.?auto|openreverse|fill.?form|浏览器.?自动化|桌面.?自动化|自动.?填表') { [void]$sel.Add('R19') } if ($t -match 'writeup|write.?report|generate.?report|\breport\b|写.?报告|出.?报告|渗透.?报告|逆向.?报告') { [void]$sel.Add('R20') } if ($t -match 'diagram|mermaid|graphviz|plantuml|flowchart|流程图|架构图|时序图|状态图|数据流图|攻击路径图|er.?图|画图|图表') { [void]$sel.Add('R39') } +if ($t -match 'case.?review|case.?audit|evidence.?chain|evidence.?graph|traceability|fixity.?check|证据.?链|证据.?图|可追溯性|案件.?审查|案例.?审计') { [void]$sel.Add('R40') } if ($t -match 'protocol.?reverse|custom.?protocol|protobuf|grpc|pcap.?protocol|wireshark.?dissector|协议.?逆向|自定义.?协议|流量.?逆向') { [void]$sel.Add('R21') } if ($t -match 'ghidra|ghidra.?mcp|analyzeheadless|无.?ida|开源.?反编译') { [void]$sel.Add('R22') } if ($t -match 'kubernetes|\bk8s\b|container.?escape|docker.?escape|kube-?bench|cloud.?secur|imds|169\.254\.169\.254|容器.?逃逸|云.?安全|k8s.?渗透') { [void]$sel.Add('R23') } @@ -159,8 +162,8 @@ foreach ($item in $sel) { $uniq = New-Object System.Collections.Generic.List[string] foreach ($d in $scores.Keys) { [void]$uniq.Add($d) } -# priority high -> low (all R0-R39 must appear) -$priority = @('R4','R1','R2','R3','R30','R31','R33','R5','R9','R21','R22','R6','R7','R8','R34','R28','R17','R16','R18','R24','R37','R23','R35','R25','R36','R29','R38','R32','R26','R27','R10','R11','R12','R13','R14','R15','R19','R20','R39','R0') +# priority high -> low (all R0-R40 must appear) +$priority = @('R4','R1','R2','R3','R30','R31','R33','R5','R9','R21','R22','R6','R7','R8','R34','R28','R17','R16','R18','R24','R37','R23','R35','R25','R36','R29','R38','R32','R26','R27','R10','R11','R12','R13','R14','R15','R19','R40','R20','R39','R0') $primary = $null $maxScore = -1 diff --git a/skills/scripts/refresh-tool-index.ps1 b/skills/scripts/refresh-tool-index.ps1 index d86898b..eab3cba 100644 --- a/skills/scripts/refresh-tool-index.ps1 +++ b/skills/scripts/refresh-tool-index.ps1 @@ -34,7 +34,7 @@ $scriptRefs = @{ 'radiff2' = @('radare2/SKILL.md') 'rahash2' = @('radare2/SKILL.md') 'rax2' = @('radare2/SKILL.md') - 'python' = @('apk-reverse/scripts/frida-run.ps1') + 'python' = @('apk-reverse/scripts/frida-run.ps1', 'case-review/scripts/review_case.py') 'pip' = @() 'node' = @('js-reverse/SKILL.md') 'npx' = @('js-reverse/SKILL.md') diff --git a/skills/scripts/smoke.ps1 b/skills/scripts/smoke.ps1 index 499461b..ffc4545 100644 --- a/skills/scripts/smoke.ps1 +++ b/skills/scripts/smoke.ps1 @@ -91,7 +91,8 @@ $cases = @( @{ 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 = 'zh-js'; Hint = '前端签名 JS逆向 加密参数'; Expect = 'js-reverse' }, + @{ Name = 'evidence'; Hint = 'case review evidence chain traceability'; Expect = 'case-review' } ) $routeOk = 0 $routeFail = 0 diff --git a/skills/scripts/test-p0-friction.ps1 b/skills/scripts/test-p0-friction.ps1 index ecdfd1f..1028518 100644 --- a/skills/scripts/test-p0-friction.ps1 +++ b/skills/scripts/test-p0-friction.ps1 @@ -96,6 +96,25 @@ else { 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 @@ -286,7 +305,7 @@ if (Test-Path $smokeLog) { # OPT summary $opt = @( - "entrypoints: smoke.ps1, case-init.ps1, append-evidence.ps1, case-guard.ps1, master-route.ps1, verify-routing-coherence.ps1, test-p0-friction.ps1", + "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)", "ghost_skills: blockchain-security=$(Test-Path (Join-Path $skillsRoot 'blockchain-security')) bitcoin-puzzle=$(Test-Path (Join-Path $skillsRoot 'bitcoin-puzzle'))", "FAIL_COUNT=$($fail.Count)", diff --git a/skills/scripts/verify-routing-coherence.ps1 b/skills/scripts/verify-routing-coherence.ps1 index 91723db..60afe58 100644 --- a/skills/scripts/verify-routing-coherence.ps1 +++ b/skills/scripts/verify-routing-coherence.ps1 @@ -37,6 +37,8 @@ $opsFiles = @( 'MASTER-ROUTING.md', 'scripts\master-route.ps1', 'scripts\case-init.ps1', + 'case-review\SKILL.md', + 'case-review\scripts\review_case.py', 'docs-generator\references\security-report-templates.md', 'field-journal\_template.md' ) @@ -122,6 +124,7 @@ Assert-Fields (Join-Path $skillsRoot 'reverse-engineering\references\re-agent-wo 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') $fieldLog | Set-Content -LiteralPath (Join-Path $ScratchDir 'template-fields.txt') -Encoding UTF8 # --- role map skills exist for primary rows --- @@ -154,7 +157,8 @@ $cases = @( @{ 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' } + @{ N = 'sdr'; H = 'sdr hackrf gnu radio rf'; Id = 'R38'; Sub = 'radio-sdr/SKILL.md' }, + @{ N = 'evidence'; H = 'case review evidence chain traceability'; Id = 'R40'; Sub = 'case-review/SKILL.md' } ) foreach ($c in $cases) { $out = Join-Path $ScratchDir ("route-{0}" -f $c.N)