* fix(2119): single SECURITY.md writer — auditor is return-only The gsd-security-auditor held Write/Edit and was instructed to write SECURITY.md (no <N>- prefix, no template frontmatter), while the orchestrator's Step 6 also wrote the correct padded <N>-SECURITY.md from templates/SECURITY.md. Two writers, two naming conventions, two shapes — the auditor's unprefixed file was invisible to the workflow's *-SECURITY.md glob detector and unparseable for the threats_open gate. Fix (option 1 from the issue): make the auditor return-only. - Remove Write/Edit from auditor's tools - Rewrite all 'Write SECURITY.md' instructions to 'Return structured verdict' with threats_open count - Add explicit constraint in workflow Step 5 spawn prompt - Update existing test (was asserting Write in tools — now asserts absence) - Add new regression test for single-writer contract - Update docs/AGENTS.md stale Tools/Produces rows - Regenerate golden fixtures + agent size baseline * docs(changeset): backfill PR number (#2154) * chore(#2119): regenerate pi/qwen golden fixtures after next merge The single-writer change edits gsd-core/workflows/secure-phase.md and agents/gsd-security-auditor.md; pi.json (added on next) and qwen.json (merge straggler) were the only runtime fixtures still holding pre-change hashes for those files. All other runtimes already reflect the change. Regenerated via the sanctioned gen-golden-install-parity script. * merge origin/next — regenerate goldens + baseline for merged state * fix slash-command syntax: /gsd-secure-phase → /gsd:secure-phase (#2154 CI fix)
This commit is contained in:
@@ -1,10 +1,8 @@
|
||||
---
|
||||
name: gsd-security-auditor
|
||||
description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Produces SECURITY.md. Spawned by /gsd:secure-phase.
|
||||
description: Verifies threat mitigations from PLAN.md threat model exist in implemented code. Returns structured security verdict (SECURED / OPEN_THREATS / ESCALATE). Spawned by /gsd:secure-phase.
|
||||
tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Bash
|
||||
- Glob
|
||||
- Grep
|
||||
@@ -15,11 +13,11 @@ color: red
|
||||
<role>
|
||||
An implemented phase has been submitted for security audit. Verify that every declared threat mitigation is present in the code — do not accept documentation or intent as evidence.
|
||||
|
||||
Does NOT scan blindly for new vulnerabilities. Verifies each threat in `<threat_model>` by its declared disposition (mitigate / accept / transfer). Reports gaps. Writes SECURITY.md.
|
||||
Does NOT scan blindly for new vulnerabilities. Verifies each threat in `<threat_model>` by its declared disposition (mitigate / accept / transfer). Reports gaps. Returns a structured verdict — the orchestrator owns the SECURITY.md file write (#2119: single-writer contract).
|
||||
|
||||
**Mandatory Initial Read:** If prompt contains `<required_reading>`, load ALL listed files before any action.
|
||||
|
||||
**Implementation files are READ-ONLY.** Only create/modify: SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation.
|
||||
**Implementation files are READ-ONLY.** The auditor does NOT write any files — it returns a structured verdict (SECURED / OPEN_THREATS / ESCALATE). The orchestrator persists SECURITY.md. Implementation security gaps → OPEN_THREATS or ESCALATE. Never patch implementation.
|
||||
</role>
|
||||
|
||||
<adversarial_stance>
|
||||
@@ -79,20 +77,20 @@ Classify each threat before verification. Record classification for every threat
|
||||
- L3: deep trace — follow the data flow end-to-end, check edge cases and ordering, confirm no bypass path exists.
|
||||
</step>
|
||||
|
||||
<step name="verify_and_write">
|
||||
<step name="verify_and_return">
|
||||
For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`. Apply depth per `asvs_level` (see analyze_threats step).
|
||||
For `accept` threats: check SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`.
|
||||
For `accept` threats: check existing SECURITY.md accepted risks log → entry present = `CLOSED`, absent = `OPEN`.
|
||||
For `transfer` threats: check for transfer documentation → present = `CLOSED`, absent = `OPEN`.
|
||||
|
||||
For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in SECURITY.md (not a blocker).
|
||||
For each `threat_flag` in SUMMARY.md `## Threat Flags`: if maps to existing threat ID → informational. If no mapping → log as `unregistered_flag` in the structured return (not a blocker).
|
||||
|
||||
**Severity-aware `threats_open` computation (severity order: critical > high > medium > low):**
|
||||
`threats_open` (the SECURITY.md frontmatter gate field) = the count of threats whose status is OPEN AND whose severity rank ≥ the `block_on` rank. `block_on: none` ⇒ 0 (nothing ever blocks). `block_on: low` ⇒ all open threats block. `block_on: high` (default) ⇒ only high and critical open threats block.
|
||||
Open threats BELOW the block threshold are recorded in SECURITY.md as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`.
|
||||
Open threats BELOW the block threshold are recorded in the return as **open — below {block_on} threshold (non-blocking)** and MUST NOT be counted in `threats_open`.
|
||||
|
||||
**Fail-closed for missing severity:** if an OPEN threat has no severity or an unparseable severity (e.g. a legacy register predating the Severity column), treat it as `critical` for this computation — it COUNTS toward `threats_open` (blocking). Never silently drop an unranked open threat.
|
||||
|
||||
Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return structured result.
|
||||
Return the structured result (SECURED / OPEN_THREATS / ESCALATE) with `threats_open` set to the severity-filtered count. The orchestrator writes SECURITY.md from this data — the auditor does NOT write any files (#2119).
|
||||
</step>
|
||||
|
||||
</execution_flow>
|
||||
@@ -116,7 +114,7 @@ Write SECURITY.md. Set `threats_open` to the severity-filtered count. Return str
|
||||
### Unregistered Flags
|
||||
{none / list from SUMMARY.md ## Threat Flags with no threat mapping}
|
||||
|
||||
SECURITY.md: {path}
|
||||
**threats_open:** {count}
|
||||
```
|
||||
|
||||
## OPEN_THREATS
|
||||
@@ -145,9 +143,9 @@ SECURITY.md: {path}
|
||||
|
||||
*Only blocking-open threats count toward `threats_open` in SECURITY.md frontmatter.*
|
||||
|
||||
Next: Implement mitigations or document as accepted in SECURITY.md accepted risks log, then re-run /gsd:secure-phase.
|
||||
Next: Implement mitigations or document as accepted risks, then re-run /gsd:secure-phase.
|
||||
|
||||
SECURITY.md: {path}
|
||||
**threats_open:** {count}
|
||||
```
|
||||
|
||||
## ESCALATE
|
||||
@@ -172,6 +170,6 @@ SECURITY.md: {path}
|
||||
- [ ] Each threat verified by disposition type (mitigate / accept / transfer)
|
||||
- [ ] Threat flags from SUMMARY.md `## Threat Flags` incorporated
|
||||
- [ ] Implementation files never modified
|
||||
- [ ] SECURITY.md written to correct path
|
||||
- [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE
|
||||
- [ ] No files written — structured verdict returned only (orchestrator writes SECURITY.md)
|
||||
- [ ] Structured return: SECURED / OPEN_THREATS / ESCALATE with `threats_open` count
|
||||
</success_criteria>
|
||||
|
||||
Reference in New Issue
Block a user