workflow.security_block_on was documented as the minimum threat severity that blocks advancement, but threats carried no severity and the auditor's threats_open count (the SECURITY.md gate field) counted every open threat regardless of severity — so the threshold had no effect, and the auditor's block_on vocabulary (open/unregistered/none) did not even match the config enum (critical/high/medium/low/none). - planner: add a Severity column to the STRIDE threat register; assign severity per threat. - auditor: read severity; reconcile the <config> block_on domain to the severity enum; redefine threats_open as the count of OPEN threats whose severity is at or above block_on (none => 0). Below-threshold opens are reported as non-blocking and excluded from threats_open. - SECURITY.md template + planning-config.md reconciled. No gate-check site changed: threats_open == 0 stays the gate everywhere; only its computation is now severity-filtered. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -276,7 +276,7 @@ Set via `workflow.*` namespace in config.json (e.g., `"workflow": { "research":
|
||||
| `workflow._auto_chain_active` | boolean | `false` | `true`, `false` | Internal: tracks whether autonomous chaining is active |
|
||||
| `workflow.security_enforcement` | boolean | `true` | `true`, `false` | Enable threat-model-anchored security verification via `/gsd:secure-phase`. When `false`, security checks are skipped entirely |
|
||||
| `workflow.security_asvs_level` | number | `1` | `1`, `2`, `3` | OWASP ASVS verification level. Level 1 = opportunistic, Level 2 = standard, Level 3 = comprehensive. Scales both planner threat-disposition rigor (which threats must be mitigated vs. accepted) and auditor verification depth (grep-level → boundary-placement check → full data-flow trace). See `gsd-core/references/security-asvs-levels.md`. |
|
||||
| `workflow.security_block_on` | string | `"high"` | `"high"`, `"medium"`, `"low"` | Minimum severity that blocks phase advancement |
|
||||
| `workflow.security_block_on` | string | `"high"` | `"critical"`, `"high"`, `"medium"`, `"low"`, `"none"` | Minimum threat severity that blocks phase advancement. The auditor counts only open threats at or above this severity toward the blocking gate (SECURITY.md `threats_open`); `none` disables severity blocking. |
|
||||
| `workflow.post_planning_gaps` | boolean | `true` | `true`, `false` | Post-planning gap report (#2493). After plans are generated, scans REQUIREMENTS.md and CONTEXT.md `<decisions>` against all PLAN.md files and emits a unified `Source \| Item \| Status` table. Non-blocking. Set to `false` to skip Step 13e of plan-phase. _Alias:_ `post_planning_gaps` is the flat-key form used in `CONFIG_DEFAULTS`; `workflow.post_planning_gaps` is the canonical namespaced form. |
|
||||
|
||||
### Ship Fields
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
phase: {N}
|
||||
slug: {phase-slug}
|
||||
status: draft
|
||||
# threats_open = count of OPEN threats at or above workflow.security_block_on severity (the blocking gate)
|
||||
threats_open: 0
|
||||
asvs_level: 1
|
||||
created: {date}
|
||||
@@ -23,11 +24,12 @@ created: {date}
|
||||
|
||||
## Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation | Status |
|
||||
|-----------|----------|-----------|-------------|------------|--------|
|
||||
| T-{N}-01 | {STRIDE category} | {component} | {mitigate / accept / transfer} | {control or reference} | open |
|
||||
| Threat ID | Category | Component | Severity | Disposition | Mitigation | Status |
|
||||
|-----------|----------|-----------|----------|-------------|------------|--------|
|
||||
| T-{N}-01 | {STRIDE category} | {component} | {critical / high / medium / low} | {mitigate / accept / transfer} | {control or reference} | open |
|
||||
|
||||
*Status: open · closed*
|
||||
*Status: open · closed · open — below {block_on} threshold (non-blocking)*
|
||||
*Severity: critical > high > medium > low — only open threats at or above workflow.security_block_on count toward threats_open*
|
||||
*Disposition: mitigate (implementation required) · accept (documented risk) · transfer (third-party)*
|
||||
|
||||
---
|
||||
|
||||
@@ -53,7 +53,7 @@ SUMMARY_FILES=$(ls "${PHASE_DIR}"/*-SUMMARY.md 2>/dev/null)
|
||||
|
||||
### 2a. Read Phase Artifacts
|
||||
|
||||
Read PLAN.md — extract `<threat_model>` block: trust boundaries, STRIDE register (`threat_id`, `category`, `component`, `disposition`, `mitigation_plan`).
|
||||
Read PLAN.md — extract `<threat_model>` block: trust boundaries, STRIDE register (`threat_id`, `category`, `component`, `severity`, `disposition`, `mitigation_plan`).
|
||||
|
||||
### 2b. Read Summary Threat Flags
|
||||
|
||||
@@ -61,7 +61,7 @@ Read SUMMARY.md — extract `## Threat Flags` entries.
|
||||
|
||||
### 2c. Build Threat Register
|
||||
|
||||
Per threat: `{ threat_id, category, component, disposition, mitigation_pattern, files_to_check }`
|
||||
Per threat: `{ threat_id, category, component, severity, disposition, mitigation_pattern, files_to_check }`
|
||||
|
||||
Also set `register_authored_at_plan_time: true` if **at least one** PLAN file contained a parseable `<threat_model>` block; `false` if no PLAN files had any `<threat_model>` block (legacy phase authored before formal threat modelling was standard).
|
||||
|
||||
@@ -74,10 +74,10 @@ Classify each threat:
|
||||
| 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 }`
|
||||
Build: `{ threat_id, category, component, severity, disposition, status, evidence }`
|
||||
|
||||
**Short-circuit rule:**
|
||||
- If `threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level == 1` → skip to Step 6 directly. All plan-time threats are verified CLOSED at L1 grep-depth; no deeper verification required.
|
||||
- If `threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level == 1` → skip to Step 6 directly. No open threats at or above the block threshold remain (threats_open: 0); below-threshold open threats may remain and are non-blocking. L1 grep-depth is sufficient; no deeper verification required.
|
||||
- If `threats_open: 0 AND register_authored_at_plan_time: true AND asvs_level >= 2` → **do NOT skip**. The preliminary threat classification is grep-level (L1 depth) and is insufficient for L2/L3. Proceed to Step 5 (spawn the auditor) so that L2 boundary-placement checks and L3 end-to-end trace checks are performed. Skipping the auditor here would defeat ASVS level scaling for "clean" phases.
|
||||
- If `threats_open: 0 AND register_authored_at_plan_time: false` → **do NOT skip**. Empty-by-no-planning must not rubber-stamp a clean SECURITY.md. Proceed to Step 5 in **retroactive-STRIDE mode** — the auditor builds a register from implementation files first, then verifies mitigations.
|
||||
- If `threats_open > 0` → proceed to Step 4 (present threat plan to user).
|
||||
@@ -146,7 +146,7 @@ Handle return:
|
||||
|
||||
```
|
||||
GSD > PHASE {N} SECURITY BLOCKED
|
||||
{K} threats open — phase advancement blocked until threats_open: 0
|
||||
{K} blocking 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.
|
||||
```
|
||||
@@ -164,7 +164,7 @@ gsd_run query commit "docs(phase-${PHASE}): add/update security threat verificat
|
||||
**Secured (threats_open: 0):**
|
||||
```
|
||||
GSD > PHASE {N} THREAT-SECURE
|
||||
threats_open: 0 — all threats have dispositions.
|
||||
threats_open: 0 — no blocking threats remain (threats_open: 0).
|
||||
▶ /gsd:validate-phase {N} validate test coverage
|
||||
▶ /gsd:verify-work {N} run UAT
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user