From 7d3fe7bb72448d96e829273e29fcf7e5bfde7406 Mon Sep 17 00:00:00 2001 From: zippon Date: Thu, 9 Apr 2026 23:57:43 +0530 Subject: [PATCH 01/22] =?UTF-8?q?feat:=20@confirmed=20verb,=20@feature=20f?= =?UTF-8?q?iltering,=20pentest=20integration,=20expanded=20report=20?= =?UTF-8?q?=E2=80=94=20v1.5.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New annotation verbs: - @confirmed: mark a threat as verified exploitable (pentest/scan evidence), distinct from @exposes (theoretical). Parser, model, SARIF error-level, CLI status, dashboard, MCP lookup, and LLM report support. - @feature "Name": tag files with a product feature name; drives --feature filtering across status, report, dashboard, and the new /feature TUI command. New commands: - guardlink translate: generate CERT-X-GEN pentest templates from threat findings (all agent backends: --claude-code, --codex, --gemini, --cursor, --windsurf, --clipboard) - guardlink ask: natural-language questions about the threat model and codebase Pentest integration: - Loads CXG scan results from .guardlink/pentest-findings/ (JSON) - Injects findings as context into threat-report and AI analyses - Dashboard gains a Pentest Findings sidebar section with scan tables and detail drawers - New PentestFinding/PentestScanResult/PentestData interfaces in src/analyze/index.ts Expanded guardlink report (10 structured sections): - Application Overview, Scope, Architecture, Key Flows & Sequence, Data Inventory, Roles & Access, Dependencies, Secrets & Credentials, Logging & Audit, AI/ML System Details (conditional) - Reads .guardlink/prompt.md for Application Overview (created by init/sync) - Confirmed findings row in Executive Summary table - GuardLink version + git commit/branch in report header New files: - src/parser/feature-filter.ts: listFeatures(), filterByFeature(), getFeatureSummaries() - src/report/sequence.ts: Mermaid sequenceDiagram generator from @flows annotations Docs: CHANGELOG.md v1.5.0 entry, README pentest integration section, @feature in annotation table + commands table, SPEC.md @feature spec, GUARDLINK_REFERENCE.md translate/ask commands. Co-Authored-By: Claude Sonnet 4.6 --- AGENTS.md | 47 +- CHANGELOG.md | 50 + CLAUDE.md | 47 +- README.md | 30 +- docs/GUARDLINK_REFERENCE.md | 23 +- docs/SPEC.md | 36 +- package-lock.json | 4 +- src/agents/index.ts | 2 +- src/agents/prompts.ts | 458 +++++- src/analyze/index.ts | 210 ++- src/analyze/prompts.ts | 18 +- src/analyzer/sarif.ts | 32 +- src/cli/index.ts | 393 ++++- src/dashboard/data.ts | 24 + src/dashboard/generate.ts | 579 ++++++- src/diff/engine.ts | 8 +- src/init/index.ts | 29 +- src/init/templates.ts | 79 +- src/mcp/lookup.ts | 35 + src/mcp/server.ts | 27 +- src/parser/clear.ts | 2 +- src/parser/feature-filter.ts | 133 ++ src/parser/index.ts | 2 + src/parser/parse-line.ts | 22 +- src/parser/parse-project.ts | 23 +- src/parser/validate.ts | 7 + src/report/index.ts | 1 + src/report/report.ts | 1013 +++++++++++- src/report/sequence.ts | 165 ++ src/tui/commands.ts | 91 +- src/tui/index.ts | 6 +- src/types/index.ts | 39 + src/workspace/merge.ts | 8 +- tests/fixtures/all-annotations.ts | 1 + tests/parser.test.ts | 24 + threat-dashboard.html | 2555 +++++++++++++++++++---------- 36 files changed, 5233 insertions(+), 990 deletions(-) create mode 100644 src/parser/feature-filter.ts create mode 100644 src/report/sequence.ts diff --git a/AGENTS.md b/AGENTS.md index bf121a4..018fad1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,11 +14,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 1. **Annotate new code.** When you add a function, endpoint, or module that handles user input, accesses data, crosses a trust boundary, or could fail in a security-relevant way — add `@exposes`, `@mitigates`, `@flows`, `@handles`, or at minimum `@comment` annotations. This is not optional. 2. **NEVER write `@accepts`.** That is a human-only governance decision. When you find a risk with no mitigation in code, write `@exposes` to document the risk + `@audit` to flag it for human review + `@comment` to suggest potential controls. -3. Do not delete or mangle existing annotations. Treat them as part of the code. Edit only when intentionally changing the threat model. -4. Definitions (`@asset`, `@threat`, `@control` with `(#id)`) live in `.guardlink/definitions.ts`. Reuse existing `#id`s — never redefine. If you need a new asset or threat, add the definition there first, then reference it in source files. -5. Source files use relationship verbs only: `@mitigates`, `@exposes`, `@flows`, `@handles`, `@boundary`, `@comment`, `@validates`, `@audit`, `@owns`, `@assumes`, `@transfers`. -6. Write coupled annotation blocks that tell a complete story: risk + control (or audit) + data flow + context note. Never write a lone `@exposes` without follow-up. -7. Avoid `@shield` unless a human explicitly asks to hide code from AI — it creates blind spots. +3. **Use `@confirmed` for verified exploits.** When a pentest, CXG scan, or manual reproduction proves a threat is exploitable, mark it with `@confirmed #threat on Asset [severity] -- "evidence"`. This is distinct from `@exposes` (theoretical) — `@confirmed` means real, verified, not a false positive. Include severity based on actual observed impact. +4. Do not delete or mangle existing annotations. Treat them as part of the code. Edit only when intentionally changing the threat model. +5. Definitions (`@asset`, `@threat`, `@control` with `(#id)`) live in `.guardlink/definitions.ts`. Reuse existing `#id`s — never redefine. If you need a new asset or threat, add the definition there first, then reference it in source files. +6. Source files use relationship verbs only: `@mitigates`, `@exposes`, `@confirmed`, `@flows`, `@handles`, `@boundary`, `@comment`, `@validates`, `@audit`, `@owns`, `@assumes`, `@transfers`, `@feature`. +7. Write coupled annotation blocks that tell a complete story: risk + control (or audit) + data flow + context note. Never write a lone `@exposes` without follow-up. +8. Avoid `@shield` unless a human explicitly asks to hide code from AI — it creates blind spots. ### Workflow (while coding) @@ -43,6 +44,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c @handles pii on App.API -- "Processes email and session token" @validates #prepared-stmts for App.API -- "sqlInjectionTest.ts ensures placeholders used" @audit App.API -- "Token rotation logic needs crypto review" +@confirmed #sqli on App.API [critical] cwe:CWE-89 -- "Pentest verified: raw SQL injection via email param" +@feature "SSO Login" -- "Single sign-on authentication flow" @owns security-team for App.API -- "Team responsible for reviews" @comment -- "Rate limit: 100 req/15min via express-rate-limit" ``` @@ -52,7 +55,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Current Definitions (REUSE these IDs — do NOT redefine) **Assets:** #parser (GuardLink,Parser), #cli (GuardLink,CLI), #tui (GuardLink,TUI), #mcp (GuardLink,MCP), #llm-client (GuardLink,LLM_Client), #dashboard (GuardLink,Dashboard), #init (GuardLink,Init), #agent-launcher (GuardLink,Agent_Launcher), #diff (GuardLink,Diff), #report (GuardLink,Report), #sarif (GuardLink,SARIF), #suggest (GuardLink,Suggest), #workspace-link (Workspace,Link), #merge-engine (Workspace,Merge), #report-metadata (Workspace,Metadata), #workspace-config (Workspace,Config) -**Threats:** #path-traversal (Path_Traversal) [high], #cmd-injection (Command_Injection) [critical], #xss (Cross_Site_Scripting) [high], #api-key-exposure (API_Key_Exposure) [high], #ssrf (Server_Side_Request_Forgery) [medium], #redos (ReDoS) [medium], #arbitrary-write (Arbitrary_File_Write) [high], #prompt-injection (Prompt_Injection) [medium], #dos (Denial_of_Service) [medium], #data-exposure (Sensitive_Data_Exposure) [medium], #insecure-deser (Insecure_Deserialization) [medium], #child-proc-injection (Child_Process_Injection) [high], #info-disclosure (Information_Disclosure) [low], #tag-collision (Tag_Collision) [medium], #config-tamper (Config_Tampering) [medium] +**Threats:** #path-traversal (Path_Traversal) [high], #cmd-injection (Command_Injection) [critical], #xss (Cross_Site_Scripting) [high], #api-key-exposure (API_Key_Exposure) [high], #ssrf (Server_Side_Request_Forgery) [medium], #redos (ReDoS) [medium], #arbitrary-write (Arbitrary_File_Write) [high], #prompt-injection (Prompt_Injection) [medium], #dos (Denial_of_Service) [medium], #data-exposure (Sensitive_Data_Exposure) [medium], #insecure-deser (Insecure_Deserialization) [medium], #child-proc-injection (Child_Process_Injection) [high], #info-disclosure (Information_Disclosure) [low], #secret-exposure (Secret_Exposure) [critical], #tag-collision (Tag_Collision) [medium], #config-tamper (Config_Tampering) [medium] **Controls:** #path-validation (Path_Validation), #input-sanitize (Input_Sanitization), #output-encoding (Output_Encoding), #key-redaction (Key_Redaction), #process-sandbox (Process_Sandboxing), #config-validation (Config_Validation), #resource-limits (Resource_Limits), #param-commands (Parameterized_Commands), #glob-filtering (Glob_Pattern_Filtering), #regex-anchoring (Regex_Anchoring), #prefix-ownership (Prefix_Ownership), #yaml-validation (YAML_Validation) ### Open Exposures (need @mitigates or @audit) @@ -60,16 +63,17 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #agent-launcher exposed to #prompt-injection [medium] (src/agents/launcher.ts:13) - #agent-launcher exposed to #dos [low] (src/agents/launcher.ts:15) - #agent-launcher exposed to #prompt-injection [high] (src/agents/prompts.ts:6) +- #agent-launcher exposed to #config-tamper [medium] (src/agents/prompts.ts:10) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) -- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:16) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:33) - #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) - #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -82,24 +86,29 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #agent-launcher -> AgentProcess via spawn - AgentProcess -> #agent-launcher via stdout - UserPrompt -> #agent-launcher via buildAnnotatePrompt +- UserPrompt -> #agent-launcher via buildTranslatePrompt +- UserPrompt -> #agent-launcher via buildAskPrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync +- PentestFindings -> #llm-client via readFileSync - LLMConfig -> #llm-client via chatCompletion - #llm-client -> LLMProvider via fetch - LLMProvider -> #llm-client via response - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch -- ProjectFiles -> #llm-client via readFileSync -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return -- ... and 48 more +- ... and 52 more + +### Features (filter with `--feature`) + +- "Dashboard" +- "MCP Integration" ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +305 annotations, 16 assets, 16 threats, 12 controls, 61 exposures, 0 confirmed, 46 mitigations, 72 flows, 2 features > **Note:** This section is auto-generated. Run `guardlink sync` to update after code changes. > Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) should reference these IDs @@ -116,6 +125,16 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + + + + + + diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6ffd3..25b4ecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,56 @@ All notable changes to GuardLink CLI will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.5.0] — 2026-04-09 + +### Added + +- **`@confirmed` annotation** — New verb for verified exploitable findings. Distinct from `@exposes` (theoretical) and `@accepts` (governance). Syntax: `@confirmed #threat on Asset [severity] cwe:CWE-NNN -- "evidence"`. A `@confirmed` annotation means the threat has been proven exploitable through pentest, automated CXG scan with reproducible evidence, or manual reproduction — not a false positive. Full pipeline: parser, model assembly, dangling-ref validation, SARIF `error`-level export, CLI `status` output, dashboard emphasis, LLM report inclusion, MCP `guardlink_lookup "confirmed"`. + +- **`@feature` annotation** — New metadata verb to tag files/code with a named product feature. Syntax: `@feature "Feature Name" -- "description"`. Association is file-level: all annotations in a file with `@feature "X"` are considered part of that feature. Enables feature-scoped filtering across all output modes. + +- **Feature filtering (`--feature` flag)** — `guardlink status`, `guardlink report`, and `guardlink dashboard` all gain `--feature ` (comma-separated). Filters all output — assets, threats, exposures, flows — to files tagged with the named feature(s). Dashboard gets a live feature filter dropdown in the header with a dismissible banner. TUI gains `/feature [name]` command to list features or drill into one. + +- **`guardlink translate [prompt]`** — New command that translates GuardLink threat model findings into CERT-X-GEN (CXG) pentest templates (generation only, no execution). Supports all agent backends: `--claude-code`, `--codex`, `--gemini`, `--cursor`, `--windsurf`, `--clipboard`. Reads CXG reference docs and skeleton templates from `GUARDLINK_CXG_ROOT` env or configured default path. + +- **`guardlink ask `** — New command that answers natural-language questions about the threat model and codebase context, launching an AI agent with full model serialization as context. + +- **Pentest integration** — GuardLink now loads CXG scan results from `.guardlink/pentest-findings/` (JSON) and template metadata from `.guardlink/cxg-templates/`. New interfaces: `PentestFinding`, `PentestScanResult`, `PentestTemplate`, `PentestData`. Findings are injected as a `` block into AI threat reports, `guardlink threat-report`, and the dashboard. Dashboard gains a dedicated **Pentest Findings** sidebar section with scan summary tables and per-finding detail drawers. + +- **Expanded threat model report** (`guardlink report`) — `generateReport()` now produces 10 structured sections (was: Executive Summary + tables): + 1. Application Overview (auto-populated from `.guardlink/prompt.md` if present) + 2. Scope of This Threat Model + 3. Architecture (Mermaid DFD) + 4. Key Flows & Sequence (new Mermaid sequence diagram from `@flows`) + 5. Data Inventory + 6. Roles & Access + 7. Dependencies + 8. Secrets, Keys & Credential Management + 9. Logging, Monitoring & Audit + 10. AI/ML System Details (conditional — emitted only when AI-related threats are detected) + + Report header now includes GuardLink version and git commit/branch from metadata. Confirmed exploitable findings appear as a row in the Executive Summary table. + +- **Sequence diagram** (`src/report/sequence.ts`) — New Mermaid `sequenceDiagram` generator built from `@flows` annotations, showing step-by-step participant interactions. Used in the Key Flows & Sequence report section. + +- **`.guardlink/prompt.md`** — `guardlink init` and `guardlink sync` now create this skeleton file. AI annotation agents fill it in with a security-focused project overview (what the app does, components, trust boundaries, data sensitivity, deployment). `guardlink report` reads it and injects the content as the Application Overview section. + +- **SARIF: confirmed exploitable rule** — New `guardlink/confirmed-exploitable` SARIF rule emitting `error`-level results for `@confirmed` annotations. These appear alongside unmitigated exposures in GitHub Advanced Security. + +- **MCP `guardlink_lookup` queries** — Two new query types: `"confirmed"` returns all `@confirmed` verified findings; `"features"` returns all `@feature`-tagged feature names with their associated files. + +- **LLM prompt improvements** — `buildUserMessage()` accepts pentest findings context. AI prompts now distinguish pentest-confirmable threats from governance/design gaps, and teach agents when to use `@confirmed` vs `@exposes` vs `@audit`. + +### Changed + +- **`guardlink status`** — Now prints `@confirmed` findings with a red badge below the exposure list. Accepts `--feature` for filtered output. +- **`guardlink report`** — Accepts `--feature` for scoped reports. Reads `.guardlink/prompt.md` for Application Overview. +- **`guardlink dashboard`** — Accepts `--feature`. Risk score formula now accounts for confirmed finding count. Feature filter dropdown in header. +- **`guardlink threat-report`** — Pentest findings from `.guardlink/pentest-findings/` are automatically included in AI analysis context. AI prompted to emit a dedicated "Pentest Results" section when findings are present. +- **`/gal` TUI command** — Documents `@feature` tagging with examples. +- **SARIF export** — `@confirmed` findings now appear as `error`-level entries under the new rule; `@exposes` severity mapping unchanged. +- **MCP server** — Status tool description updated to reflect confirmed count. `guardlink_lookup` extended with `confirmed` and `features` queries. + ## [1.4.1] — 2026-03-12 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 68c3a9f..e3f2682 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -14,11 +14,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 1. **Annotate new code.** When you add a function, endpoint, or module that handles user input, accesses data, crosses a trust boundary, or could fail in a security-relevant way — add `@exposes`, `@mitigates`, `@flows`, `@handles`, or at minimum `@comment` annotations. This is not optional. 2. **NEVER write `@accepts`.** That is a human-only governance decision. When you find a risk with no mitigation in code, write `@exposes` to document the risk + `@audit` to flag it for human review + `@comment` to suggest potential controls. -3. Do not delete or mangle existing annotations. Treat them as part of the code. Edit only when intentionally changing the threat model. -4. Definitions (`@asset`, `@threat`, `@control` with `(#id)`) live in `.guardlink/definitions.ts`. Reuse existing `#id`s — never redefine. If you need a new asset or threat, add the definition there first, then reference it in source files. -5. Source files use relationship verbs only: `@mitigates`, `@exposes`, `@flows`, `@handles`, `@boundary`, `@comment`, `@validates`, `@audit`, `@owns`, `@assumes`, `@transfers`. -6. Write coupled annotation blocks that tell a complete story: risk + control (or audit) + data flow + context note. Never write a lone `@exposes` without follow-up. -7. Avoid `@shield` unless a human explicitly asks to hide code from AI — it creates blind spots. +3. **Use `@confirmed` for verified exploits.** When a pentest, CXG scan, or manual reproduction proves a threat is exploitable, mark it with `@confirmed #threat on Asset [severity] -- "evidence"`. This is distinct from `@exposes` (theoretical) — `@confirmed` means real, verified, not a false positive. Include severity based on actual observed impact. +4. Do not delete or mangle existing annotations. Treat them as part of the code. Edit only when intentionally changing the threat model. +5. Definitions (`@asset`, `@threat`, `@control` with `(#id)`) live in `.guardlink/definitions.ts`. Reuse existing `#id`s — never redefine. If you need a new asset or threat, add the definition there first, then reference it in source files. +6. Source files use relationship verbs only: `@mitigates`, `@exposes`, `@confirmed`, `@flows`, `@handles`, `@boundary`, `@comment`, `@validates`, `@audit`, `@owns`, `@assumes`, `@transfers`, `@feature`. +7. Write coupled annotation blocks that tell a complete story: risk + control (or audit) + data flow + context note. Never write a lone `@exposes` without follow-up. +8. Avoid `@shield` unless a human explicitly asks to hide code from AI — it creates blind spots. ### Workflow (while coding) @@ -43,6 +44,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c @handles pii on App.API -- "Processes email and session token" @validates #prepared-stmts for App.API -- "sqlInjectionTest.ts ensures placeholders used" @audit App.API -- "Token rotation logic needs crypto review" +@confirmed #sqli on App.API [critical] cwe:CWE-89 -- "Pentest verified: raw SQL injection via email param" +@feature "SSO Login" -- "Single sign-on authentication flow" @owns security-team for App.API -- "Team responsible for reviews" @comment -- "Rate limit: 100 req/15min via express-rate-limit" ``` @@ -52,7 +55,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Current Definitions (REUSE these IDs — do NOT redefine) **Assets:** #parser (GuardLink,Parser), #cli (GuardLink,CLI), #tui (GuardLink,TUI), #mcp (GuardLink,MCP), #llm-client (GuardLink,LLM_Client), #dashboard (GuardLink,Dashboard), #init (GuardLink,Init), #agent-launcher (GuardLink,Agent_Launcher), #diff (GuardLink,Diff), #report (GuardLink,Report), #sarif (GuardLink,SARIF), #suggest (GuardLink,Suggest), #workspace-link (Workspace,Link), #merge-engine (Workspace,Merge), #report-metadata (Workspace,Metadata), #workspace-config (Workspace,Config) -**Threats:** #path-traversal (Path_Traversal) [high], #cmd-injection (Command_Injection) [critical], #xss (Cross_Site_Scripting) [high], #api-key-exposure (API_Key_Exposure) [high], #ssrf (Server_Side_Request_Forgery) [medium], #redos (ReDoS) [medium], #arbitrary-write (Arbitrary_File_Write) [high], #prompt-injection (Prompt_Injection) [medium], #dos (Denial_of_Service) [medium], #data-exposure (Sensitive_Data_Exposure) [medium], #insecure-deser (Insecure_Deserialization) [medium], #child-proc-injection (Child_Process_Injection) [high], #info-disclosure (Information_Disclosure) [low], #tag-collision (Tag_Collision) [medium], #config-tamper (Config_Tampering) [medium] +**Threats:** #path-traversal (Path_Traversal) [high], #cmd-injection (Command_Injection) [critical], #xss (Cross_Site_Scripting) [high], #api-key-exposure (API_Key_Exposure) [high], #ssrf (Server_Side_Request_Forgery) [medium], #redos (ReDoS) [medium], #arbitrary-write (Arbitrary_File_Write) [high], #prompt-injection (Prompt_Injection) [medium], #dos (Denial_of_Service) [medium], #data-exposure (Sensitive_Data_Exposure) [medium], #insecure-deser (Insecure_Deserialization) [medium], #child-proc-injection (Child_Process_Injection) [high], #info-disclosure (Information_Disclosure) [low], #secret-exposure (Secret_Exposure) [critical], #tag-collision (Tag_Collision) [medium], #config-tamper (Config_Tampering) [medium] **Controls:** #path-validation (Path_Validation), #input-sanitize (Input_Sanitization), #output-encoding (Output_Encoding), #key-redaction (Key_Redaction), #process-sandbox (Process_Sandboxing), #config-validation (Config_Validation), #resource-limits (Resource_Limits), #param-commands (Parameterized_Commands), #glob-filtering (Glob_Pattern_Filtering), #regex-anchoring (Regex_Anchoring), #prefix-ownership (Prefix_Ownership), #yaml-validation (YAML_Validation) ### Open Exposures (need @mitigates or @audit) @@ -60,16 +63,17 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #agent-launcher exposed to #prompt-injection [medium] (src/agents/launcher.ts:13) - #agent-launcher exposed to #dos [low] (src/agents/launcher.ts:15) - #agent-launcher exposed to #prompt-injection [high] (src/agents/prompts.ts:6) +- #agent-launcher exposed to #config-tamper [medium] (src/agents/prompts.ts:10) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) -- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:16) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:33) - #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) - #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -82,24 +86,29 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #agent-launcher -> AgentProcess via spawn - AgentProcess -> #agent-launcher via stdout - UserPrompt -> #agent-launcher via buildAnnotatePrompt +- UserPrompt -> #agent-launcher via buildTranslatePrompt +- UserPrompt -> #agent-launcher via buildAskPrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync +- PentestFindings -> #llm-client via readFileSync - LLMConfig -> #llm-client via chatCompletion - #llm-client -> LLMProvider via fetch - LLMProvider -> #llm-client via response - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch -- ProjectFiles -> #llm-client via readFileSync -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return -- ... and 48 more +- ... and 52 more + +### Features (filter with `--feature`) + +- "Dashboard" +- "MCP Integration" ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +305 annotations, 16 assets, 16 threats, 12 controls, 61 exposures, 0 confirmed, 46 mitigations, 72 flows, 2 features > **Note:** This section is auto-generated. Run `guardlink sync` to update after code changes. > Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) should reference these IDs @@ -116,6 +125,16 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + + + + + + diff --git a/README.md b/README.md index aab6572..1674431 100644 --- a/README.md +++ b/README.md @@ -178,6 +178,8 @@ GuardLink ships an MCP server and behavioral directives for AI coding agents. Af | `guardlink sarif [dir]` | Export unmitigated exposures as SARIF 2.1.0 | | `guardlink threat-report [fw]` | AI threat report (stride/dread/pasta/attacker/rapid/general) | | `guardlink threat-reports` | List saved AI threat reports | +| `guardlink translate [prompt]` | Generate CERT-X-GEN pentest templates from threat model findings | +| `guardlink ask ` | Ask a natural-language question about the threat model and codebase | | `guardlink review [dir]` | Interactive governance review — accept, remediate, or skip unmitigated exposures | | `guardlink review --list` | List reviewable exposures without prompting | | `guardlink clear [dir]` | Remove all annotations from source files (with `--dry-run` preview) | @@ -241,6 +243,8 @@ GuardLink annotations go in comments in any language. The parser supports `//`, | `@control` | Define a security control | `@control WAF (#waf)` | | `@mitigates` | Control protects asset against threat | `@mitigates #api against #sqli using #prepared-stmts` | | `@exposes` | Asset vulnerable to threat | `@exposes #api to #xss [P1]` | +| `@confirmed` | Threat verified exploitable (pentest/scan) | `@confirmed #sqli on #api [critical] -- "Verified in pen test"` | +| `@feature` | Tag code with a product feature name | `@feature "SSO Login" -- "Single sign-on authentication flow"` | | `@accepts` | Risk acknowledged | `@accepts #dos on #api -- "By design"` | | `@transfers` | Risk moved between assets | `@transfers #sqli from #api to #db` | | `@flow` | Data flow between assets | `@flow #api -> #db via "SQL"` | @@ -303,7 +307,31 @@ For workspace setups, GuardLink provides two additional workflow templates: a pe ### SARIF -`guardlink sarif` exports unmitigated exposures as SARIF 2.1.0. Upload to GitHub Advanced Security and every `@exposes` appears as a code scanning alert with file, line, severity, and CWE. +`guardlink sarif` exports unmitigated exposures and `@confirmed` findings as SARIF 2.1.0. Upload to GitHub Advanced Security: unmitigated `@exposes` appear as warnings or errors by severity; `@confirmed` exploitable findings appear as errors. + +### Pentest Integration + +GuardLink bridges threat modeling and penetration testing in both directions. + +**From threat model to pentest templates** — `guardlink translate` reads your `@exposes` annotations and generates CERT-X-GEN (CXG) pentest template stubs targeting the specific threats you've documented. Run it with any agent backend: + +```bash +guardlink translate --claude-code +guardlink translate "focus on injection paths" --clipboard +``` + +**From pentest results back to the threat model** — Drop CXG scan result JSON files into `.guardlink/pentest-findings/`. GuardLink reads them automatically and: +- Injects findings as empirical evidence in `guardlink threat-report` and AI analyses +- Displays a **Pentest Findings** section in `guardlink dashboard` +- Teaches agents to cross-reference scan results against `@exposes` annotations + +**Marking verified findings** — When a pentest or scan proves a threat is exploitable, add `@confirmed` to close the loop: + +```typescript +// @confirmed #sqli on App.API [critical] cwe:CWE-89 -- "CXG scan 2026-04: time-based blind SQLi on /login confirmed" +``` + +`@confirmed` is distinct from `@exposes` (hypothesis) — it means real, verified, not a false positive. --- diff --git a/docs/GUARDLINK_REFERENCE.md b/docs/GUARDLINK_REFERENCE.md index 5d059c9..8afc4b1 100644 --- a/docs/GUARDLINK_REFERENCE.md +++ b/docs/GUARDLINK_REFERENCE.md @@ -12,6 +12,7 @@ DEFINE @asset (#id) -- "description" RELATE @mitigates against <#threat> using <#control> -- "how" @exposes to <#threat> [severity] cwe:CWE-NNN -- "what's wrong" + @confirmed <#threat> on [severity] cwe:CWE-NNN -- "verified evidence" @accepts <#threat> on -- "HUMAN-ONLY — AI agents must use @audit instead" @transfers <#threat> from to -- "who handles it" @@ -26,6 +27,8 @@ LIFECYCLE @handles on @assumes -- "unverified assumption" +METADATA @feature "Feature Name" -- "tag code with a feature for filtering" + COMMENT @comment -- "security-relevant developer note" PROTECT @shield -- "reason" @@ -55,10 +58,12 @@ Append after severity: `cwe:CWE-89`, `owasp:A03:2021`, `capec:CAPEC-66`, `attack | Writing new endpoint/handler | `@exposes` + `@mitigates` (or `@audit`) + `@flows` + `@comment` — tell the complete story | | New service/component | `@asset` in definitions, then reference in source | | Security gap exists | `@exposes Asset to #threat` + `@audit Asset` | +| Threat verified exploitable | `@confirmed #threat on Asset [severity] -- "pentest/scan evidence"` | | Risk with no fix yet | `@audit Asset` + `@comment` explaining potential controls. NEVER `@accepts`. | | Implementing a fix | `@mitigates Asset against #threat using #control` | | Processing sensitive data | `@handles pii on Asset` | | Proprietary algorithm | `@shield:begin` ... `@shield:end` (only if human requests it) | +| Tagging code to a feature | `@feature "SSO Login" -- "Single sign-on flow"` | | Unsure which annotation | `@comment -- "describe what you see"` | ## CLI Commands @@ -80,6 +85,8 @@ guardlink diff [ref] # Compare threat model against a git ref guardlink threat-report # AI threat report (see frameworks below) guardlink threat-reports # List saved threat reports guardlink annotate # Launch coding agent to add annotations +guardlink translate [prompt] # Generate CERT-X-GEN pentest templates from threat findings +guardlink ask # Ask questions about the threat model and codebase guardlink config # Manage LLM provider / CLI agent configuration # Governance & Maintenance @@ -88,11 +95,18 @@ guardlink review --list [--severity X] # List reviewable exposures without prom guardlink clear [dir] [--dry-run] # Remove all annotations from source files guardlink sync [dir] # Sync agent instruction files with current threat model guardlink unannotated [dir] # List source files with no annotations +guardlink feature list [dir] # List all @feature tags with stats +guardlink feature show # Show threat model for a specific feature # Interactive guardlink tui [dir] # Interactive TUI: slash commands + AI chat guardlink mcp # Start MCP server (stdio) for Claude Code, Cursor, etc. guardlink gal # Display GAL annotation language quick reference + +# Feature filtering (--feature flag on report, dashboard, status, translate) +guardlink report . --feature "SSO Login" # Report filtered to feature +guardlink dashboard . --feature "SSO,Payments" # Dashboard filtered to features +guardlink status . --feature "SSO Login" # Status filtered to feature ``` ## Threat Report Frameworks @@ -153,6 +167,7 @@ Run `guardlink tui` for the interactive terminal interface: /diff [ref] Compare model vs git ref (default: HEAD~1) /sarif [-o file] Export SARIF 2.1.0 /gal GAL annotation language guide +/feature List all @feature tags (freeform text) Chat about your threat model with AI ``` @@ -160,12 +175,12 @@ Run `guardlink tui` for the interactive terminal interface: 1. **@boundary requires TWO assets**: `@boundary between #A and #B` or `@boundary #A | #B`. 2. **@flows is ONE source → ONE target per line**: `@flows -> via `. -3. **@exposes / @mitigates require defined #id refs**: Every `#id` must have a definition in `.guardlink/definitions.*`. -4. **Severity in square brackets**: `[P0]` `[P1]` `[P2]` `[P3]` or `[critical]` `[high]` `[medium]` `[low]`. Goes AFTER the threat ref. +3. **@exposes / @mitigates / @confirmed require defined #id refs**: Every `#id` must have a definition in `.guardlink/definitions.*`. +4. **Severity in square brackets**: `[P0]` `[P1]` `[P2]` `[P3]` or `[critical]` `[high]` `[medium]` `[low]`. Goes AFTER the threat ref on `@exposes`; on `@confirmed` it reflects **verified** impact (optional but recommended). 5. **Descriptions in double quotes after --**: `-- "description text here"`. 6. **IDs use parentheses in definitions, hash in references**: Define `(#sqli)`, reference `#sqli`. 7. **Asset references**: Use `#id` or `Dotted.Path` — no spaces or special chars. -8. **External refs space-separated after severity**: `cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66`. +8. **External refs space-separated after severity**: `cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66` (on `@threat`, `@exposes`, `@confirmed`). 9. **@comment always needs -- and quotes**: `@comment -- "your note here"`. 10. **One annotation per comment line.** Do NOT put two @verbs on the same line. @@ -173,7 +188,7 @@ Run `guardlink tui` for the interactive terminal interface: When connected via `.mcp.json`, use: - `guardlink_parse` — parse annotations, return threat model -- `guardlink_lookup` — query threats, controls, exposures by ID +- `guardlink_lookup` — query threats, controls, exposures by ID (try `unmitigated`, `confirmed`) - `guardlink_suggest` — get annotation suggestions for a file - `guardlink_validate` — check for syntax errors - `guardlink_status` — coverage stats diff --git a/docs/SPEC.md b/docs/SPEC.md index 02318cf..24d2a4f 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -24,7 +24,7 @@ GuardLink extends the original ThreatSpec specification (dormant since 2020) wit **1.3. Annotations are structured data.** While readable as English, every annotation has a deterministic grammar that can be parsed by regex into typed data structures. Ambiguous or free-form syntax is avoided. -**1.4. Annotations capture intent, not implementation.** An `@exposes` annotation says "this code is vulnerable to X" — it does not describe how the vulnerability works. A `@mitigates` annotation says "this code defends against X" — it does not duplicate the implementation. The annotation is a pointer from code to the threat model. +**1.4. Annotations capture intent, not implementation.** An `@exposes` annotation says "this code is vulnerable to X" — it does not describe how the vulnerability works. A `@mitigates` annotation says "this code defends against X" — it does not duplicate the implementation. A `@confirmed` annotation says "this threat was verified exploitable here" with evidence summarized in the description — it is not a false positive. The annotation is a pointer from code to the threat model. **1.5. Annotations are incremental.** A codebase does not need 100% annotation coverage to be useful. A single `@exposes` annotation on a critical endpoint is valuable. Tools should work with partial annotation and measure coverage over time. @@ -337,6 +337,40 @@ def get_user(user_id: int): return db.get_user(user_id) # Anyone can access any user ``` +#### `@confirmed` — Verified Exploitable Finding + +``` +@confirmed on [] [] [-- ""] +``` + +Declares that a threat against an asset has been **verified** through penetration testing, automated security scanning with reproducible evidence, or manual exploitation — not a theoretical risk. This is distinct from `@exposes` (developer hypothesis) and from `@accepts` (governance decision to live with risk). Tools should surface `@confirmed` as highest-priority findings (e.g., SARIF `error`, dashboard emphasis). + +The optional `[severity]` reflects **observed** impact from verification; it may differ from the severity on a matching `@exposes` or `@threat` definition. + +```typescript +// @confirmed #secret-exposure on App.Config [critical] cwe:CWE-798 -- "TruffleHog + manual: live API key in committed .env.example, verified against provider" +``` + +#### `@feature` — Feature Tag + +``` +@feature "" [-- ""] +``` + +Tags the file with a named product feature. All annotations in a file that carries `@feature "X"` are associated with that feature. A file may carry multiple `@feature` tags. Feature names are free-form quoted strings. + +Feature tags are metadata only — they have no effect on validation or coverage scoring. Their purpose is to allow threat model consumers (CLI, dashboard, reports) to filter or scope the model to a subset of the system: + +- `guardlink status . --feature "SSO Login"` — coverage stats for one feature +- `guardlink report . --feature "Payments"` — report scoped to that feature +- Dashboard feature filter dropdown + +```typescript +// @feature "SSO Login" -- "Single sign-on authentication flow" +// @feature "Payment Processing" +export async function handleOAuthCallback(req: Request) { ... } +``` + #### `@accepts` — Acknowledge a Risk ``` diff --git a/package-lock.json b/package-lock.json index 1598cd4..28998be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "guardlink", - "version": "1.3.0", + "version": "1.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "guardlink", - "version": "1.3.0", + "version": "1.4.1", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", diff --git a/src/agents/index.ts b/src/agents/index.ts index a1bf5c5..36086b4 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -50,4 +50,4 @@ export function agentFromOpts(opts: Record): AgentEntry | null { export { launchAgentForeground, launchAgentIDE, launchAgent, launchAgentInline, copyToClipboard } from './launcher.js'; export type { InlineResult } from './launcher.js'; -export { buildAnnotatePrompt } from './prompts.js'; +export { buildAnnotatePrompt, buildTranslatePrompt, buildAskPrompt } from './prompts.js'; diff --git a/src/agents/prompts.ts b/src/agents/prompts.ts index 646b7fa..ea7f2a4 100644 --- a/src/agents/prompts.ts +++ b/src/agents/prompts.ts @@ -7,7 +7,11 @@ * @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context" * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths" * @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access" + * @exposes #agent-launcher to #config-tamper [medium] cwe:CWE-15 -- "Translate prompt may read CXG reference paths from environment overrides" + * @audit #agent-launcher -- "Environment override paths are optional convenience; verify trusted local paths in CI" * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input" + * @flows UserPrompt -> #agent-launcher via buildTranslatePrompt -- "Template translation instruction input" + * @flows UserPrompt -> #agent-launcher via buildAskPrompt -- "Threat model question input" * @flows ThreatModel -> #agent-launcher via model -- "Model context injection" * @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output" * @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt" @@ -17,6 +21,18 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import type { ThreatModel } from '../types/index.js'; +const DEFAULT_CXG_ROOT = '/Users/shahidhakim/Downloads/cert-x-gen-fix-template-update-url-migration-and-cli'; +const DEFAULT_CXG_SKELETON_DIR = '/Users/shahidhakim/Downloads/cert-x-gen-fix-template-update-url-migration-and-cli/cert-x-gen-templates-main/templates/skeleton'; + +function readIfExists(path: string, maxChars = 5000): string { + if (!existsSync(path)) return ''; + try { + return readFileSync(path, 'utf-8').slice(0, maxChars); + } catch { + return ''; + } +} + /** * Build a prompt for annotation agents. * @@ -50,6 +66,7 @@ export function buildAnnotatePrompt( const parts = [ `${model.annotations_parsed} annotations`, `${model.exposures.length} exposures`, + ...((model.confirmed || []).length > 0 ? [`${model.confirmed.length} confirmed exploitable`] : []), `${model.assets.length} assets`, `${model.threats.length} threats`, `${model.controls.length} controls`, @@ -262,6 +279,16 @@ Example — what to do when no mitigation exists: Leaving exposures unmitigated is HONEST. The dashboard and reports will surface them as open risks for humans to triage. +### Pentest-Confirmable vs Governance-Only Gaps +When documenting threats, distinguish between: +1. **Pentest-confirmable findings**: testable with concrete I/O behavior (e.g., injection, auth bypass, IDOR, exposed service, unsafe deserialization). Document the risk with @exposes (hypothesis). After a pentest, CXG scan, or manual reproduction **proves** exploitability with evidence, add @confirmed #threat on Asset [severity] -- "evidence summary" — never use @confirmed for guesses or scanner noise without verification. +2. **Governance/design gaps**: important risks that are not directly testable as a penetration test template (e.g., missing ownership process, policy-only controls, broad architectural assumptions with no direct exploit path). + +For governance/design gaps: +- Do NOT force a fake exploit-style exposure. +- Add @audit on the relevant asset with precise reasoning. +- Add @comment suggesting concrete controls or follow-up review tasks. + ### @shield — DO NOT USE Unless Explicitly Asked @shield and @shield:begin/@shield:end block AI coding assistants from reading the annotated code. This means any shielded code becomes invisible to AI tools — they cannot analyze, refactor, or annotate it. @@ -285,6 +312,7 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re \`\`\` // @shield:begin -- "Relationship syntax examples, excluded from parsing" // @exposes #auth to #sqli [P0] cwe:CWE-89 owasp:A03:2021 -- "User input concatenated into query" +// @confirmed #sqli on #auth [critical] cwe:CWE-89 -- "Pentest 2026-04: time-based blind SQLi on /login confirmed" // @mitigates #auth against #sqli using #prepared-stmts -- "Uses parameterized queries via sqlx" // @audit #auth -- "Timing attack risk — needs human review to decide if bcrypt constant-time comparison is sufficient" // @transfers #ddos from #api to #cdn -- "Cloudflare handles L7 DDoS mitigation" @@ -295,6 +323,7 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re // @audit #auth -- "Session token rotation logic needs cryptographic review" // @assumes #auth -- "Upstream API gateway has already validated TLS and rate-limited requests" // @owns security-team for #auth -- "Security team reviews all auth PRs" +// @feature "SSO Login" -- "Single sign-on authentication flow" // @comment -- "Password hashing uses bcrypt with cost factor 12, migration from SHA256 completed in v2.1" // @shield:end \`\`\` @@ -315,6 +344,7 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re 4. **Severity in square brackets**: \`[P0]\` \`[P1]\` \`[P2]\` \`[P3]\` or \`[critical]\` \`[high]\` \`[medium]\` \`[low]\`. Goes AFTER the threat ref in @exposes: \`@exposes #app to #sqli [P0] cwe:CWE-89\` + On @confirmed, severity is optional but recommended — it reflects **verified** impact: \`@confirmed #sqli on #app [critical] -- "evidence"\` 5. **Descriptions in double quotes after --**: \`-- "description text here"\` WRONG: \`@comment "just a note"\` or \`@comment -- note without quotes\` @@ -352,8 +382,432 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re 5. **Use the project's comment style** (// for JS/TS/Go/Rust, # for Python/Ruby/Shell, etc.) -6. **Run validation** via guardlink_validate (MCP) or \`guardlink validate\` to check for errors. +6. **Generate project description.** If \`.guardlink/prompt.md\` exists and contains only the skeleton template + (HTML comments / placeholder headings with no real content), fill it in based on what you learned while + reading the codebase. Write a security-focused project overview covering: + - What the application does and who its users are + - Key components and services + - Trust boundaries (where trust changes between components) + - Data sensitivity (PII, credentials, financial data, etc.) + - Deployment context (cloud, containers, CI/CD, etc.) + This file feeds into \`guardlink report\` as the Application Overview section. + **Do NOT overwrite user-written content** — only fill in the template placeholders. + +7. **Run validation** via guardlink_validate (MCP) or \`guardlink validate\` to check for errors. + +8. **Fix any validation errors** before finishing — especially dangling refs and malformed syntax. +`; +} + +/** + * Build a prompt for translating GuardLink threat model findings into + * CERT-X-GEN (CXG) pentest templates. + */ +export function buildTranslatePrompt( + userPrompt: string, + root: string, + model: ThreatModel | null, +): string { + const cxgRoot = process.env.GUARDLINK_CXG_ROOT || DEFAULT_CXG_ROOT; + const skeletonDir = process.env.GUARDLINK_CXG_SKELETON_DIR || DEFAULT_CXG_SKELETON_DIR; + + const templateGuide = readIfExists(resolve(cxgRoot, 'cert-x-gen-templates-main', 'docs', 'TEMPLATE_GUIDE.md'), 4000); + const promptEngine = readIfExists(resolve(cxgRoot, 'src', 'ai', 'prompt.rs'), 4000); + const yamlSkeleton = readIfExists(resolve(skeletonDir, 'yaml-template-skeleton.yaml'), 5000); + const pythonSkeleton = readIfExists(resolve(skeletonDir, 'python-template-skeleton.py'), 3000); + + let modelSummary = 'No threat model parsed yet.'; + let candidateExposures = ''; + if (model) { + const unmitigated = model.exposures.filter((e) => + !model.mitigations.some((m) => m.asset === e.asset && m.threat === e.threat) + ); + + modelSummary = `Current model: ${model.annotations_parsed} annotations, ${model.exposures.length} exposures, ${(model.confirmed || []).length} confirmed, ${unmitigated.length} unmitigated exposures, ${model.assets.length} assets, ${model.threats.length} threats.`; + if (unmitigated.length > 0) { + const lines = unmitigated.slice(0, 40).map((e) => + `- ${e.asset} -> ${e.threat} [${e.severity || 'unrated'}] (${e.location.file}:${e.location.line})` + ); + candidateExposures = `\n\nUnmitigated exposure candidates:\n${lines.join('\n')}`; + if (unmitigated.length > 40) { + candidateExposures += `\n- ... and ${unmitigated.length - 40} more`; + } + } + } + + const instruction = userPrompt.trim() + ? userPrompt.trim() + : 'Generate CXG pentest templates for all pentest-confirmable high/critical threats first, then medium.'; + + return `You are a senior offensive security engineer translating GuardLink threat-model findings into CERT-X-GEN (CXG) templates. + +## Mission +Convert pentest-confirmable threats into runnable CXG templates. Do NOT execute templates. Only author template files. + +## Current Threat Model +${modelSummary}${candidateExposures} + +## User Request +${instruction} + +## Required CXG CLI Discovery (Do This First) +Before generating final user guidance, discover the actual CLI usage on this machine: +1. Try: \`cxg --help\` +2. Try: \`cxg scan --help\` +3. Try: \`cxg template --help\` +4. If \`cxg\` is not in PATH, try local binary from source checkout (if present): + - \`${cxgRoot}/target/release/cxg --help\` + - \`${cxgRoot}/target/release/cxg scan --help\` + - \`${cxgRoot}/target/release/cxg template --help\` +5. Base user instructions on the commands that actually work. If none work, clearly state the blocker and provide install/build steps first. + +## Required Decision Rule (Critical) +For every candidate threat/exposure: +1. Decide if it is **pentest-confirmable** — meaning it can be validated via: + - Network request/response behavior (HTTP, TCP, etc.) + - Local CLI invocation with crafted inputs (command injection, path traversal, etc.) + - File system operations (symlink attacks, arbitrary writes, config tampering) + - MCP/stdio protocol interactions (JSON-RPC tool calls with malicious payloads) + - Process spawning behavior (canary file creation, shell metacharacter interpretation) +2. If yes: create one or more CXG templates. For local CLI/codebase threats, templates should use \`subprocess.run()\` or \`subprocess.Popen()\` with \`cwd=target\` to invoke the tool under test. +3. If no (pure governance/process/design gap): do NOT create a template. Instead document it as audit-only guidance: + - Include suggested GuardLink @audit text and @comment text for the relevant asset/file. + - Explain briefly why no pentest template is appropriate. + +## Output and File Operations +1. Create templates under: \`.guardlink/cxg-templates/\` +2. Use meaningful filenames like: + - \`.guardlink/cxg-templates/.yaml\` + - or language variants \`.py\`, \`.js\`, \`.go\`, etc. if needed. +3. Write an index file at \`.guardlink/cxg-templates/README.md\` with: + - generated templates list + - mapping: GuardLink threat/exposure -> template file(s) + - "audit-only / no-template" items with suggested @audit annotations +4. CXG scan output goes to: \`.guardlink/pentest-findings/\` (this is where \`guardlink dashboard\` and \`guardlink threat-report\` read pentest results from). Always tell users to output to this path. +5. Do NOT run CXG CLI or execute generated templates. +6. Keep checks non-destructive. +7. You MAY run \`cxg --help\` and other help/listing commands only for usage discovery. Do not run active scans unless user explicitly asks. + +## CXG Format Contract (from source) +Use the project skeleton contract and examples; mirror field names and structure exactly. + +${templateGuide ? `### TEMPLATE_GUIDE excerpt\n${templateGuide}\n` : ''} +${promptEngine ? `### prompt.rs excerpt\n${promptEngine}\n` : ''} +${yamlSkeleton ? `### YAML skeleton excerpt\n${yamlSkeleton}\n` : ''} +${pythonSkeleton ? `### Python skeleton excerpt\n${pythonSkeleton}\n` : ''} + +## Quality Bar +- Each template must include clear metadata: id/name/author/severity/description/tags/references. +- Detection logic must align to the threat and include concrete matchers/assertions. +- Prefer YAML templates for declarative checks; use code templates where procedural logic is required. +- Avoid placeholder TODO logic. +- Keep template logic scoped to the specific threat confirmation. + +## CXG Engine Contract (Critical — templates MUST follow this) +When CXG runs a Python template, it does NOT pass the target as a CLI argument. +Instead, it sets environment variables and expects JSON on stdout. + +### Target resolution (in main / entry point): +\`\`\`python +target = os.environ.get("CERT_X_GEN_PROJECT_ROOT") or args.target or os.environ.get("CERT_X_GEN_TARGET_HOST") +\`\`\` +- \`CERT_X_GEN_PROJECT_ROOT\`: set for local codebase/CLI targets (absolute path). +- \`CERT_X_GEN_TARGET_HOST\`: set for network targets (hostname/IP). +- The positional \`target\` arg MUST use \`nargs="?"\` (optional) since CXG engine passes no argv. + +### Output contract: +- When \`CERT_X_GEN_MODE == "engine"\` (always true under CXG), print ONLY a JSON array to stdout. +- Output \`[]\` (empty array) when no findings — never print plain text in engine mode. +- Use: \`print(json.dumps(findings, indent=2))\` + +### Environment variables available: +| Variable | Value | +|----------|-------| +| \`CERT_X_GEN_MODE\` | Always \`"engine"\` | +| \`CERT_X_GEN_TARGET_HOST\` | Target address (path for local, hostname for network) | +| \`CERT_X_GEN_TARGET_TYPE\` | \`"local"\` or \`"network"\` | +| \`CERT_X_GEN_PROJECT_ROOT\` | Absolute path (local targets only) | +| \`CERT_X_GEN_TARGET_PORT\` | Port number (network targets, default 80) | + +### Running templates with CXG local scope: +\`\`\`bash +cxg scan --scope local://. --template-dir .guardlink/cxg-templates/ --output .guardlink/pentest-findings/guardlink-pentest --output-format json,sarif,html +\`\`\` + +## CXG Evidence Contract (Critical — findings MUST include rich evidence) +CXG parses finding evidence using specific field names. If these fields are missing or empty, +the output report will show blank evidence — making findings impossible to verify. + +### Required evidence structure in every finding dict: +\`\`\`python +"evidence": { + "request": "", + "response": "", + "matched_patterns": ["", ...], # list of STRINGS (not dicts) — e.g. CWE IDs, indicators found, regex matches + "data": { # arbitrary key-value map for all raw evidence details + "key1": "", + "key2": "", + ... + } +} +\`\`\` + +### Rules for populating evidence: +1. **\`request\`**: MUST contain the exact input that triggered the finding. Examples: + - For CLI injection: the full command with payload (e.g., \`npx guardlink annotate "; touch /tmp/canary"\`) + - For MCP tests: the JSON-RPC request body sent to the tool + - For path traversal: the malicious path used (e.g., \`../../etc/passwd\`) + - For config tamper: the environment variable name and injected value + +2. **\`response\`**: MUST contain the raw output that proves the vulnerability. Examples: + - stdout/stderr excerpt from the command execution (up to 2000 chars) + - The MCP JSON-RPC response content + - File contents read from an unexpected location + - Error messages that reveal injection + +3. **\`matched_patterns\`**: MUST be a list of **strings** (CXG drops non-strings). Include: + - Shell error indicators found (e.g., "sh: command not found") + - Sensitive data patterns matched (e.g., "absolute_paths: 5 found") + - CWE/OWASP identifiers relevant to the finding + - Canary strings that proved exploitation + +4. **\`data\`**: Store ALL evidence key-value pairs here. All values must be strings + (use \`json.dumps()\` to serialize complex objects). This is the catch-all for: + - \`canary_created\`: "true" + - \`exit_code\`: "0" + - \`symlink_path\`: "/path/to/symlink" + - \`traversal_root\`: "/etc" + - \`env_var\`: "GUARDLINK_CXG_ROOT" + +### Helper pattern for \`create_finding\`: +Always use a centralized helper that maps your raw evidence dict into the CXG structure: + +\`\`\`python +def create_finding(self, title, description, evidence): + return { + "template_id": self.id, + "title": title, + "severity": self.severity, + "confidence": self.confidence, + "description": description, + "evidence": { + "request": evidence.get("request") or evidence.get("payload") or evidence.get("rpc_request") or + json.dumps({k: v for k, v in evidence.items() + if k not in ("response", "stdout_excerpt", "stderr_excerpt", + "output_excerpt", "response_snippet", "matched_patterns")}, default=str), + "response": evidence.get("response") or evidence.get("stdout_excerpt") or + evidence.get("stderr_excerpt") or evidence.get("output_excerpt") or + evidence.get("response_snippet") or evidence.get("content_snippet") or "", + "matched_patterns": [p if isinstance(p, str) else + (f"{p.get('type','')}: {p.get('count','?')}" if isinstance(p, dict) else str(p)) + for p in (evidence.get("matched_patterns") or [])], + "data": {k: (v if isinstance(v, str) else json.dumps(v, default=str)) + for k, v in evidence.items()}, + }, + "cwe": self.cwe, + "tags": self.tags, + "remediation": "...", + } +\`\`\` + +### What to capture as evidence for each template type: +| Template type | request | response | matched_patterns | +|---|---|---|---| +| CLI injection | Full CLI command with payload | stdout + stderr (first 2000 chars) | Shell indicators, canary proof | +| MCP tool call | JSON-RPC request body | JSON-RPC response body | Sensitive data types found | +| Path traversal | Traversal path used | File/dir content from outside project | Path indicators (/etc, /tmp) | +| Config tamper | Env var name + injected value | Command output with canary | Canary string match | +| Prompt injection | Injected prompt text | LLM/agent output text | Injection markers found | +| Arbitrary write | Symlink/path payload | guardlink clear output showing external files | External paths listed | + +### NEVER do this: +- Do NOT pass raw evidence dicts without the CXG structure — CXG will show empty evidence fields. +- Do NOT put dicts or lists in \`matched_patterns\` — CXG drops non-string entries silently. +- Do NOT skip evidence collection — a finding without evidence is unverifiable. + +## Python Template Boilerplate (MUST use this structure) +Every Python template you create MUST follow this exact \`main()\` structure: + +\`\`\`python +def main(): + parser = argparse.ArgumentParser(description="...") + parser.add_argument("target", nargs="?", help="Project root or target host") + parser.add_argument("--port", type=int, default=0) + parser.add_argument("--json", action="store_true") + args = parser.parse_args() + + template = CertXGenTemplate() + target = os.environ.get("CERT_X_GEN_PROJECT_ROOT") or args.target or os.environ.get("CERT_X_GEN_TARGET_HOST") + if not target: + parser.error("target is required (positional, CERT_X_GEN_PROJECT_ROOT, or CERT_X_GEN_TARGET_HOST)") + + findings = template.execute(target, args.port) + if args.json or os.environ.get("CERT_X_GEN_MODE") == "engine": + print(json.dumps(findings, indent=2)) + elif findings: + for f in findings: + print(f"[{f['severity'].upper()}] {f['title']}") + print(f" {f['description']}") + print() + else: + print("No findings detected.") + +if __name__ == "__main__": + main() +\`\`\` + +Key rules: +- \`target\` positional arg uses \`nargs="?"\` — CXG engine does NOT pass target as argv. +- Target resolution order: \`CERT_X_GEN_PROJECT_ROOT\` > \`args.target\` > \`CERT_X_GEN_TARGET_HOST\`. +- When \`CERT_X_GEN_MODE == "engine"\`, ALWAYS output JSON (even if \`--json\` is not set). +- Output \`[]\` (empty JSON array) when no findings — never plain text in engine mode. +- For local/CLI templates, use \`target\` as \`cwd\` in \`subprocess.run()\` / \`subprocess.Popen()\` calls. + +## Final Response Format +After writing files, return: +1. A short "Generated templates" list with file paths. +2. A short "Audit-only (no template)" list with recommended GuardLink @audit/@comment text. +3. A "How to run these templates with CXG" section with these **exact steps**: + + **Step 1 — Prerequisites:** + \`\`\`bash + cxg --version # Verify CXG is installed (expect v1.1.0+) + python3 --version # Python 3.8+ required for template execution + ls .guardlink/cxg-templates/*.py # Verify templates were created + \`\`\` + + **Step 2 — Validate templates:** + \`\`\`bash + cxg template validate .guardlink/cxg-templates/ --recursive + \`\`\` + + **Step 3 — Create output directory and run scan using local scope (for CLI/codebase targets):** + \`\`\`bash + mkdir -p .guardlink/pentest-findings + cxg scan \\ + --scope local://. \\ + --template-dir .guardlink/cxg-templates/ \\ + --template-language python \\ + --output .guardlink/pentest-findings/guardlink-pentest \\ + --output-format json,sarif,html + \`\`\` + The \`local://.\` scope tells CXG this is a local codebase target. CXG will set + \`CERT_X_GEN_PROJECT_ROOT\` to the absolute path of the current directory and + \`CERT_X_GEN_TARGET_TYPE=local\`, so templates receive the correct project root. + + Output is stored in \`.guardlink/pentest-findings/\` so that \`guardlink dashboard\` + and \`guardlink threat-report\` automatically pick up the results. + + **Step 3b — Run scan using network scope (for HTTP/API targets):** + \`\`\`bash + cxg scan \\ + --scope https://api.example.com \\ + --template-dir .guardlink/cxg-templates/ \\ + --output .guardlink/pentest-findings/guardlink-pentest \\ + --output-format json,sarif,html + \`\`\` + + **Step 4 — Run with verbose output for debugging:** + \`\`\`bash + cxg -vv scan \\ + --scope local://. \\ + --template-dir .guardlink/cxg-templates/ \\ + --output .guardlink/pentest-findings/guardlink-pentest \\ + --output-format json,sarif,html + \`\`\` + + **Step 5 — Run individual templates standalone (without CXG):** + \`\`\`bash + python3 .guardlink/cxg-templates/.py . --json + \`\`\` + + **Expected output artifacts (in \`.guardlink/pentest-findings/\`):** + - \`guardlink-pentest.json\` — JSON with scan_id, findings array, statistics + - \`guardlink-pentest.sarif\` — SARIF 2.1.0 for GitHub Advanced Security / CI integration + - \`guardlink-pentest.html\` — Human-readable HTML report + - Each finding includes: template_id, severity, title, description, evidence (with request, response, matched_patterns, data), remediation + - **Evidence must be populated** — a finding with empty evidence (null request, null response, empty data) is a template bug + - These files are automatically consumed by \`guardlink dashboard\` (Pentest Findings tab) and \`guardlink threat-report\` (pentest context) + + **Troubleshooting:** + | Issue | Fix | + |---|---| + | \`target is required\` error | Template is missing \`nargs="?"\` on target arg — engine uses env vars, not argv | + | \`JSON parse error\` | Template prints non-JSON text to stdout in engine mode — wrap all output in \`json.dumps()\` | + | \`Operation timed out\` | Template takes >30s; add \`--timeout 60s\` to scan command | + | All templates show 0 findings | Run with \`-vv\` to check for WARN lines; ensure \`local://.\` scope is used for CLI templates | + | \`guardlink CLI not found\` | Run \`npm install\` in the project root first | + | Evidence fields are null/empty | Template is passing raw dict without CXG structure — use the \`create_finding\` helper pattern from the Evidence Contract section | + +4. A "What to expect" section that explains: + - what a positive finding looks like (JSON with template_id, severity, evidence) + - what a negative/no-finding run means (code is secure against those specific checks) + - false-positive caveats and manual verification guidance +5. Any assumptions requiring human review.`; +} + +/** + * Build a prompt for answering freeform user questions about the codebase + * and GuardLink threat model. + */ +export function buildAskPrompt( + userQuery: string, + root: string, + model: ThreatModel | null, +): string { + let modelSummary = 'No threat model parsed yet.'; + let idSummary = ''; + let exposureSummary = ''; + if (model) { + modelSummary = `Current model: ${model.annotations_parsed} annotations, ${model.exposures.length} exposures, ${(model.confirmed || []).length} confirmed, ${model.mitigations.length} mitigations, ${model.assets.length} assets, ${model.threats.length} threats, ${model.flows.length} flows.`; + + const assetIds = model.assets.filter(a => a.id).slice(0, 30).map(a => `#${a.id}`); + const threatIds = model.threats.filter(t => t.id).slice(0, 30).map(t => `#${t.id}`); + const controlIds = model.controls.filter(c => c.id).slice(0, 30).map(c => `#${c.id}`); + const idLines: string[] = []; + if (assetIds.length) idLines.push(`Assets: ${assetIds.join(', ')}`); + if (threatIds.length) idLines.push(`Threats: ${threatIds.join(', ')}`); + if (controlIds.length) idLines.push(`Controls: ${controlIds.join(', ')}`); + if (idLines.length) idSummary = `\n\nKnown IDs:\n${idLines.join('\n')}`; + + const unmitigated = model.exposures.filter((e) => + !model.mitigations.some((m) => m.asset === e.asset && m.threat === e.threat) + ); + if (unmitigated.length > 0) { + const lines = unmitigated.slice(0, 25).map((e) => + `- ${e.asset} -> ${e.threat} [${e.severity || 'unrated'}] (${e.location.file}:${e.location.line})` + ); + exposureSummary = `\n\nOpen unmitigated exposures:\n${lines.join('\n')}`; + if (unmitigated.length > 25) { + exposureSummary += `\n- ... and ${unmitigated.length - 25} more`; + } + } + } + + return `You are a senior AppSec engineer answering questions about a GuardLink-instrumented codebase. + +## Project Root +${root} + +## Current Threat Model Context +${modelSummary}${idSummary}${exposureSummary} + +## User Question +${userQuery} + +## Required Method +1. Read relevant source files and configs before answering. +2. Use GuardLink annotations as guidance, but verify with actual code. +3. If the question asks about a specific area (e.g. admin portal, API, auth), trace entry points, data flows, and related threats. +4. If information is missing or ambiguous, say so clearly and list what was checked. +5. Never invent endpoints, threats, or controls. -7. **Fix any validation errors** before finishing — especially dangling refs and malformed syntax. +## Output Format +- Provide a direct answer first. +- Then include concise evidence: + - files/components examined + - relevant threats/exposures/controls + - important gaps or unknowns +- If asked "do we have X threats," include counts and examples with file paths. `; } diff --git a/src/analyze/index.ts b/src/analyze/index.ts index 22ca41e..617bb2e 100644 --- a/src/analyze/index.ts +++ b/src/analyze/index.ts @@ -360,6 +360,12 @@ export function serializeModel(model: ThreatModel): string { description: e.description, file: e.location.file, line: e.location.line, })); + if (model.confirmed.length) compact.confirmed = model.confirmed.map(c => ({ + threat: c.threat, asset: c.asset, severity: c.severity, + refs: c.external_refs.length ? c.external_refs : undefined, + description: c.description, + file: c.location.file, line: c.location.line, + })); if (model.acceptances.length) compact.acceptances = model.acceptances.map(a => ({ asset: a.asset, threat: a.threat, description: a.description, file: a.location.file, line: a.location.line, @@ -453,7 +459,7 @@ export function serializeModelCompact(model: ThreatModel): string { const compact: Record = { project: model.project, - summary: `${model.annotations_parsed} annotations, ${model.assets.length} assets, ${model.threats.length} threats, ${model.controls.length} controls, ${model.exposures.length} exposures (${unmitigated.length} unmitigated), ${model.mitigations.length} mitigations`, + summary: `${model.annotations_parsed} annotations, ${model.assets.length} assets, ${model.threats.length} threats, ${model.controls.length} controls, ${model.exposures.length} exposures (${unmitigated.length} unmitigated), ${model.confirmed.length} confirmed, ${model.mitigations.length} mitigations`, severity_breakdown: sevCounts, }; @@ -523,8 +529,10 @@ export async function generateThreatReport(opts: ThreatReportOptions): Promise #llm-client via readFileSync -- "Reads CXG scan results for dashboard and report context" + * @handles internal on #llm-client -- "Processes pentest scan output (JSON/SARIF)" + */ + +export interface PentestFinding { + id: string; + target: string; + template_id: string; + severity: string; + confidence: number; + title: string; + description: string; + evidence: { + request: string | null; + response: string | null; + matched_patterns: string[]; + data: Record; + timestamp?: string; + }; + cve_ids: string[]; + cwe_ids: string[]; + cvss_score: number | null; + remediation: string; + references: string[]; + tags: string[]; + timestamp: string; +} + +export interface PentestScanResult { + scan_id: string; + started_at: string; + completed_at: string; + findings: PentestFinding[]; + statistics: { + targets_scanned: number; + templates_executed: number; + findings_by_severity: Record; + success_rate: number; + duration?: { secs: number; nanos: number }; + }; + source_file: string; +} + +export interface PentestTemplate { + filename: string; + id: string; + tags: string[]; + severity: string; + language: string; +} + +export interface PentestData { + scans: PentestScanResult[]; + templates: PentestTemplate[]; + totalFindings: number; + findingsBySeverity: Record; +} + +const PENTEST_FINDINGS_DIR = 'pentest-findings'; +const CXG_TEMPLATES_DIR = 'cxg-templates'; + +/** + * Load pentest findings from .guardlink/pentest-findings/ and template + * metadata from .guardlink/cxg-templates/. + * + * @mitigates #llm-client against #path-traversal using #path-validation -- "join() constrains reads to .guardlink/" + */ +export function loadPentestData(root: string): PentestData { + const data: PentestData = { scans: [], templates: [], totalFindings: 0, findingsBySeverity: {} }; + + // Load scan results (JSON files) + const findingsDir = join(root, '.guardlink', PENTEST_FINDINGS_DIR); + if (existsSync(findingsDir)) { + try { + const files = readdirSync(findingsDir).filter(f => f.endsWith('.json')); + for (const file of files) { + try { + const raw = readFileSync(join(findingsDir, file), 'utf-8'); + const parsed = JSON.parse(raw); + if (parsed.findings && Array.isArray(parsed.findings)) { + data.scans.push({ ...parsed, source_file: file }); + data.totalFindings += parsed.findings.length; + for (const f of parsed.findings) { + const sev = (f.severity || 'unknown').toLowerCase(); + data.findingsBySeverity[sev] = (data.findingsBySeverity[sev] || 0) + 1; + } + } + } catch { /* skip malformed JSON */ } + } + } catch { /* dir not readable */ } + } + + // Also check repo root for legacy guardlink-pentest.json + for (const name of ['guardlink-pentest.json']) { + const rootFile = join(root, name); + if (existsSync(rootFile)) { + try { + const raw = readFileSync(rootFile, 'utf-8'); + const parsed = JSON.parse(raw); + if (parsed.findings && Array.isArray(parsed.findings)) { + const alreadyLoaded = data.scans.some(s => s.scan_id === parsed.scan_id); + if (!alreadyLoaded) { + data.scans.push({ ...parsed, source_file: name }); + data.totalFindings += parsed.findings.length; + for (const f of parsed.findings) { + const sev = (f.severity || 'unknown').toLowerCase(); + data.findingsBySeverity[sev] = (data.findingsBySeverity[sev] || 0) + 1; + } + } + } + } catch { /* skip */ } + } + } + + // Sort scans newest first + data.scans.sort((a, b) => (b.completed_at || '').localeCompare(a.completed_at || '')); + + // Load template metadata from .guardlink/cxg-templates/ + const templatesDir = join(root, '.guardlink', CXG_TEMPLATES_DIR); + if (existsSync(templatesDir)) { + try { + const files = readdirSync(templatesDir).filter(f => + f.endsWith('.py') || f.endsWith('.yaml') || f.endsWith('.yml') || + f.endsWith('.go') || f.endsWith('.rs') || f.endsWith('.js') || f.endsWith('.sh') + ); + for (const file of files) { + try { + const raw = readFileSync(join(templatesDir, file), 'utf-8'); + const ext = file.split('.').pop() || ''; + const idMatch = raw.match(/id[:\s]*["']?([a-z0-9_-]+)["']?/i); + const sevMatch = raw.match(/severity[:\s]*["']?(critical|high|medium|low|info)["']?/i); + const tagsMatch = raw.match(/tags[:\s]*\[([^\]]*)\]/); + data.templates.push({ + filename: file, + id: idMatch?.[1] || file.replace(/\.[^.]+$/, ''), + severity: sevMatch?.[1] || 'medium', + language: ext === 'py' ? 'python' : ext === 'yml' || ext === 'yaml' ? 'yaml' : ext, + tags: tagsMatch?.[1] + ? tagsMatch[1].split(',').map(t => t.trim().replace(/["']/g, '')).filter(Boolean) + : [], + }); + } catch { /* skip unreadable */ } + } + } catch { /* dir not readable */ } + } + + return data; +} + +/** + * Serialize pentest findings into a compact text summary for LLM context. + */ +export function serializePentestFindings(data: PentestData): string { + if (data.scans.length === 0 && data.templates.length === 0) return ''; + + const lines: string[] = ['## Pentest Findings (CXG Scan Results)', '']; + + if (data.templates.length > 0) { + lines.push(`### Templates (${data.templates.length})`); + for (const t of data.templates) { + lines.push(`- ${t.id} [${t.severity}] (${t.language}) — ${t.tags.slice(0, 5).join(', ')}`); + } + lines.push(''); + } + + if (data.scans.length > 0) { + lines.push(`### Scan Results (${data.totalFindings} findings across ${data.scans.length} scan(s))`); + const sevSummary = Object.entries(data.findingsBySeverity) + .sort(([, a], [, b]) => b - a) + .map(([sev, count]) => `${sev}: ${count}`) + .join(', '); + if (sevSummary) lines.push(`Severity breakdown: ${sevSummary}`); + lines.push(''); + + for (const scan of data.scans) { + lines.push(`#### Scan ${scan.scan_id.slice(0, 8)} (${scan.completed_at?.slice(0, 19) || 'unknown'}) — ${scan.source_file}`); + lines.push(`Templates executed: ${scan.statistics?.templates_executed || '?'} | Success rate: ${((scan.statistics?.success_rate || 0) * 100).toFixed(0)}%`); + lines.push(''); + + for (const f of scan.findings) { + lines.push(`**[${f.severity.toUpperCase()}] ${f.title}** (${f.template_id})`); + lines.push(` CWE: ${f.cwe_ids.join(', ') || 'none'} | Confidence: ${f.confidence}%`); + lines.push(` ${f.description}`); + if (f.evidence?.request) lines.push(` Request: ${String(f.evidence.request).slice(0, 300)}`); + if (f.evidence?.response) lines.push(` Response: ${String(f.evidence.response).slice(0, 300)}`); + if (f.evidence?.matched_patterns?.length) lines.push(` Patterns: ${f.evidence.matched_patterns.join(', ')}`); + lines.push(` Remediation: ${f.remediation}`); + lines.push(''); + } + } + } + + return lines.join('\n'); +} diff --git a/src/analyze/prompts.ts b/src/analyze/prompts.ts index e2a48fe..b961673 100644 --- a/src/analyze/prompts.ts +++ b/src/analyze/prompts.ts @@ -29,12 +29,17 @@ You will receive: 1. **Project context** — language/framework, dependencies, deployment signals (Dockerfile, CI config, etc.) 2. **Annotation graph** — structured security metadata extracted from source code comments (GuardLink annotations) 3. **Code snippets** — the actual source lines surrounding each annotation, so you can validate what developers claimed +4. **Pentest findings** (if available) — actual CXG (CERT-X-GEN) scan results from automated security testing, including confirmed vulnerabilities with evidence, severity, CWE mappings, and remediation guidance ## How to use these inputs - Treat annotations as **developer hypotheses**, not ground truth. Validate them against the code snippets. - Use the project context to reason about the **real attack surface** — what frameworks introduce, what dependencies are known-vulnerable, what the deployment model exposes. - **Identify gaps**: what is NOT annotated but should be? Look at unannotated symbols, data flows with no security coverage, and dependency-level risks. +- If **pentest findings** are provided, treat them as **empirical evidence** — these are confirmed or high-confidence vulnerabilities found by automated security templates. Cross-reference findings against annotations: which @exposes are now validated? Recommend adding @confirmed in code where verification is definitive. Which threats have actual evidence? Are there findings for threats that were NOT annotated? +- Pentest findings with high confidence (>70%) and evidence (request + response) should be treated as **confirmed exploitable** unless the evidence clearly shows a false positive. +- If the annotation graph includes **@confirmed** rows, treat them as **already verified** — prioritize them in executive summary and remediation ordering alongside pentest JSON evidence. +- Include a dedicated **Pentest Results** section in your report that summarizes confirmed findings, correlates them with the threat model, and highlights any new attack vectors discovered by scanning. - Produce a threat model a **security team could hand to an auditor** — specific, evidence-based, and actionable. ## Annotation semantics @@ -44,6 +49,7 @@ You will receive: - **@control** — a security mechanism in place - **@mitigates** — a real control exists in code defending an asset against a threat. This is a genuine defense. - **@exposes** — a known vulnerability: this asset is exposed to this threat +- **@confirmed** — the threat was **verified** exploitable (pentest, scan with evidence, or manual reproduction). Treat as ground truth for that finding, not a hypothesis. Distinct from @exposes (theoretical) and @accepts (governance). - **@accepts** — risk acknowledged but **NO control in code**. This is a governance decision, not a technical fix. - **@flows** — data movement between components - **@boundary** — a trust boundary between security zones @@ -57,6 +63,7 @@ You will receive: - Treat accepted-but-unmitigated exposures as **OPEN RISKS**, not resolved findings. - If a code snippet contradicts its annotation (e.g., a @mitigates annotation but the code shows no actual check), flag the annotation as **potentially inaccurate**. - Challenge accepted risks: "You accepted this — is that reasonable given the severity and blast radius?" +- Distinguish **pentest-confirmable threats** from **governance/design-only gaps**. Pentest-confirmable issues should include concrete validation ideas; governance/design-only risks should be called out explicitly as **audit-required** items with suggested @audit/@comment annotations instead of fake exploit claims. - Always reference **specific files, assets, and threat IDs** from the model. Never give generic advice. ## Output structure @@ -308,7 +315,8 @@ Top 5–10 items the team should act on, ordered by risk. For each: one-line des /** * Build the user message containing the serialized threat model, - * optional project context, and optional code snippets. + * optional project context, optional code snippets, and optional + * pentest findings from CXG scans. */ export function buildUserMessage( modelJson: string, @@ -316,6 +324,7 @@ export function buildUserMessage( customPrompt?: string, projectContext?: string, codeSnippets?: string, + pentestFindings?: string, ): string { const header = customPrompt ? `Use these annotations as input to produce a threat model. Additional focus: ${customPrompt}` @@ -341,5 +350,12 @@ export function buildUserMessage( parts.push(''); } + if (pentestFindings) { + parts.push(''); + parts.push(''); + parts.push(pentestFindings); + parts.push(''); + } + return parts.join('\n'); } diff --git a/src/analyzer/sarif.ts b/src/analyzer/sarif.ts index a172da6..15fbdb1 100644 --- a/src/analyzer/sarif.ts +++ b/src/analyzer/sarif.ts @@ -9,8 +9,9 @@ * * We emit results for: * 1. Unmitigated exposures (the primary security findings) - * 2. Parse errors (annotation syntax problems) - * 3. Dangling references (broken #id refs) + * 2. @confirmed verified exploitable annotations (always error-level) + * 3. Parse errors (annotation syntax problems) + * 4. Dangling references (broken #id refs) * * @exposes #sarif to #data-exposure [low] cwe:CWE-200 -- "Exposes threat model findings to SARIF consumers" * @audit #sarif -- "SARIF output intentionally reveals security findings for CI/CD integration" @@ -90,6 +91,14 @@ const RULES: SarifRule[] = [ helpUri: 'https://guardlink.bugb.io/docs/exposures', defaultConfiguration: { level: 'error' }, }, + { + id: 'guardlink/confirmed-exploitable', + name: 'ConfirmedExploitable', + shortDescription: { text: 'Threat verified exploitable through testing' }, + fullDescription: { text: 'A @confirmed annotation marks this threat as verified through pentest, scanning, or manual reproduction. This is not a false positive and requires immediate remediation.' }, + helpUri: 'https://guardlink.bugb.io/docs/confirmed', + defaultConfiguration: { level: 'error' }, + }, { id: 'guardlink/parse-error', name: 'AnnotationParseError', @@ -163,6 +172,25 @@ export function generateSarif( }); } + // ── Confirmed exploitable ── + for (const c of (model.confirmed || [])) { + const threat = c.threat.startsWith('#') ? c.threat.slice(1) : c.threat; + const desc = c.description ? `: ${c.description}` : ''; + + results.push({ + ruleId: 'guardlink/confirmed-exploitable', + level: 'error', + message: { text: `CONFIRMED: ${c.asset} exploitable via ${threat}${desc}` }, + locations: [locationFrom(c.location.file, c.location.line)], + properties: { + severity: c.severity || 'unset', + asset: c.asset, + threat: c.threat, + ...(c.external_refs.length > 0 ? { externalRefs: c.external_refs } : {}), + }, + }); + } + // ── Parse errors ── if (includeDiagnostics) { for (const d of diagnostics) { diff --git a/src/cli/index.ts b/src/cli/index.ts index ec93948..14cbeaf 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -13,6 +13,8 @@ * guardlink sarif [dir] Export SARIF 2.1.0 for GitHub / VS Code * guardlink threat-report AI-powered threat analysis (STRIDE, DREAD, PASTA, etc.) * guardlink threat-reports List saved AI threat reports + * guardlink translate [prompt] Generate CERT-X-GEN pentest templates from threats + * guardlink ask Ask questions about threats and codebase context * guardlink annotate Launch coding agent to add annotations * guardlink config Manage LLM provider configuration * guardlink dashboard [dir] Generate interactive HTML dashboard @@ -39,15 +41,15 @@ import { Command } from 'commander'; import { resolve, basename } from 'node:path'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; -import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } from '../parser/index.js'; +import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations, listFeatures, filterByFeature, getFeatureSummaries } from '../parser/index.js'; import { initProject, detectProject, promptAgentSelection, syncAgentFiles } from '../init/index.js'; import { generateReport, generateMermaid } from '../report/index.js'; import { diffModels, formatDiff, formatDiffMarkdown, parseAtRef, getCurrentRef } from '../diff/index.js'; import { generateSarif } from '../analyzer/index.js'; import { startStdioServer } from '../mcp/index.js'; -import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage, type AnalysisFramework } from '../analyze/index.js'; +import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, loadPentestData, serializePentestFindings, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage, type AnalysisFramework } from '../analyze/index.js'; import { generateDashboardHTML } from '../dashboard/index.js'; -import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt } from '../agents/index.js'; +import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt, buildTranslatePrompt, buildAskPrompt } from '../agents/index.js'; import { resolveConfig, saveProjectConfig, saveGlobalConfig, loadProjectConfig, loadGlobalConfig, maskKey, describeConfigSource } from '../agents/config.js'; import { getReviewableExposures, applyReviewAction, formatExposureForReview, summarizeReview, type ReviewResult } from '../review/index.js'; import { populateMetadata, mergeReports, formatMergeSummary, diffMergedReports, formatDiffSummary, linkProject, addToWorkspace, removeFromWorkspace } from '../workspace/index.js'; @@ -211,9 +213,17 @@ program .argument('[dir]', 'Project directory to scan', '.') .option('-p, --project ', 'Project name', 'unknown') .option('--not-annotated', 'List source files with no GuardLink annotations') - .action(async (dir: string, opts: { project: string; notAnnotated?: boolean }) => { + .option('--feature ', 'Filter status to specific feature(s) (comma-separated)') + .action(async (dir: string, opts: { project: string; notAnnotated?: boolean; feature?: string }) => { const root = resolve(dir); - const { model, diagnostics } = await parseProject({ root, project: opts.project }); + let { model, diagnostics } = await parseProject({ root, project: opts.project }); + + // Apply feature filter if specified + if (opts.feature) { + const featureNames = opts.feature.split(',').map(s => s.trim()); + model = filterByFeature(model, featureNames); + console.log(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}\n`); + } printDiagnostics(diagnostics); printStatus(model); @@ -273,6 +283,13 @@ program } } + if ((model.confirmed || []).length > 0) { + console.error(`\n🔴 ${model.confirmed.length} confirmed exploitable finding(s) — verified, not false positives:`); + for (const c of model.confirmed) { + console.error(` ${c.asset} ← ${c.threat} [${c.severity || 'unset'}] (${c.location.file}:${c.location.line})`); + } + } + const errorCount = allDiags.filter(d => d.level === 'error').length; const hasUnmitigated = unmitigated.length > 0; @@ -307,9 +324,17 @@ program .option('-f, --format ', 'Output format: md, json, or both (default: md)', 'md') .option('--diagram-only', 'Output only the Mermaid diagram, no report wrapper') .option('--json', 'Also output threat-model.json alongside the report (legacy; prefer --format)') - .action(async (dir: string, opts: { project: string; output?: string; format: string; diagramOnly?: boolean; json?: boolean }) => { + .option('--feature ', 'Filter report to specific feature(s) (comma-separated)') + .action(async (dir: string, opts: { project: string; output?: string; format: string; diagramOnly?: boolean; json?: boolean; feature?: string }) => { const root = resolve(dir); - const { model, diagnostics } = await parseProject({ root, project: opts.project }); + let { model, diagnostics } = await parseProject({ root, project: opts.project }); + + // Apply feature filter if specified + if (opts.feature) { + const featureNames = opts.feature.split(',').map(s => s.trim()); + model = filterByFeature(model, featureNames); + console.error(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}`); + } // Show errors if any const errors = diagnostics.filter(d => d.level === 'error'); @@ -321,6 +346,18 @@ program // Enrich with provenance metadata (git SHA, branch, workspace, schema version) const enrichedModel = populateMetadata(model, root); + // Load project description from .guardlink/prompt.md if it exists + try { + const { readFile } = await import('node:fs/promises'); + const promptPath = resolve(root, '.guardlink', 'prompt.md'); + const promptContent = await readFile(promptPath, 'utf-8'); + if (promptContent.trim()) { + enrichedModel.prompt = promptContent.trim(); + } + } catch { + // No prompt file — that's fine, report will use annotation-derived overview + } + if (opts.diagramOnly) { // Just output Mermaid const mermaid = generateMermaid(enrichedModel); @@ -501,8 +538,11 @@ program const { buildProjectContext, extractCodeSnippets } = await import('../analyze/index.js'); const projectContext = buildProjectContext(root); const codeSnippets = extractCodeSnippets(root, model); + const pentestData = loadPentestData(root); + const pentestContext = serializePentestFindings(pentestData); const systemPrompt = FRAMEWORK_PROMPTS[fw]; - const userMessage = buildUserMessage(serialized, fw, customPrompt, projectContext || undefined, codeSnippets || undefined); + const userMessage = buildUserMessage(serialized, fw, customPrompt, projectContext || undefined, codeSnippets || undefined, pentestContext || undefined); + const hasPentest = pentestData.totalFindings > 0; const analysisPrompt = `You are analyzing a codebase with GuardLink security annotations. You have access to the full source code in the current directory. @@ -520,7 +560,8 @@ ${userMessage} 3. Produce the full report as markdown 4. Be specific — reference actual files, functions, and line numbers from the codebase 5. Output ONLY the markdown report content — do NOT add any metadata comments, save confirmations, or file path messages -6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."`; +6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."${hasPentest ? ` +7. The section contains CONFIRMED vulnerabilities from automated CXG security scans with real evidence. Cross-reference these against the threat model — mark confirmed findings as exploitable, identify which @exposes are now validated, and include a dedicated "Pentest Results" section summarizing all confirmed findings with their evidence and remediation guidance` : ''}`; // Resolve agent: explicit flag > project config CLI agent let agent = agentFromOpts(opts); @@ -743,6 +784,192 @@ program } }); +// ─── translate ─────────────────────────────────────────────────────── + +program + .command('translate') + .description('Translate GuardLink threats into CERT-X-GEN pentest templates (generation only, no execution)') + .argument('[prompt...]', 'Optional translation instructions') + .option('-d, --dir ', 'Project directory', '.') + .option('-p, --project ', 'Project name', 'unknown') + .option('--claude-code', 'Launch Claude Code in foreground') + .option('--codex', 'Launch Codex CLI in foreground') + .option('--gemini', 'Launch Gemini CLI in foreground') + .option('--cursor', 'Open Cursor IDE with prompt on clipboard') + .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard') + .option('--clipboard', 'Copy translation prompt to clipboard only') + .option('--feature ', 'Filter to specific feature(s) (comma-separated)') + .action(async (promptParts: string[], opts: { + dir: string; project: string; + claudeCode?: boolean; codex?: boolean; gemini?: boolean; + cursor?: boolean; windsurf?: boolean; clipboard?: boolean; + feature?: string; + }) => { + const root = resolve(opts.dir); + const project = detectProjectName(root, opts.project); + const userPrompt = promptParts.join(' ').trim(); + + let { model, diagnostics } = await parseProject({ root, project }); + + // Apply feature filter if specified + if (opts.feature) { + const featureNames = opts.feature.split(',').map(s => s.trim()); + model = filterByFeature(model, featureNames); + console.error(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}`); + } + const errors = diagnostics.filter(d => d.level === 'error'); + if (errors.length > 0) printDiagnostics(errors); + + if (model.annotations_parsed === 0) { + console.error('No annotations found. Run: guardlink init . && add annotations first.'); + process.exit(1); + } + + // Build translate prompt + const fullPrompt = buildTranslatePrompt(userPrompt, root, model); + + // Resolve agent: explicit flag > project config > default (Claude Code) + let agent = agentFromOpts(opts); + if (!agent) { + const projCfg = loadProjectConfig(root); + if (projCfg?.aiMode === 'cli-agent' && projCfg?.cliAgent) { + agent = AGENTS.find(a => a.id === projCfg.cliAgent) || null; + } + if (!agent) { + agent = AGENTS.find(a => a.id === 'claude-code') || null; + } + } + + if (!agent) { + console.error('No agent available. Use one of:'); + for (const a of AGENTS) { + console.error(` ${a.flag.padEnd(16)} ${a.name}`); + } + process.exit(1); + } + + console.log(`Launching ${agent.name} for CXG template translation...`); + console.log(`Threat model: ${model.annotations_parsed} annotations, ${model.exposures.length} exposures`); + if (agent.cmd) { + console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`); + } + + const result = launchAgent(agent, fullPrompt, root); + + if (result.clipboardCopied) { + console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`); + } + + if (result.error) { + console.error(`✗ ${result.error}`); + if (result.clipboardCopied) { + console.log('Prompt is on your clipboard — paste it manually.'); + } + process.exit(1); + } + + if (agent.cmd && result.launched) { + console.log(`\n✓ ${agent.name} session ended.`); + console.log(' Expected output location: .guardlink/cxg-templates/'); + console.log(' Note: Templates are generated only, not executed.'); + } else if (agent.app && result.launched) { + console.log(`✓ ${agent.name} launched with project: ${project}`); + console.log('\nPaste (Cmd+V) the prompt in the AI chat panel.'); + console.log('When done, review .guardlink/cxg-templates/'); + } else if (agent.id === 'clipboard') { + console.log('\nPaste the prompt into your preferred AI tool.'); + console.log('When done, review .guardlink/cxg-templates/'); + } + }); + +// ─── ask ───────────────────────────────────────────────────────────── + +program + .command('ask') + .description('Ask questions about this project, its threat model, and security posture') + .argument('[query...]', 'Question to answer') + .option('-d, --dir ', 'Project directory', '.') + .option('-p, --project ', 'Project name', 'unknown') + .option('--claude-code', 'Launch Claude Code in foreground') + .option('--codex', 'Launch Codex CLI in foreground') + .option('--gemini', 'Launch Gemini CLI in foreground') + .option('--cursor', 'Open Cursor IDE with prompt on clipboard') + .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard') + .option('--clipboard', 'Copy ask prompt to clipboard only') + .action(async (queryParts: string[], opts: { + dir: string; project: string; + claudeCode?: boolean; codex?: boolean; gemini?: boolean; + cursor?: boolean; windsurf?: boolean; clipboard?: boolean; + }) => { + const root = resolve(opts.dir); + const project = detectProjectName(root, opts.project); + const query = queryParts.join(' ').trim(); + + if (!query) { + console.error('Usage: guardlink ask "" [--claude-code|--codex|--gemini|--cursor|--windsurf|--clipboard]'); + process.exit(1); + } + + // Parse model if available; allow questions even for lightly-annotated projects + let model: ThreatModel | null = null; + try { + const parsed = await parseProject({ root, project }); + model = parsed.model; + } catch { + model = null; + } + + const fullPrompt = buildAskPrompt(query, root, model); + + // Resolve agent: explicit flag > project config > default (Claude Code) + let agent = agentFromOpts(opts); + if (!agent) { + const projCfg = loadProjectConfig(root); + if (projCfg?.aiMode === 'cli-agent' && projCfg?.cliAgent) { + agent = AGENTS.find(a => a.id === projCfg.cliAgent) || null; + } + if (!agent) { + agent = AGENTS.find(a => a.id === 'claude-code') || null; + } + } + + if (!agent) { + console.error('No agent available. Use one of:'); + for (const a of AGENTS) { + console.error(` ${a.flag.padEnd(16)} ${a.name}`); + } + process.exit(1); + } + + console.log(`Launching ${agent.name} for question answering...`); + if (agent.cmd) { + console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`); + } + + const result = launchAgent(agent, fullPrompt, root); + + if (result.clipboardCopied) { + console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`); + } + + if (result.error) { + console.error(`✗ ${result.error}`); + if (result.clipboardCopied) { + console.log('Prompt is on your clipboard — paste it manually.'); + } + process.exit(1); + } + + if (agent.cmd && result.launched) { + console.log(`\n✓ ${agent.name} session ended.`); + } else if (agent.app && result.launched) { + console.log(`✓ ${agent.name} launched with project: ${project}`); + console.log('\nPaste (Cmd+V) the prompt in the AI chat panel.'); + } else if (agent.id === 'clipboard') { + console.log('\nPaste the prompt into your preferred AI tool.'); + } + }); + // ─── clear ─────────────────────────────────────────────────────────── program @@ -1094,21 +1321,30 @@ program .option('-p, --project ', 'Project name', 'unknown') .option('-o, --output ', 'Output file (default: threat-dashboard.html)') .option('--light', 'Default to light theme instead of dark') - .action(async (dir: string, opts: { project: string; output?: string; light?: boolean }) => { + .option('--feature ', 'Filter dashboard to specific feature(s) (comma-separated)') + .action(async (dir: string, opts: { project: string; output?: string; light?: boolean; feature?: string }) => { const root = resolve(dir); const project = detectProjectName(root, opts.project); - const { model, diagnostics } = await parseProject({ root, project }); + let { model, diagnostics } = await parseProject({ root, project }); + + // Apply feature filter if specified + if (opts.feature) { + const featureNames = opts.feature.split(',').map(s => s.trim()); + model = filterByFeature(model, featureNames); + console.error(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}`); + } const errors = diagnostics.filter(d => d.level === 'error'); if (errors.length > 0) printDiagnostics(errors); - if (model.annotations_parsed === 0) { + if (model.annotations_parsed === 0 && !opts.feature) { console.error('No annotations found. Add GuardLink annotations first.'); process.exit(1); } const analyses = loadThreatReportsForDashboard(root); - let html = generateDashboardHTML(model, root, analyses); + const pentestData = loadPentestData(root); + let html = generateDashboardHTML(model, root, analyses, pentestData); // Switch default theme if requested if (opts.light) { @@ -1363,6 +1599,106 @@ program console.log(formatMergeSummary(merged)); }); +// ─── feature ────────────────────────────────────────────────────────── + +const featureCmd = program + .command('feature') + .description('Manage and inspect feature tags across the threat model'); + +featureCmd + .command('list') + .description('List all features found in annotations') + .argument('[dir]', 'Project directory to scan', '.') + .option('-p, --project ', 'Project name', 'unknown') + .option('--json', 'Output as JSON') + .action(async (dir: string, opts: { project: string; json?: boolean }) => { + const root = resolve(dir); + const project = detectProjectName(root, opts.project); + const { model } = await parseProject({ root, project }); + + const features = listFeatures(model); + if (features.length === 0) { + console.log('No @feature annotations found.'); + console.log('Tag code with: // @feature "Feature Name" -- "description"'); + return; + } + + if (opts.json) { + const summaries = getFeatureSummaries(model); + console.log(JSON.stringify(summaries, null, 2)); + return; + } + + const summaries = getFeatureSummaries(model); + console.log(`Features in ${project}:\n`); + for (const s of summaries) { + const files = s.files.length === 1 ? '1 file' : `${s.files.length} files`; + const exposureInfo = s.exposures > 0 ? ` | ${s.exposures} exposure(s)` : ''; + const confirmedInfo = s.confirmed > 0 ? ` | ${s.confirmed} confirmed` : ''; + console.log(` "${s.name}" (${files}, ${s.annotations} annotations${exposureInfo}${confirmedInfo})`); + for (const f of s.files) { + console.log(` → ${f}`); + } + } + console.log(`\n${features.length} feature(s) total`); + }); + +featureCmd + .command('show') + .description('Show detailed threat model for a specific feature') + .argument('', 'Feature name (case-insensitive)') + .option('-d, --dir ', 'Project directory', '.') + .option('-p, --project ', 'Project name', 'unknown') + .option('--json', 'Output as JSON') + .action(async (name: string, opts: { dir: string; project: string; json?: boolean }) => { + const root = resolve(opts.dir); + const project = detectProjectName(root, opts.project); + const { model } = await parseProject({ root, project }); + + const filtered = filterByFeature(model, [name]); + const totalAnnotations = filtered.assets.length + filtered.threats.length + + filtered.controls.length + filtered.mitigations.length + filtered.exposures.length + + filtered.confirmed.length + filtered.flows.length + filtered.boundaries.length; + + if (totalAnnotations === 0) { + console.error(`No annotations found for feature "${name}".`); + const available = listFeatures(model); + if (available.length > 0) { + console.error(`Available features: ${available.map(f => `"${f}"`).join(', ')}`); + } + process.exit(1); + } + + if (opts.json) { + console.log(JSON.stringify(filtered, null, 2)); + return; + } + + console.log(`Feature: "${name}"\n`); + console.log(` Assets: ${filtered.assets.length}`); + console.log(` Threats: ${filtered.threats.length}`); + console.log(` Controls: ${filtered.controls.length}`); + console.log(` Mitigations: ${filtered.mitigations.length}`); + console.log(` Exposures: ${filtered.exposures.length}`); + if (filtered.confirmed.length > 0) console.log(` Confirmed: ${filtered.confirmed.length} 🔴`); + console.log(` Flows: ${filtered.flows.length}`); + console.log(` Boundaries: ${filtered.boundaries.length}`); + + if (filtered.exposures.length > 0) { + console.log(`\n Exposures:`); + for (const e of filtered.exposures) { + console.log(` ${e.asset} → ${e.threat} [${e.severity || 'unset'}] (${e.location.file}:${e.location.line})`); + } + } + + if (filtered.mitigations.length > 0) { + console.log(`\n Mitigations:`); + for (const m of filtered.mitigations) { + console.log(` ${m.asset} against ${m.threat}${m.control ? ` using ${m.control}` : ''} (${m.location.file}:${m.location.line})`); + } + } + }); + // ─── mcp ───────────────────────────────────────────────────────────── program @@ -1452,6 +1788,13 @@ program console.log(EX(' // @mitigates db.users against Token Theft -- "Rotation implemented in v2"')); console.log(''); + console.log(` ${V('@confirmed')} ${K('')} ${D('on')} ${K('')} ${D('[severity]')} ${D('[ext-refs]')} ${D('[-- "evidence"]')}`); + console.log(D(' Mark a threat as verified exploitable (pentest, scan, or manual repro).')); + console.log(D(' Not a false positive — use observed severity. Distinct from @exposes (hypothesis).')); + console.log(EX(' // @confirmed SQL Injection on api.auth [critical] cwe:CWE-89 -- "Pen test: blind SQLi on /login"')); + console.log(EX(' // @confirmed #secret-exposure on App.Config [critical] -- "Live key in repo; verified with provider"')); + console.log(''); + console.log(` ${V('@accepts')} ${K('')} ${D('on')} ${K('')} ${D('[-- "reason"]')}`); console.log(D(' Explicitly accept a risk. Removes it from open findings.')); console.log(D(' Use when the risk is known and intentionally not mitigated.')); @@ -1513,6 +1856,23 @@ program console.log(EX(' // @assumes api.gateway -- "Upstream WAF filters malformed requests"')); console.log(''); + // ── FEATURE TAGGING ── + console.log(H(' ── Feature Tagging ─────────────────────────────────────────')); + console.log(''); + + console.log(` ${V('@feature')} ${K('"Feature Name"')} ${D('[-- "description"]')}`); + console.log(D(' Tag code with a feature name for filtering reports and dashboards.')); + console.log(D(' A file can have multiple @feature tags. All annotations in that file')); + console.log(D(' are associated with the tagged features.')); + console.log(EX(' // @feature "SSO Login" -- "Single sign-on authentication flow"')); + console.log(EX(' // @feature "Payment Processing"')); + console.log(D('')); + console.log(D(' Filter commands by feature:')); + console.log(EX(' guardlink feature list')); + console.log(EX(' guardlink report . --feature "SSO Login"')); + console.log(EX(' guardlink dashboard . --feature "SSO Login,Payment Processing"')); + console.log(''); + console.log(` ${V('@comment')} ${D('[-- "description"]')}`); console.log(D(' Free-form developer security note (no structural effect).')); console.log(EX(' // @comment -- "TODO — add rate limiting before v2 launch"')); @@ -1536,7 +1896,7 @@ program // ── EXTERNAL REFERENCES ── console.log(H(' ── External References ─────────────────────────────────────')); console.log(''); - console.log(D(' Append space-separated refs after severity on @threat and @exposes:')); + console.log(D(' Append space-separated refs after severity on @threat, @exposes, and @confirmed:')); console.log(EX(' cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66 attack:T1190')); console.log(''); console.log(D(' Example:')); @@ -1597,6 +1957,7 @@ function printStatus(model: ThreatModel) { console.log(`Controls: ${model.controls.length}`); console.log(`Mitigations: ${model.mitigations.length}`); console.log(`Exposures: ${model.exposures.length}`); + if ((model.confirmed || []).length > 0) console.log(`Confirmed: ${model.confirmed.length} 🔴`); console.log(`Acceptances: ${model.acceptances.length}`); console.log(`Transfers: ${model.transfers.length}`); console.log(`Flows: ${model.flows.length}`); @@ -1606,6 +1967,10 @@ function printStatus(model: ThreatModel) { console.log(`Ownership: ${model.ownership.length}`); console.log(`Data handling: ${model.data_handling.length}`); console.log(`Assumptions: ${model.assumptions.length}`); + if (model.features.length > 0) { + const uniqueFeatures = new Set(model.features.map(f => f.feature)); + console.log(`Features: ${uniqueFeatures.size} (${model.features.length} tag${model.features.length > 1 ? 's' : ''})`); + } console.log(`Comments: ${model.comments.length}`); console.log(`Shields: ${model.shields.length}`); } diff --git a/src/dashboard/data.ts b/src/dashboard/data.ts index 83500ca..c9ad2a4 100644 --- a/src/dashboard/data.ts +++ b/src/dashboard/data.ts @@ -18,6 +18,7 @@ export interface DashboardStats { controls: number; mitigations: number; exposures: number; + confirmed: number; acceptances: number; transfers: number; flows: number; @@ -52,6 +53,16 @@ export interface ExposureRow { accepted: boolean; } +export interface ConfirmedRow { + threat: string; + asset: string; + severity: string; + description: string; + file: string; + line: number; + external_refs: string[]; +} + export interface AssetHeatmapEntry { name: string; exposures: number; @@ -70,6 +81,7 @@ export function computeStats(model: ThreatModel): DashboardStats { controls: model.controls.length, mitigations: model.mitigations.length, exposures: model.exposures.length, + confirmed: model.confirmed.length, acceptances: model.acceptances.length, transfers: model.transfers.length, flows: model.flows.length, @@ -147,3 +159,15 @@ export function computeAssetHeatmap(model: ThreatModel): AssetHeatmapEntry[] { return order[a.riskLevel] - order[b.riskLevel]; }); } + +export function computeConfirmed(model: ThreatModel): ConfirmedRow[] { + return (model.confirmed || []).map(c => ({ + threat: c.threat, + asset: c.asset, + severity: c.severity || 'unset', + description: c.description || '', + file: c.location.file, + line: c.location.line, + external_refs: c.external_refs || [], + })); +} diff --git a/src/dashboard/generate.ts b/src/dashboard/generate.ts index 0b0a612..69842e9 100644 --- a/src/dashboard/generate.ts +++ b/src/dashboard/generate.ts @@ -13,31 +13,35 @@ * @flows SourceFiles -> #dashboard via readFileSync -- "Code snippet reads" * @flows #dashboard -> HTML via return -- "Generated HTML output" * @handles internal on #dashboard -- "Processes and displays threat model data" + * @feature "Dashboard" -- "Interactive HTML threat model dashboard" */ import type { ThreatModel } from '../types/index.js'; -import { computeStats, computeSeverity, computeExposures, computeAssetHeatmap } from './data.js'; -import type { DashboardStats, SeverityBreakdown, ExposureRow, AssetHeatmapEntry } from './data.js'; +import { listFeatures } from '../parser/feature-filter.js'; +import { computeStats, computeSeverity, computeExposures, computeConfirmed, computeAssetHeatmap } from './data.js'; +import type { DashboardStats, SeverityBreakdown, ExposureRow, ConfirmedRow, AssetHeatmapEntry } from './data.js'; import { generateThreatGraph, generateDataFlowDiagram, generateAttackSurface } from './diagrams.js'; -import type { ThreatReportWithContent } from '../analyze/index.js'; +import type { ThreatReportWithContent, PentestData } from '../analyze/index.js'; import { readFileSync } from 'fs'; import { resolve, isAbsolute } from 'path'; -export function generateDashboardHTML(model: ThreatModel, root?: string, analyses?: ThreatReportWithContent[]): string { +export function generateDashboardHTML(model: ThreatModel, root?: string, analyses?: ThreatReportWithContent[], pentestData?: PentestData): string { const stats = computeStats(model); const severity = computeSeverity(model); const exposures = computeExposures(model); + const confirmedRows = computeConfirmed(model); const heatmap = computeAssetHeatmap(model); const threatGraph = generateThreatGraph(model); const dataFlow = generateDataFlowDiagram(model); const attackSurface = generateAttackSurface(model); + const featureNames = listFeatures(model); const timestamp = new Date().toISOString().slice(0, 19).replace('T', ' '); const unmitigated = exposures.filter(e => !e.mitigated && !e.accepted); const mitigatedCount = exposures.filter(e => e.mitigated).length; const mitigationCoveragePercent = exposures.length > 0 ? Math.round((mitigatedCount / exposures.length) * 100) : 0; - const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length); + const riskScore = computeRiskGrade(severity, unmitigated.length, exposures.length, confirmedRows.length); // Build file annotations data for code browser + drawer const fileAnnotations = buildFileAnnotations(model, root); @@ -45,6 +49,9 @@ export function generateDashboardHTML(model: ThreatModel, root?: string, analyse // Build analysis data for drawer const analysisData = buildAnalysisData(model, exposures); + // Pentest data (may be null/empty) + const pentest = pentestData || { scans: [], templates: [], totalFindings: 0, findingsBySeverity: {} }; + // Check for saved AI analyses // (we embed the latest one if model has it, otherwise empty) const aiAnalysis = ''; // Will be loaded from .guardlink/analyses/ by CLI @@ -77,12 +84,26 @@ ${CSS_CONTENT}
Open ${unmitigated.length}
Controls ${stats.controls}
Coverage ${stats.coveragePercent}%
+${featureNames.length > 0 ? `
+ +
` : ''} + + +
@@ -90,6 +111,7 @@ ${CSS_CONTENT}