Vendored from cloudflare/security-audit-skill under .agents/skills, pinned in skills-lock.json and linked into .claude/skills. Also adds the project's shared permission allow-rules in .claude/settings.json. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019Tk3nAVF6n4dtjQS17FRFr
16 KiB
Reconnaissance
Phase 1: Map the source and plan coverage
The parent initializes run-metadata.json, applies the strict pre-reconnaissance budget gate in SKILL.md, then creates agent scratch roots and the shared ledger before hunting. If the gate fails, record the incomplete status in metadata and launch no reconnaissance agent. Reconnaissance reads the target and locally available build/configuration state only. It does not contact deployed endpoints, external identity providers, registries, brokers, cloud APIs, or other shared services.
Launch several research agents in parallel. They return structured facts to the parent and do not write files.
Agent 1a: Product, stack, and local operation
Read the target at <target>. Do not use network access. Return:
1. Product type, users, operators, and ordinary trust-sensitive actions.
2. Languages, frameworks, build system, runtimes, and locally visible deployment models.
3. Repository-relative entry points and subsystem boundaries.
4. Exact build and test commands that could run offline with local dependencies, their expected write locations, and the target-controlled inputs they process. Do not run them during reconnaissance.
5. Comparable software or protocol visible from local documentation and dependencies. If no useful comparison is source-grounded, say so.
6. Missing local toolchains or runtime facts that limit bounded execution.
Return only source facts with repository-relative file:line references.
Agent 1b: Principals, authority, and controls
Read all source that establishes identity, authorization, isolation, and privilege. Map:
1. Each lower-trust principal and the actions it has by design.
2. Authentication or peer identity at each entry surface.
3. Per-resource authorization and tenant/owner scope.
4. Process, browser, workload, CI, plugin, model/tool, device, or local-IPC authority.
5. Privilege changes, confirmation, revocation, recovery, and fallback paths.
6. Which controls are source-visible and which depend on an unobserved deployment fact.
Return trust boundaries and control locations with repository-relative file:line references. Do not infer live reachability.
Agent 1c: Entry surfaces, copies, and sinks
Inventory every source-visible place external or lower-trust input enters:
- HTTP/browser, RPC/message/protocol, files/archive/document, CLI/env/config, plugins/dependencies/CI, cloud events/IAM selectors, model context/tool arguments, mobile/deep-link/webview, and local IPC.
For each surface, follow major transformations, stored or derived copies, and security-relevant sinks. Record source-visible limits and parallel paths to the same effect.
Return repository-relative paths and line numbers. Be complete, but do not execute or send inputs.
Agent 1d: Local execution and deployment visibility
Read tests, build definitions, manifests, packaging, and maintained environment overlays. Return:
1. Small offline tests or existing fixtures that could validate trust boundaries with dummy data inside the required OS-enforced sandbox.
2. Processes that could use an isolated loopback network namespace without external or shared dependencies.
3. Commands that would fetch dependencies, publish artifacts, contact paid/provider APIs, or affect shared state; mark them prohibited for this run.
4. Deployed controls and attachments that source cannot establish and therefore require needs_validation if decisive.
5. The final active source path for each deployment mode only where the repository selects it deterministically.
6. Whether the local platform can enforce an empty allowlisted environment, no external network, read-only target/toolchain mounts, scratch-only writes, and explicit CPU, memory, process, file-size, disk, and wall-clock limits. Missing controls block target-controlled execution.
7. Whether trusted parent-side code can promote predeclared scratch files with path-confined no-follow descriptor traversal, nonblocking regular-file checks, no-follow traversal of every destination parent, exclusive regular-file destination creation, and explicit per-file and cumulative size bounds. Missing promotion controls block use of scratch files as evidence.
Add focused reconnaissance agents for materially distinct deployment modes or subsystems that these four do not map. Do not silently omit them: if the budget gate in SKILL.md blocks a focused agent, launch nothing for it, seed the unmapped area as a deferred ledger unit with reason budget_cannot_reserve_critics_and_validation, and disclose the gap in the report.
Prior-run input
Before selecting work, the parent reads every available prior coverage-ledger.json and findings.json for the same repo:
- Compare the source locations, controls, conditions, and source-derived identity for every prior record and unit against the current source.
- Carry an unchanged prior
confirmedrecord into the current candidate set, with the same fingerprint, only when its relevant source, conditions, and qualifying evidence still apply. Link it to a currentplannedunit withprior_status: "prior_confirmed_same_source"and put only that root cause on the hunter exclusion list. The Phase 3 verifier that re-verifies the carried record becomes that unit's assignment owner; its source re-check is the unit's first check and moves the unit tocandidatewith the carried fingerprint. - Build a current planned
prior_confirmed_changed_sourcerevalidation unit when any relevant source or condition changed. Do not exclude that root cause from hunting or assume the prior verdict still applies. - Build current work units for every prior
needs_validation,deferred,blocked,out_of_scope, and changed-source unit. These states are priority input, never deduplication or suppression keys. - Carry a still-blocked prior
needs_validationrecord with the same fingerprint only after current source supports its trace. Link it to a currentplannedunit withprior_status: "prior_needs_validation"; the record keeps the unresolved blocker. The Phase 3 verifier that re-checks the carried record becomes that unit's assignment owner; its re-check is the unit's first check and moves the unit tocandidatewith the carried fingerprint. Include the record in final verification. - Treat prior rejected records as stale claims unless current evidence changes the failed trace or missing condition. An unchanged rejection suppresses only that exact claim, not review of the coverage unit.
- Record missing or incompatible ledgers instead of treating them as empty coverage.
State paths and source refs used in run-metadata.json. Summarize only the coverage consequences in architecture.md.
Architecture summary and companion selection
The parent synthesizes <output-dir>/architecture.md, with a hard cap of about 1,000 words. Include:
- Product, principals, normal authority, and protected resources.
- The comparable-software baseline from Agent 1a, when one is source-grounded: what security trade-offs the comparable accepts. Use it to calibrate effort and severity, never to dismiss a demonstrated finding; if the comparable shares a defect pattern that has mattered in practice, that strengthens the finding. Omit this line when no meaningful comparable exists.
- Tech stack, source-visible deployment paths, and offline build/test limits.
- Entry surfaces and the important source-to-sink or lifecycle paths.
- Trust boundaries and the strongest source-visible control on each.
- Repository-relative starting paths.
- Prior coverage gaps, changed-source and blocked revalidation targets, and same-source confirmed exclusions.
- A short companion-selection summary derived from ATTACK-CLASSES.md: selected files and the source-visible boundaries that require them.
Keep the assignment-level ordinary block, selected companion blocks, and excluded blocks with reasons in each ledger unit, not in architecture.md. This keeps the architecture cap valid for large runs and makes the exact hunter prompt map machine-checkable.
Do not select a companion file merely because the language or dependency name appears. Select it because reconnaissance found the trust-sensitive boundary described by its When to use this file section. Do not exclude a visible boundary just because another agent will review a related class.
Deterministic coverage ledger
The parent writes <output-dir>/coverage-ledger.json as a top-level JSON array. Derive one unit for every material combination of entry surface, trust boundary, subsystem, and applicable ordinary or companion attack class at the granularity the run profile sets (quick uses one all-in-scope subsystem identity; deep adds lifecycle modes). For a scoped run, seed in-scope surfaces for assignment and retain discovered excluded surfaces as out_of_scope units so later full runs can turn them into current work.
Each dimension has a human label and a stable source-derived value in canonical_refs. Use the same canonical reference for the same source object across runs even if its display label changes. Suitable references include a repository-relative entry path plus exported scope, a route or message identity defined in source, the source control that defines a boundary, a repository package path, and the exact attack-class block reference. A block reference is FILE.md# plus the exact class name as written in bold or as a heading in that file — a stable identifier matched against the file text, not a rendered HTML anchor. For companion section blocks, use the heading text before any parenthetical qualifier (for example Core discipline). Do not derive references by lowercasing or slugging display labels.
Derive coverage_id without lossy slugs:
- Require every reference to be Unicode NFC with valid scalar values, visible content, no control, format, line/paragraph separator, or default-ignorable code point, and no surrounding whitespace.
- Encode its UTF-8 bytes with RFC 3986 percent encoding: leave only
A-Z a-z 0-9 - . _ ~unescaped and use uppercase%HHfor every other byte. - Join encoded
surface,boundary,subsystem, andattack_classreferences with::; append encodedlifecyclewhen present.
Use the fixed canonical value profile/quick/all-in-scope-subsystems for the quick profile's coarsened subsystem dimension. Do not include wave number, agent, verdict, severity, or line number in a reference or ID. Sort units lexicographically by coverage_id before each assignment. Fail on every duplicate ID. If duplicate IDs have different semantic fields, treat that as a canonical identity collision; never merge or silently overwrite them. The validator also rejects one semantic tuple represented by different canonical references.
Each unit records:
{
"coverage_id": "...",
"canonical_refs": {
"surface": "src/router.ts#POST /users/:id",
"boundary": "src/authz.ts#requireOwner",
"subsystem": "packages/api",
"attack_class": "ATTACK-CLASSES.md#Access control"
},
"surface": "...",
"boundary": "...",
"subsystem": "...",
"attack_class": "...",
"starting_paths": ["repo/relative/path"],
"ordinary_attack_class_block": "ATTACK-CLASSES.md#Access control",
"selected_companion_blocks": ["FILE.md#section"],
"excluded_blocks": [{"block": "FILE.md#section", "reason": "..."}],
"prior_status": "new|prior_confirmed_same_source|prior_confirmed_changed_source|prior_needs_validation|prior_deferred|prior_blocked|prior_out_of_scope|prior_covered_same_source|prior_covered_changed_source|prior_rejected_claim_changed|none",
"attempts": [],
"wave": 1,
"status": "planned",
"agent_id": null,
"reviewed_paths": [],
"local_checks": [],
"result_fingerprints": [],
"unresolved": []
}
When lifecycle is material, add both canonical_refs.lifecycle and a human lifecycle field. ordinary_attack_class_block is null only when no ordinary block applies. The selected companion list includes each applicable class plus its companion Core discipline, Universal moves, and Validation rules; excluded_blocks records every considered but unselected block and the source fact that excludes it.
The parent may add bookkeeping fields but keeps the semantic fields above stable. In prior_status, new marks a surface first seen in this run when compatible prior ledgers exist; none marks a unit seeded when no compatible prior ledger is available. Prior deferred, blocked, out_of_scope, and changed-source units initialize as current planned work when now in scope. A prior same-source covered unit remains visible in the current ledger; assign changed source, important lifecycle paths, and exact conflicts first, then use the coverage critic to decide whether it needs another pass.
attempts is an append-only archive for evidence-bearing assignments that a coverage critic reopens. Before reassignment, append the prior unit's exact wave, status, agent_id, reviewed_paths, local_checks, result_fingerprints, and unresolved, plus the critic's source-backed reassignment_reason. Only blocked, covered, and candidate states can be archived. Archived attempts retain the same state and evidence invariants as live units, use strictly increasing waves below the current wave, and retain their producing owners and artifacts. The next assignment increments wave, uses a fresh owner, and starts with empty live evidence. If the profile or budget prevents another assignment, increment wave and use live deferred state with null owner, empty evidence, and the stop reason. Never copy an archived owner's checks or artifacts into the live state. A later live terminal state contains only the new attempt's evidence; the archive remains unchanged.
Enforce this state table exactly:
| Status | Unit agent_id |
reviewed_paths / local_checks |
result_fingerprints |
unresolved |
|---|---|---|---|---|
planned |
null | empty | empty | empty |
not_applicable, out_of_scope, deferred |
null | empty | empty | nonempty reason |
in_progress |
canonical owner | empty | empty | empty |
blocked |
canonical owner | both nonempty owned partial evidence | empty | nonempty blocker |
covered |
canonical owner | both nonempty | empty | empty |
candidate |
canonical owner | both nonempty | nonempty | optional |
Canonical agent IDs match ^[a-z0-9][a-z0-9_-]{0,63}$ and are not Windows device names. Lowercase is mandatory, so one ledger cannot contain case-fold aliases. The unit agent_id records the assignment owner. Every check records its own agent_id and nonempty reviewed_paths; the unit-level reviewed_paths is exactly their union. A source-only check uses artifact: null. A local check requires a regular file promoted only by trusted parent-side code under exactly agents/<check.agent_id>/artifacts/. This lets hunter and verifier checks coexist in one unit. Scratch paths, output-root files, symlinks, special files, and another check owner's artifacts are not evidence.
The ledger is the coverage claim. An architecture summary, agent count, or generic "auth reviewed" sentence is not coverage evidence. Phase 2 closes units only from the paths and checks in a hunter's structured result.
Run node <skill-dir>/validate-coverage-ledger.cjs <output-dir>/coverage-ledger.json after seeding, after every parent update, and before Phase 6. The validator rejects input beyond 5 MiB, 64 nesting levels, 10,000 units, 1,000 entries in a nested collection, or 500,000 traversed values, and caps reported validation errors at 100. In practice the 5 MiB byte limit holds roughly 2,000-5,000 realistic units, so it binds before the 10,000-unit cap. Fix every error before assigning work or making a coverage claim.