diff --git a/agents/gsd-executor.md b/agents/gsd-executor.md index 03cbf9b3..d258fe7b 100644 --- a/agents/gsd-executor.md +++ b/agents/gsd-executor.md @@ -133,6 +133,8 @@ No user permission needed for Rules 1-3. **Critical = required for correct/secure/performant operation.** These aren't "features" — they're correctness requirements. +**Threat model reference:** Before starting each task, check if the plan's `` assigns `mitigate` dispositions to this task's files. Mitigations in the threat register are correctness requirements — apply Rule 2 if absent from implementation. + --- **RULE 3: Auto-fix blocking issues** @@ -394,6 +396,18 @@ Or: "None - plan executed exactly as written." - Components with no data source wired (props always receiving empty/mock data) If any stubs exist, add a `## Known Stubs` section to the SUMMARY listing each stub with its file, line, and reason. These are tracked for the verifier to catch. Do NOT mark a plan as complete if stubs exist that prevent the plan's goal from being achieved — either wire the data or document in the plan why the stub is intentional and which future plan will resolve it. + +**Threat surface scan:** Before writing the SUMMARY, check if any files created/modified introduce security-relevant surface NOT in the plan's `` — new network endpoints, auth paths, file access patterns, or schema changes at trust boundaries. If found, add: + +```markdown +## Threat Flags + +| Flag | File | Description | +|------|------|-------------| +| threat_flag: {type} | {file} | {new surface description} | +``` + +Omit section if nothing found. diff --git a/agents/gsd-phase-researcher.md b/agents/gsd-phase-researcher.md index 28a7a00f..f8c89c2e 100644 --- a/agents/gsd-phase-researcher.md +++ b/agents/gsd-phase-researcher.md @@ -222,6 +222,8 @@ Priority: Context7 > Exa (verified) > Firecrawl (official docs) > Official GitHu - [ ] Confidence levels assigned honestly - [ ] "What might I have missed?" review completed - [ ] **If rename/refactor phase:** Runtime State Inventory completed — all 5 categories answered explicitly (not left blank) +- [ ] Security domain included (or `security_enforcement: false` confirmed) +- [ ] ASVS categories verified against phase tech stack @@ -393,6 +395,27 @@ Verified patterns from official sources: *(If no gaps: "None — existing test infrastructure covers all phase requirements")* +## Security Domain + +> Required when `security_enforcement` is enabled (absent = enabled). Omit only if explicitly `false` in config. + +### Applicable ASVS Categories + +| ASVS Category | Applies | Standard Control | +|---------------|---------|-----------------| +| V2 Authentication | {yes/no} | {library or pattern} | +| V3 Session Management | {yes/no} | {library or pattern} | +| V4 Access Control | {yes/no} | {library or pattern} | +| V5 Input Validation | yes | {e.g., zod / joi / pydantic} | +| V6 Cryptography | {yes/no} | {library — never hand-roll} | + +### Known Threat Patterns for {stack} + +| Pattern | STRIDE | Standard Mitigation | +|---------|--------|---------------------| +| {e.g., SQL injection} | Tampering | {parameterized queries / ORM} | +| {pattern} | {category} | {mitigation} | + ## Sources ### Primary (HIGH confidence) diff --git a/agents/gsd-planner.md b/agents/gsd-planner.md index 9c01b4bd..90fcd9f5 100644 --- a/agents/gsd-planner.md +++ b/agents/gsd-planner.md @@ -454,6 +454,21 @@ Output: [Artifacts created] + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| {e.g., client→API} | {untrusted input crosses here} | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-{phase}-01 | {S/T/R/I/D/E} | {function/endpoint/file} | mitigate | {specific: e.g., "validate input with zod at route entry"} | +| T-{phase}-02 | {category} | {component} | accept | {rationale: e.g., "no PII, low-value target"} | + + [Overall phase checks] @@ -585,6 +600,8 @@ Only include what Claude literally cannot do. **Step 0: Extract Requirement IDs** Read ROADMAP.md `**Requirements:**` line for this phase. Strip brackets if present (e.g., `[AUTH-01, AUTH-02]` → `AUTH-01, AUTH-02`). Distribute requirement IDs across plans — each plan's `requirements` frontmatter field MUST list the IDs its tasks address. **CRITICAL:** Every requirement ID MUST appear in at least one plan. Plans with an empty `requirements` field are invalid. +**Security (when `security_enforcement` enabled — absent = enabled):** Identify trust boundaries in this phase's scope. Map STRIDE categories to applicable tech stack from RESEARCH.md security domain. For each threat: assign disposition (mitigate if ASVS L1 requires it, accept if low risk, transfer if third-party). Every plan MUST include `` when security_enforcement is enabled. + **Step 1: State the Goal** Take phase goal from ROADMAP.md. Must be outcome-shaped, not task-shaped. - Good: "Working chat interface" (outcome) @@ -1338,6 +1355,9 @@ Phase planning complete when: - [ ] Wave structure maximizes parallelism - [ ] PLAN file(s) committed to git - [ ] User knows next steps and wave structure +- [ ] `` present with STRIDE register (when `security_enforcement` enabled) +- [ ] Every threat has a disposition (mitigate / accept / transfer) +- [ ] Mitigations reference specific implementation (not generic advice) ## Gap Closure Mode diff --git a/agents/gsd-security-auditor.md b/agents/gsd-security-auditor.md new file mode 100644 index 00000000..f045461d --- /dev/null +++ b/agents/gsd-security-auditor.md @@ -0,0 +1,128 @@ +--- +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. +tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep +color: "#EF4444" +--- + + +GSD security auditor. Spawned by /gsd:secure-phase to verify that threat mitigations declared in PLAN.md are present in implemented code. + +Does NOT scan blindly for new vulnerabilities. Verifies each threat in `` by its declared disposition (mitigate / accept / transfer). Reports gaps. Writes SECURITY.md. + +**Mandatory Initial Read:** If prompt contains ``, 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. + + + + + +Read ALL files from ``. Extract: +- PLAN.md `` block: full threat register with IDs, categories, dispositions, mitigation plans +- SUMMARY.md `## Threat Flags` section: new attack surface detected by executor during implementation +- `` block: `asvs_level` (1/2/3), `block_on` (open / unregistered / none) +- Implementation files: exports, auth patterns, input handling, data flows + + + +For each threat in ``, determine verification method by disposition: + +| Disposition | Verification Method | +|-------------|---------------------| +| `mitigate` | Grep for mitigation pattern in files cited in mitigation plan | +| `accept` | Verify entry present in SECURITY.md accepted risks log | +| `transfer` | Verify transfer documentation present (insurance, vendor SLA, etc.) | + +Classify each threat before verification. Record classification for every threat — no threat skipped. + + + +For each `mitigate` threat: grep for declared mitigation pattern in cited files → found = `CLOSED`, not found = `OPEN`. +For `accept` threats: check 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). + +Write SECURITY.md. Set `threats_open` count. Return structured result. + + + + + + +## SECURED + +```markdown +## SECURED + +**Phase:** {N} — {name} +**Threats Closed:** {count}/{total} +**ASVS Level:** {1/2/3} + +### Threat Verification +| Threat ID | Category | Disposition | Evidence | +|-----------|----------|-------------|----------| +| {id} | {category} | {mitigate/accept/transfer} | {file:line or doc reference} | + +### Unregistered Flags +{none / list from SUMMARY.md ## Threat Flags with no threat mapping} + +SECURITY.md: {path} +``` + +## OPEN_THREATS + +```markdown +## OPEN_THREATS + +**Phase:** {N} — {name} +**Closed:** {M}/{total} | **Open:** {K}/{total} +**ASVS Level:** {1/2/3} + +### Closed +| Threat ID | Category | Disposition | Evidence | +|-----------|----------|-------------|----------| +| {id} | {category} | {disposition} | {evidence} | + +### Open +| Threat ID | Category | Mitigation Expected | Files Searched | +|-----------|----------|---------------------|----------------| +| {id} | {category} | {pattern not found} | {file paths} | + +Next: Implement mitigations or document as accepted in SECURITY.md accepted risks log, then re-run /gsd:secure-phase. + +SECURITY.md: {path} +``` + +## ESCALATE + +```markdown +## ESCALATE + +**Phase:** {N} — {name} +**Closed:** 0/{total} + +### Details +| Threat ID | Reason Blocked | Suggested Action | +|-----------|----------------|------------------| +| {id} | {reason} | {action} | +``` + + + + +- [ ] All `` loaded before any analysis +- [ ] Threat register extracted from PLAN.md `` block +- [ ] 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 + diff --git a/commands/gsd/secure-phase.md b/commands/gsd/secure-phase.md new file mode 100644 index 00000000..a4984396 --- /dev/null +++ b/commands/gsd/secure-phase.md @@ -0,0 +1,35 @@ +--- +name: gsd:secure-phase +description: Retroactively verify threat mitigations for a completed phase +argument-hint: "[phase number]" +allowed-tools: + - Read + - Write + - Edit + - Bash + - Glob + - Grep + - Task + - AskUserQuestion +--- + +Verify threat mitigations for a completed phase. Three states: +- (A) SECURITY.md exists — audit and verify mitigations +- (B) No SECURITY.md, PLAN.md with threat model exists — run from artifacts +- (C) Phase not executed — exit with guidance + +Output: updated SECURITY.md. + + + +@~/.claude/get-shit-done/workflows/secure-phase.md + + + +Phase: $ARGUMENTS — optional, defaults to last completed phase. + + + +Execute @~/.claude/get-shit-done/workflows/secure-phase.md. +Preserve all workflow gates. + diff --git a/get-shit-done/templates/SECURITY.md b/get-shit-done/templates/SECURITY.md new file mode 100644 index 00000000..77f5c4da --- /dev/null +++ b/get-shit-done/templates/SECURITY.md @@ -0,0 +1,61 @@ +--- +phase: {N} +slug: {phase-slug} +status: draft +threats_open: 0 +asvs_level: 1 +created: {date} +--- + +# Phase {N} — Security + +> Per-phase security contract: threat register, accepted risks, and audit trail. + +--- + +## Trust Boundaries + +| Boundary | Description | Data Crossing | +|----------|-------------|---------------| +| {boundary} | {description} | {data type / sensitivity} | + +--- + +## Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation | Status | +|-----------|----------|-----------|-------------|------------|--------| +| T-{N}-01 | {STRIDE category} | {component} | {mitigate / accept / transfer} | {control or reference} | open | + +*Status: open · closed* +*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)* + +--- + +## Accepted Risks Log + +| Risk ID | Threat Ref | Rationale | Accepted By | Date | +|---------|------------|-----------|-------------|------| + +*Accepted risks do not resurface in future audit runs.* + +*If none: "No accepted risks."* + +--- + +## Security Audit Trail + +| Audit Date | Threats Total | Closed | Open | Run By | +|------------|---------------|--------|------|--------| +| {YYYY-MM-DD} | {N} | {N} | {N} | {name / agent} | + +--- + +## Sign-Off + +- [ ] All threats have a disposition (mitigate / accept / transfer) +- [ ] Accepted risks documented in Accepted Risks Log +- [ ] `threats_open: 0` confirmed +- [ ] `status: verified` set in frontmatter + +**Approval:** {pending / verified YYYY-MM-DD} diff --git a/get-shit-done/templates/VALIDATION.md b/get-shit-done/templates/VALIDATION.md index d569841e..6adaf46d 100644 --- a/get-shit-done/templates/VALIDATION.md +++ b/get-shit-done/templates/VALIDATION.md @@ -36,9 +36,9 @@ created: {date} ## Per-Task Verification Map -| Task ID | Plan | Wave | Requirement | Test Type | Automated Command | File Exists | Status | -|---------|------|------|-------------|-----------|-------------------|-------------|--------| -| {N}-01-01 | 01 | 1 | REQ-{XX} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| {N}-01-01 | 01 | 1 | REQ-{XX} | T-{N}-01 / — | {expected secure behavior or "N/A"} | unit | `{command}` | ✅ / ❌ W0 | ⬜ pending | *Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky* diff --git a/get-shit-done/templates/config.json b/get-shit-done/templates/config.json index fd147b6f..8fb0ecc6 100644 --- a/get-shit-done/templates/config.json +++ b/get-shit-done/templates/config.json @@ -7,6 +7,9 @@ "verifier": true, "auto_advance": false, "nyquist_validation": true, + "security_enforcement": true, + "security_asvs_level": 1, + "security_block_on": "high", "discuss_mode": "discuss", "research_before_questions": false }, diff --git a/get-shit-done/workflows/execute-phase.md b/get-shit-done/workflows/execute-phase.md index 0bdb1117..83817919 100644 --- a/get-shit-done/workflows/execute-phase.md +++ b/get-shit-done/workflows/execute-phase.md @@ -436,6 +436,27 @@ After all waves: ### Issues Encountered [Aggregate from SUMMARYs, or "None"] ``` + +**Security gate check:** +```bash +SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true") +SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1) +``` + +If `SECURITY_CFG` is `false`: skip. + +If `SECURITY_CFG` is `true` AND `SECURITY_FILE` is empty (no SECURITY.md yet): +Include in the next-steps routing output: +``` +⚠ Security enforcement enabled — run before advancing: + /gsd:secure-phase {PHASE} ${GSD_WS} +``` + +If `SECURITY_CFG` is `true` AND SECURITY.md exists: check frontmatter `threats_open`. If > 0: +``` +⚠ Security gate: {threats_open} threats open + /gsd:secure-phase {PHASE} — resolve before advancing +``` diff --git a/get-shit-done/workflows/plan-phase.md b/get-shit-done/workflows/plan-phase.md index b13dc034..f72dd581 100644 --- a/get-shit-done/workflows/plan-phase.md +++ b/get-shit-done/workflows/plan-phase.md @@ -360,6 +360,32 @@ test -f "${PHASE_DIR}/${PADDED_PHASE}-VALIDATION.md" && echo "VALIDATION_CREATED **If not found:** Warn and continue — plans may fail Dimension 8. +## 5.55. Security Threat Model Gate + +> Skip if `workflow.security_enforcement` is explicitly `false`. Absent = enabled. + +```bash +SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true") +SECURITY_ASVS=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_asvs_level --raw 2>/dev/null || echo "1") +SECURITY_BLOCK=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_block_on --raw 2>/dev/null || echo "high") +``` + +**If `SECURITY_CFG` is `false`:** Skip to step 5.6. + +**If `SECURITY_CFG` is `true`:** Display banner: + +``` +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + GSD ► SECURITY THREAT MODEL REQUIRED (ASVS L{SECURITY_ASVS}) +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +Each PLAN.md must include a block. +Block on: {SECURITY_BLOCK} severity threats. +Opt out: set security_enforcement: false in .planning/config.json +``` + +Continue to step 5.6. Security config is passed to the planner in step 8. + ## 5.6. UI Design Contract Gate > Skip if `workflow.ui_phase` is explicitly `false` AND `workflow.ui_safety_gate` is explicitly `false` in `.planning/config.json`. If keys are absent, treat as enabled. @@ -496,6 +522,7 @@ ${AGENT_SKILLS_PLANNER} **Project instructions:** Read ./CLAUDE.md if exists — follow project-specific guidelines **Project skills:** Check .claude/skills/ or .agents/skills/ directory (if either exists) — read SKILL.md files, plans should account for project skill rules + diff --git a/get-shit-done/workflows/secure-phase.md b/get-shit-done/workflows/secure-phase.md new file mode 100644 index 00000000..2497479b --- /dev/null +++ b/get-shit-done/workflows/secure-phase.md @@ -0,0 +1,164 @@ + +Verify threat mitigations for a completed phase. Confirm PLAN.md threat register dispositions are resolved. Update SECURITY.md. + + + +@~/.claude/get-shit-done/references/ui-brand.md + + + +Valid GSD subagent types (use exact names — do not fall back to 'general-purpose'): +- gsd-security-auditor — Verifies threat mitigation coverage + + + + +## 0. Initialize + +```bash +INIT=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" init phase-op "${PHASE_ARG}") +if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi +AGENT_SKILLS_AUDITOR=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" agent-skills gsd-security-auditor 2>/dev/null) +``` + +Parse: `phase_dir`, `phase_number`, `phase_name`, `phase_slug`, `padded_phase`. + +```bash +AUDITOR_MODEL=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" resolve-model gsd-security-auditor --raw) +SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true") +``` + +If `SECURITY_CFG` is `false`: exit with "Security enforcement disabled. Enable via /gsd:settings." + +Display banner: `GSD > SECURE PHASE {N}: {name}` + +## 1. Detect Input State + +```bash +SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1) +PLAN_FILES=$(ls "${PHASE_DIR}"/*-PLAN.md 2>/dev/null) +SUMMARY_FILES=$(ls "${PHASE_DIR}"/*-SUMMARY.md 2>/dev/null) +``` + +- **State A** (`SECURITY_FILE` non-empty): Audit existing +- **State B** (`SECURITY_FILE` empty, `PLAN_FILES` and `SUMMARY_FILES` non-empty): Run from artifacts +- **State C** (`SUMMARY_FILES` empty): Exit — "Phase {N} not executed. Run /gsd:execute-phase {N} first." + +## 2. Discovery + +### 2a. Read Phase Artifacts + +Read PLAN.md — extract `` block: trust boundaries, STRIDE register (`threat_id`, `category`, `component`, `disposition`, `mitigation_plan`). + +### 2b. Read Summary Threat Flags + +Read SUMMARY.md — extract `## Threat Flags` entries. + +### 2c. Build Threat Register + +Per threat: `{ threat_id, category, component, disposition, mitigation_pattern, files_to_check }` + +## 3. Threat Classification + +Classify each threat: + +| Status | Criteria | +|--------|----------| +| CLOSED | mitigation found OR accepted risk documented in SECURITY.md OR transfer documented | +| OPEN | none of the above | + +Build: `{ threat_id, category, component, disposition, status, evidence }` + +If `threats_open: 0` → skip to Step 6 directly. + +## 4. Present Threat Plan + +Call AskUserQuestion with threat table and options: +1. "Verify all open threats" → Step 5 +2. "Accept all open — document in accepted risks log" → add to SECURITY.md accepted risks, set all CLOSED, Step 6 +3. "Cancel" → exit + +## 5. Spawn gsd-security-auditor + +``` +Task( + prompt="Read ~/.claude/agents/gsd-security-auditor.md for instructions.\n\n" + + "{PLAN, SUMMARY, impl files, SECURITY.md}" + + "{threat register}" + + "asvs_level: {SECURITY_ASVS}, block_on: {SECURITY_BLOCK_ON}" + + "Never modify implementation files. Verify mitigations exist — do not scan for new threats. Escalate implementation gaps." + + "${AGENT_SKILLS_AUDITOR}", + subagent_type="gsd-security-auditor", + model="{AUDITOR_MODEL}", + description="Verify threat mitigations for Phase {N}" +) +``` + +Handle return: +- `## SECURED` → record closures → Step 6 +- `## OPEN_THREATS` → record closed + open, present user with accept/block choice → Step 6 +- `## ESCALATE` → present to user → Step 6 + +## 6. Write/Update SECURITY.md + +**State B (create):** +1. Read template from `~/.claude/get-shit-done/templates/SECURITY.md` +2. Fill: frontmatter, threat register, accepted risks, audit trail +3. Write to `${PHASE_DIR}/${PADDED_PHASE}-SECURITY.md` + +**State A (update):** +1. Update threat register statuses, append to audit trail: + +```markdown +## Security Audit {date} +| Metric | Count | +|--------|-------| +| Threats found | {N} | +| Closed | {M} | +| Open | {K} | +``` + +**ENFORCING GATE:** If `threats_open > 0` after all options exhausted (user did not accept, not all verified closed): + +``` +GSD > PHASE {N} SECURITY BLOCKED +{K} threats open — phase advancement blocked until threats_open: 0 +▶ Fix mitigations then re-run: /gsd:secure-phase {N} +▶ Or document accepted risks in SECURITY.md and re-run. +``` + +Do NOT emit next-phase routing. Stop here. + +## 7. Commit + +```bash +node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" commit "docs(phase-${PHASE}): add/update security threat verification" +``` + +## 8. Results + Routing + +**Secured (threats_open: 0):** +``` +GSD > PHASE {N} THREAT-SECURE +threats_open: 0 — all threats have dispositions. +▶ /gsd:validate-phase {N} validate test coverage +▶ /gsd:verify-work {N} run UAT +``` + +Display `/clear` reminder. + + + + +- [ ] Security enforcement checked — exit if false +- [ ] Input state detected (A/B/C) — state C exits cleanly +- [ ] PLAN.md threat model parsed, register built +- [ ] SUMMARY.md threat flags incorporated +- [ ] threats_open: 0 → skip directly to Step 6 +- [ ] User gate with threat table presented +- [ ] Auditor spawned with complete context +- [ ] All three return formats (SECURED/OPEN_THREATS/ESCALATE) handled +- [ ] SECURITY.md created or updated +- [ ] threats_open > 0 BLOCKS advancement (no next-phase routing emitted) +- [ ] Results with routing presented on success + diff --git a/get-shit-done/workflows/verify-work.md b/get-shit-done/workflows/verify-work.md index c21dc9ea..57e23dc8 100644 --- a/get-shit-done/workflows/verify-work.md +++ b/get-shit-done/workflows/verify-work.md @@ -375,11 +375,38 @@ Present summary: **If issues > 0:** Proceed to `diagnose_issues` **If issues == 0:** + +```bash +SECURITY_CFG=$(node "$HOME/.claude/get-shit-done/bin/gsd-tools.cjs" config-get workflow.security_enforcement --raw 2>/dev/null || echo "true") +SECURITY_FILE=$(ls "${PHASE_DIR}"/*-SECURITY.md 2>/dev/null | head -1) +``` + +If `SECURITY_CFG` is `true` AND `SECURITY_FILE` is empty: +``` +⚠ Security enforcement enabled — /gsd:secure-phase {phase} has not run. +Run before advancing to the next phase. + +All tests passed. Ready to continue. + +- `/gsd:secure-phase {phase}` — security review (required before advancing) +- `/gsd:plan-phase {next}` — Plan next phase +- `/gsd:execute-phase {next}` — Execute next phase +- `/gsd:ui-review {phase}` — visual quality audit (if frontend files were modified) +``` + +If `SECURITY_CFG` is `true` AND `SECURITY_FILE` exists: check frontmatter `threats_open`. If > 0: +``` +⚠ Security gate: {threats_open} threats open + /gsd:secure-phase {phase} — resolve before advancing +``` + +If `SECURITY_CFG` is `false` OR (`SECURITY_FILE` exists AND `threats_open` is `0`): ``` All tests passed. Ready to continue. - `/gsd:plan-phase {next}` — Plan next phase - `/gsd:execute-phase {next}` — Execute next phase +- `/gsd:secure-phase {phase}` — security review - `/gsd:ui-review {phase}` — visual quality audit (if frontend files were modified) ``` diff --git a/tests/copilot-install.test.cjs b/tests/copilot-install.test.cjs index 65c92627..25ec66c1 100644 --- a/tests/copilot-install.test.cjs +++ b/tests/copilot-install.test.cjs @@ -1177,6 +1177,7 @@ describe('E2E: Copilot full install verification', () => { 'gsd-project-researcher.agent.md', 'gsd-research-synthesizer.agent.md', 'gsd-roadmapper.agent.md', + 'gsd-security-auditor.agent.md', 'gsd-ui-auditor.agent.md', 'gsd-ui-checker.agent.md', 'gsd-ui-researcher.agent.md', diff --git a/tests/secure-phase.test.cjs b/tests/secure-phase.test.cjs new file mode 100644 index 00000000..db1dd3c3 --- /dev/null +++ b/tests/secure-phase.test.cjs @@ -0,0 +1,440 @@ +/** + * GSD Secure-Phase Tests + * + * Validates the security-first enforcement layer: + * - gsd-security-auditor agent frontmatter and structure + * - secure-phase command file + * - secure-phase workflow file + * - SECURITY.md template + * - config.json security defaults + * - VALIDATION.md security columns + * - Threat-model-anchored behaviour (structural) + */ + +const { test, describe } = require('node:test'); +const assert = require('node:assert'); +const fs = require('fs'); +const path = require('path'); + +const REPO_ROOT = path.join(__dirname, '..'); +const AGENTS_DIR = path.join(REPO_ROOT, 'agents'); +const COMMANDS_DIR = path.join(REPO_ROOT, 'commands', 'gsd'); +const WORKFLOWS_DIR = path.join(REPO_ROOT, 'get-shit-done', 'workflows'); +const TEMPLATES_DIR = path.join(REPO_ROOT, 'get-shit-done', 'templates'); + +// ─── 1. Agent frontmatter — gsd-security-auditor.md ───────────────────────── + +describe('SECURE: gsd-security-auditor agent', () => { + const agentPath = path.join(AGENTS_DIR, 'gsd-security-auditor.md'); + + test('agent file exists', () => { + assert.ok( + fs.existsSync(agentPath), + 'gsd-security-auditor.md must exist in agents/' + ); + }); + + test('has valid frontmatter with name, description, tools, color', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + const frontmatter = content.split('---')[1] || ''; + assert.ok(frontmatter.includes('name:'), 'missing name:'); + assert.ok(frontmatter.includes('description:'), 'missing description:'); + assert.ok(frontmatter.includes('tools:'), 'missing tools:'); + assert.ok(frontmatter.includes('color:'), 'missing color:'); + }); + + test('name is gsd-security-auditor', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + const frontmatter = content.split('---')[1] || ''; + assert.ok( + frontmatter.includes('name: gsd-security-auditor'), + 'name must be gsd-security-auditor' + ); + }); + + test('tools include Read, Write, Bash, Glob, Grep', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + const requiredTools = ['Read', 'Write', 'Bash', 'Glob', 'Grep']; + for (const tool of requiredTools) { + assert.ok( + content.includes(`- ${tool}`), + `tools must include ${tool}` + ); + } + }); + + test('has section', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok(content.includes(''), 'must have section'); + assert.ok(content.includes(''), 'must close section'); + }); + + test('has section', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok(content.includes(''), 'must have section'); + assert.ok(content.includes(''), 'must close section'); + }); + + test('has with SECURED, OPEN_THREATS, ESCALATE', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok(content.includes(''), 'must have section'); + assert.ok(content.includes('## SECURED'), 'must have SECURED return type'); + assert.ok(content.includes('## OPEN_THREATS'), 'must have OPEN_THREATS return type'); + assert.ok(content.includes('## ESCALATE'), 'must have ESCALATE return type'); + }); + + test('has section', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok(content.includes(''), 'must have section'); + assert.ok(content.includes(''), 'must close section'); + }); + + test('has READ-ONLY rule — does NOT modify implementation files', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok( + content.includes('READ-ONLY'), + 'must contain READ-ONLY rule for implementation files' + ); + }); +}); + +// ─── 2. Command file — secure-phase.md ────────────────────────────────────── + +describe('SECURE: secure-phase command file', () => { + const cmdPath = path.join(COMMANDS_DIR, 'secure-phase.md'); + + test('command file exists', () => { + assert.ok( + fs.existsSync(cmdPath), + 'secure-phase.md must exist in commands/gsd/' + ); + }); + + test('has valid frontmatter with name gsd:secure-phase', () => { + const content = fs.readFileSync(cmdPath, 'utf-8'); + const frontmatter = content.split('---')[1] || ''; + assert.ok( + frontmatter.includes('name: gsd:secure-phase'), + 'name must be gsd:secure-phase' + ); + }); + + test('has allowed-tools list', () => { + const content = fs.readFileSync(cmdPath, 'utf-8'); + const frontmatter = content.split('---')[1] || ''; + assert.ok( + frontmatter.includes('allowed-tools:'), + 'must have allowed-tools in frontmatter' + ); + }); + + test('contains reference to secure-phase.md workflow', () => { + const content = fs.readFileSync(cmdPath, 'utf-8'); + assert.ok( + content.includes('secure-phase.md'), + 'must reference secure-phase.md workflow' + ); + }); + + test('has section mentioning states A, B, C', () => { + const content = fs.readFileSync(cmdPath, 'utf-8'); + assert.ok(content.includes(''), 'must have section'); + assert.ok(content.includes('(A)'), 'must mention state A'); + assert.ok(content.includes('(B)'), 'must mention state B'); + assert.ok(content.includes('(C)'), 'must mention state C'); + }); +}); + +// ─── 3. Workflow file — secure-phase.md ───────────────────────────────────── + +describe('SECURE: secure-phase workflow file', () => { + const wfPath = path.join(WORKFLOWS_DIR, 'secure-phase.md'); + + test('workflow file exists', () => { + assert.ok( + fs.existsSync(wfPath), + 'secure-phase.md must exist in get-shit-done/workflows/' + ); + }); + + test('contains gsd-security-auditor reference', () => { + const content = fs.readFileSync(wfPath, 'utf-8'); + assert.ok( + content.includes('gsd-security-auditor'), + 'must reference gsd-security-auditor agent' + ); + }); + + test('contains threats_open enforcement logic', () => { + const content = fs.readFileSync(wfPath, 'utf-8'); + assert.ok( + content.includes('threats_open'), + 'must contain threats_open enforcement logic' + ); + }); + + test('contains security_enforcement config check', () => { + const content = fs.readFileSync(wfPath, 'utf-8'); + assert.ok( + content.includes('security_enforcement'), + 'must check security_enforcement config setting' + ); + }); + + test('contains SECURITY.md template reference', () => { + const content = fs.readFileSync(wfPath, 'utf-8'); + assert.ok( + content.includes('SECURITY.md'), + 'must reference SECURITY.md template' + ); + }); + + test('has success_criteria section', () => { + const content = fs.readFileSync(wfPath, 'utf-8'); + assert.ok( + content.includes(''), + 'must have section' + ); + assert.ok( + content.includes(''), + 'must close section' + ); + }); +}); + +// ─── 4. SECURITY.md template ──────────────────────────────────────────────── + +describe('SECURE: SECURITY.md template', () => { + const tplPath = path.join(TEMPLATES_DIR, 'SECURITY.md'); + + test('template exists', () => { + assert.ok( + fs.existsSync(tplPath), + 'SECURITY.md must exist in get-shit-done/templates/' + ); + }); + + test('has YAML frontmatter with required fields', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + const frontmatter = content.split('---')[1] || ''; + const requiredFields = ['phase', 'slug', 'status', 'threats_open', 'asvs_level', 'created']; + for (const field of requiredFields) { + assert.ok( + frontmatter.includes(`${field}:`), + `frontmatter must have ${field}: field` + ); + } + }); + + test('has ## Trust Boundaries section', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + assert.ok( + content.includes('## Trust Boundaries'), + 'must have ## Trust Boundaries section' + ); + }); + + test('has ## Threat Register table with required columns', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + assert.ok(content.includes('## Threat Register'), 'must have ## Threat Register section'); + const requiredColumns = ['Threat ID', 'Category', 'Component', 'Disposition', 'Mitigation', 'Status']; + for (const col of requiredColumns) { + assert.ok( + content.includes(col), + `Threat Register table must have ${col} column` + ); + } + }); + + test('has ## Accepted Risks Log section', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + assert.ok( + content.includes('## Accepted Risks Log'), + 'must have ## Accepted Risks Log section' + ); + }); + + test('has ## Security Audit Trail section', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + assert.ok( + content.includes('## Security Audit Trail'), + 'must have ## Security Audit Trail section' + ); + }); + + test('has sign-off checklist', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + assert.ok( + content.includes('## Sign-Off'), + 'must have ## Sign-Off section' + ); + assert.ok( + content.includes('- [ ]'), + 'sign-off must have checklist items' + ); + }); + + test('threats_open field is present (terminal condition field)', () => { + const content = fs.readFileSync(tplPath, 'utf-8'); + const frontmatter = content.split('---')[1] || ''; + assert.ok( + frontmatter.includes('threats_open:'), + 'threats_open must be present in frontmatter as terminal condition field' + ); + }); +}); + +// ─── 5. Config defaults ───────────────────────────────────────────────────── + +describe('SECURE: config.json security defaults', () => { + const configPath = path.join(TEMPLATES_DIR, 'config.json'); + + test('config template exists', () => { + assert.ok( + fs.existsSync(configPath), + 'config.json must exist in get-shit-done/templates/' + ); + }); + + test('has workflow.security_enforcement set to true', () => { + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.strictEqual( + config.workflow.security_enforcement, + true, + 'security_enforcement must default to true' + ); + }); + + test('has workflow.security_asvs_level set to 1', () => { + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.strictEqual( + config.workflow.security_asvs_level, + 1, + 'security_asvs_level must default to 1' + ); + }); + + test('has workflow.security_block_on set to "high"', () => { + const config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); + assert.strictEqual( + config.workflow.security_block_on, + 'high', + 'security_block_on must default to "high"' + ); + }); + + test('security_enforcement appears after nyquist_validation (opt-out pattern parity)', () => { + const raw = fs.readFileSync(configPath, 'utf-8'); + const nyquistPos = raw.indexOf('nyquist_validation'); + const securityPos = raw.indexOf('security_enforcement'); + assert.ok(nyquistPos > -1, 'nyquist_validation must exist in config'); + assert.ok(securityPos > -1, 'security_enforcement must exist in config'); + assert.ok( + securityPos > nyquistPos, + 'security_enforcement must appear after nyquist_validation for opt-out pattern parity' + ); + }); +}); + +// ─── 6. VALIDATION.md template security columns ──────────────────────────── + +describe('SECURE: VALIDATION.md security columns', () => { + const valPath = path.join(TEMPLATES_DIR, 'VALIDATION.md'); + + test('VALIDATION.md template exists', () => { + assert.ok( + fs.existsSync(valPath), + 'VALIDATION.md must exist in get-shit-done/templates/' + ); + }); + + test('contains Threat Ref column header', () => { + const content = fs.readFileSync(valPath, 'utf-8'); + assert.ok( + content.includes('Threat Ref'), + 'must have Threat Ref column in Per-Task Verification Map' + ); + }); + + test('contains Secure Behavior column header', () => { + const content = fs.readFileSync(valPath, 'utf-8'); + assert.ok( + content.includes('Secure Behavior'), + 'must have Secure Behavior column in Per-Task Verification Map' + ); + }); + + test('both columns appear in the Per-Task Verification Map table', () => { + const content = fs.readFileSync(valPath, 'utf-8'); + // Find the table header row containing both columns + const lines = content.split('\n'); + const headerLine = lines.find( + line => line.includes('Threat Ref') && line.includes('Secure Behavior') + ); + assert.ok( + headerLine, + 'Threat Ref and Secure Behavior must appear in the same table header row' + ); + // Verify this is in the Per-Task Verification Map section + const mapIdx = content.indexOf('## Per-Task Verification Map'); + const threatRefIdx = content.indexOf('Threat Ref'); + assert.ok(mapIdx > -1, 'must have Per-Task Verification Map section'); + assert.ok( + threatRefIdx > mapIdx, + 'Threat Ref column must appear after Per-Task Verification Map heading' + ); + }); +}); + +// ─── 7. Threat-model-anchored behaviour (structural) ──────────────────────── + +describe('SECURE: threat-model-anchored behaviour', () => { + const agentPath = path.join(AGENTS_DIR, 'gsd-security-auditor.md'); + const wfPath = path.join(WORKFLOWS_DIR, 'secure-phase.md'); + + test('agent does NOT contain "scan for vulnerabilities" (verifies, not scans)', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok( + !content.toLowerCase().includes('scan for vulnerabilities'), + 'agent must NOT scan for vulnerabilities — it verifies threat mitigations' + ); + }); + + test('agent does NOT contain "find vulnerabilities" (verifies, not scans)', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok( + !content.toLowerCase().includes('find vulnerabilities'), + 'agent must NOT find vulnerabilities — it verifies threat mitigations' + ); + }); + + test('agent contains mitigate, accept, transfer disposition types', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok(content.includes('mitigate'), 'must contain mitigate disposition'); + assert.ok(content.includes('accept'), 'must contain accept disposition'); + assert.ok(content.includes('transfer'), 'must contain transfer disposition'); + }); + + test('agent contains OPEN and CLOSED status values', () => { + const content = fs.readFileSync(agentPath, 'utf-8'); + assert.ok(content.includes('OPEN'), 'must contain OPEN status'); + assert.ok(content.includes('CLOSED'), 'must contain CLOSED status'); + }); + + test('workflow contains enforcing gate (threats_open + block pattern)', () => { + const content = fs.readFileSync(wfPath, 'utf-8'); + assert.ok( + content.includes('threats_open'), + 'workflow must reference threats_open for enforcement' + ); + assert.ok( + content.includes('BLOCKED') || content.includes('blocked'), + 'workflow must contain a blocking pattern when threats are open' + ); + // Verify it does NOT emit next-phase routing when blocked + assert.ok( + content.includes('Do NOT emit next-phase routing'), + 'workflow must explicitly prevent next-phase routing when blocked' + ); + }); +});