From b68ebebf3eb3e00ea6b1de3a8cf354bb4074fe1b Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 21:07:56 -0600 Subject: [PATCH 1/7] feat: unannotated file tracking, sync guidance, clear command, picker UX - Add annotated_files/unannotated_files to ThreatModel (parser tracks coverage) - CLI: 'guardlink unannotated' standalone command + status --not-annotated flag - CLI: 'guardlink clear' removes annotations with --dry-run and --include-definitions - CLI: 'guardlink sync' standalone command (was only available via MCP/TUI) - TUI: /unannotated, /clear, /sync commands - MCP: guardlink_unannotated, guardlink_clear, guardlink_sync tools - Dashboard: File Coverage bar + unannotated file list on Code & Annotations page - Templates: sync guidance in workflow section for all 7 agent formats - Templates: tightened negative guardrail against over-annotation - Picker: 'All of the above' as last numbered option instead of 'a' shortcut - Auto-sync: status and validate commands auto-sync agent files after parsing --- .github/copilot-instructions.md | 99 +++++++++++++++-- CLAUDE.md | 99 +++++++++++++++-- docs/GUARDLINK_REFERENCE.md | 16 +-- src/cli/index.ts | 156 +++++++++++++++++++++++++- src/dashboard/generate.ts | 24 +++- src/init/index.ts | 99 ++++++++++++++++- src/init/picker.ts | 24 ++-- src/init/templates.ts | 170 +++++++++++++++++++++++++---- src/mcp/server.ts | 79 +++++++++++++- src/parser/clear.ts | 188 ++++++++++++++++++++++++++++++++ src/parser/index.ts | 2 + src/parser/parse-project.ts | 22 +++- src/tui/commands.ts | 128 +++++++++++++++++++++- src/tui/index.ts | 13 +++ src/types/index.ts | 2 + 15 files changed, 1047 insertions(+), 74 deletions(-) create mode 100644 src/parser/clear.ts diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3787904..b3885eb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -6,24 +6,101 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. **Full reference: `docs/GUARDLINK_REFERENCE.md`** +### Core Requirement + +**Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change.** This includes: new endpoints, authentication/authorization logic, data validation, database queries, file I/O, external API calls, crypto operations, process spawning, user input handling, and configuration parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helper functions that never touch security boundaries. + ### Key Rules -1. Definitions (`@asset`, `@threat`, `@control` with `#id`) go in `.guardlink/definitions.ts` — read it before adding new ones. -2. Source files use relationship verbs only: `@mitigates`, `@exposes`, `@accepts`, `@flows`, `@handles`, `@boundary`, `@comment`. -3. Every `@exposes` needs a matching `@mitigates` or `@accepts`. -4. Always add at least `@comment` on security-relevant code. -5. Run `guardlink validate .` after making changes. +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. + +### Workflow (while coding) + +- Before writing code: skim `.guardlink/definitions.ts` to understand existing assets, threats, and controls. +- While writing code: add annotations above or in the doc-block of security-relevant functions as you write them — not as a separate pass afterward. +- After changes: run `guardlink validate .` to catch syntax/dangling refs; run `guardlink status .` to check coverage; commit annotation updates with the code. +- After adding annotations: run `guardlink sync` to update all agent instruction files with the current threat model context. This ensures every agent sees the latest assets, threats, controls, and open exposures. -### MCP Tools Available +### Tools -Use `guardlink_lookup` to check existing definitions. Use `guardlink_validate` after annotating. Use `guardlink_suggest ` for recommendations. +- MCP tools (when available, e.g., Claude Code): `guardlink_lookup`, `guardlink_validate`, `guardlink_status`, `guardlink_parse`, `guardlink_suggest `. +- CLI equivalents (always available): `guardlink validate .`, `guardlink status .`, `guardlink parse .`. -### Quick Syntax +### Quick Syntax (common verbs) ``` -@exposes Asset to #threat [P0] cwe:CWE-89 -- "description" -@mitigates Asset against #threat using #control -- "how" -@comment -- "security-relevant note" +@exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +@mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +@audit App.API -- "Timing attack risk — needs human review to assess bcrypt constant-time comparison" +@flows User -> App.API via HTTPS -- "Login request path" +@boundary between #api and #db (#data-boundary) -- "App → DB trust change" +@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" +@owns security-team for App.API -- "Team responsible for reviews" +@comment -- "Rate limit: 100 req/15min via express-rate-limit" ``` +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows + +> **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 +> and continue annotating new code using the same threat model vocabulary. + + diff --git a/CLAUDE.md b/CLAUDE.md index 3787904..b3885eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,24 +6,101 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. **Full reference: `docs/GUARDLINK_REFERENCE.md`** +### Core Requirement + +**Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change.** This includes: new endpoints, authentication/authorization logic, data validation, database queries, file I/O, external API calls, crypto operations, process spawning, user input handling, and configuration parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helper functions that never touch security boundaries. + ### Key Rules -1. Definitions (`@asset`, `@threat`, `@control` with `#id`) go in `.guardlink/definitions.ts` — read it before adding new ones. -2. Source files use relationship verbs only: `@mitigates`, `@exposes`, `@accepts`, `@flows`, `@handles`, `@boundary`, `@comment`. -3. Every `@exposes` needs a matching `@mitigates` or `@accepts`. -4. Always add at least `@comment` on security-relevant code. -5. Run `guardlink validate .` after making changes. +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. + +### Workflow (while coding) + +- Before writing code: skim `.guardlink/definitions.ts` to understand existing assets, threats, and controls. +- While writing code: add annotations above or in the doc-block of security-relevant functions as you write them — not as a separate pass afterward. +- After changes: run `guardlink validate .` to catch syntax/dangling refs; run `guardlink status .` to check coverage; commit annotation updates with the code. +- After adding annotations: run `guardlink sync` to update all agent instruction files with the current threat model context. This ensures every agent sees the latest assets, threats, controls, and open exposures. -### MCP Tools Available +### Tools -Use `guardlink_lookup` to check existing definitions. Use `guardlink_validate` after annotating. Use `guardlink_suggest ` for recommendations. +- MCP tools (when available, e.g., Claude Code): `guardlink_lookup`, `guardlink_validate`, `guardlink_status`, `guardlink_parse`, `guardlink_suggest `. +- CLI equivalents (always available): `guardlink validate .`, `guardlink status .`, `guardlink parse .`. -### Quick Syntax +### Quick Syntax (common verbs) ``` -@exposes Asset to #threat [P0] cwe:CWE-89 -- "description" -@mitigates Asset against #threat using #control -- "how" -@comment -- "security-relevant note" +@exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +@mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +@audit App.API -- "Timing attack risk — needs human review to assess bcrypt constant-time comparison" +@flows User -> App.API via HTTPS -- "Login request path" +@boundary between #api and #db (#data-boundary) -- "App → DB trust change" +@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" +@owns security-team for App.API -- "Team responsible for reviews" +@comment -- "Rate limit: 100 req/15min via express-rate-limit" ``` +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows + +> **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 +> and continue annotating new code using the same threat model vocabulary. + + diff --git a/docs/GUARDLINK_REFERENCE.md b/docs/GUARDLINK_REFERENCE.md index 63c5682..7f01742 100644 --- a/docs/GUARDLINK_REFERENCE.md +++ b/docs/GUARDLINK_REFERENCE.md @@ -12,7 +12,7 @@ DEFINE @asset (#id) -- "description" RELATE @mitigates against <#threat> using <#control> -- "how" @exposes to <#threat> [severity] cwe:CWE-NNN -- "what's wrong" - @accepts <#threat> on -- "why acceptable" + @accepts <#threat> on -- "HUMAN-ONLY — AI agents must use @audit instead" @transfers <#threat> from to -- "who handles it" FLOW @flows -> via -- "details" @@ -42,21 +42,23 @@ Append after severity: `cwe:CWE-89`, `owasp:A03:2021`, `capec:CAPEC-66`, `attack ## Rules -1. **Define once, reference everywhere.** `@asset`, `@threat`, `@control` with `(#id)` go in `.guardlink/definitions.ts`. Source files use relationship verbs only (`@mitigates`, `@exposes`, etc.). -2. **Read definitions before adding.** Check for existing IDs first — avoid duplicates. -3. **Every `@exposes` needs a plan.** Match with `@mitigates` (fix exists), `@accepts` (risk acknowledged), or TODO. -4. **Always annotate security-relevant code.** At minimum, add `@comment` to document intent. +1. **Annotate as you code.** When you write or modify security-relevant code (endpoints, auth, data access, validation, I/O, crypto, process spawning), add annotations in the same change. This is required, not optional. +2. **Define once, reference everywhere.** `@asset`, `@threat`, `@control` with `(#id)` go in `.guardlink/definitions.ts`. Source files use relationship verbs only (`@mitigates`, `@exposes`, etc.). +3. **Read definitions before adding.** Check for existing IDs first — avoid duplicates. +4. **Every `@exposes` needs a response.** Match with `@mitigates` (fix exists) or `@audit` (flag for human review). AI agents must NEVER write `@accepts` — that is a human-only governance decision. Use `@audit` instead. 5. **Use the full verb set.** `@flows` for data movement, `@handles` for data classification, `@boundary` for trust boundaries. ## When Writing Code | Situation | Annotation | |-----------|------------| +| 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` | +| Security gap exists | `@exposes Asset to #threat` + `@audit Asset` | +| 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` | +| Proprietary algorithm | `@shield:begin` ... `@shield:end` (only if human requests it) | | Unsure which annotation | `@comment -- "describe what you see"` | ## CLI Commands diff --git a/src/cli/index.ts b/src/cli/index.ts index 85e263b..332e1cc 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -34,8 +34,8 @@ import { Command } from 'commander'; import { resolve, basename } from 'node:path'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; -import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from '../parser/index.js'; -import { initProject, detectProject, promptAgentSelection } from '../init/index.js'; +import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } 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'; @@ -202,12 +202,25 @@ program .description('Show annotation coverage summary') .argument('[dir]', 'Project directory to scan', '.') .option('-p, --project ', 'Project name', 'unknown') - .action(async (dir: string, opts: { project: string }) => { + .option('--not-annotated', 'List source files with no GuardLink annotations') + .action(async (dir: string, opts: { project: string; notAnnotated?: boolean }) => { const root = resolve(dir); const { model, diagnostics } = await parseProject({ root, project: opts.project }); printDiagnostics(diagnostics); printStatus(model); + + if (opts.notAnnotated) { + printUnannotatedFiles(model); + } + + // Auto-sync agent instruction files with updated model + if (model.annotations_parsed > 0) { + const syncResult = syncAgentFiles({ root, model }); + if (syncResult.updated.length > 0) { + console.error(`↻ Synced ${syncResult.updated.length} agent instruction file(s)`); + } + } }); // ─── validate ──────────────────────────────────────────────────────── @@ -263,6 +276,14 @@ program console.error(`\nValidation passed with ${unmitigated.length} unmitigated exposure(s).`); } + // Auto-sync agent instruction files with updated model + if (model.annotations_parsed > 0) { + const syncResult = syncAgentFiles({ root, model }); + if (syncResult.updated.length > 0) { + console.error(`↻ Synced ${syncResult.updated.length} agent instruction file(s)`); + } + } + // Exit 1 on errors always; also on unmitigated if --strict process.exit(errorCount > 0 || (opts.strict && hasUnmitigated) ? 1 : 0); }); @@ -700,6 +721,122 @@ program } }); +// ─── clear ─────────────────────────────────────────────────────────── + +program + .command('clear') + .description('Remove all GuardLink annotations from source files — start fresh') + .argument('[dir]', 'Project directory', '.') + .option('--dry-run', 'Show what would be removed without modifying files') + .option('--include-definitions', 'Also clear .guardlink/definitions files') + .option('-y, --yes', 'Skip confirmation prompt') + .action(async (dir: string, opts: { dryRun?: boolean; includeDefinitions?: boolean; yes?: boolean }) => { + const root = resolve(dir); + + // First, show what will be cleared + const preview = await clearAnnotations({ + root, + dryRun: true, + includeDefinitions: opts.includeDefinitions, + }); + + if (preview.totalRemoved === 0) { + console.log('No GuardLink annotations found in source files.'); + return; + } + + console.log(`\nFound ${preview.totalRemoved} annotation line(s) across ${preview.modifiedFiles.length} file(s):\n`); + for (const [file, count] of preview.perFile) { + console.log(` ${file} (${count} line${count > 1 ? 's' : ''})`); + } + console.log(''); + + if (opts.dryRun) { + console.log('(dry run) No files were modified.'); + return; + } + + // Confirmation prompt + if (!opts.yes) { + if (!process.stdin.isTTY) { + console.error('Use --yes to confirm in non-interactive mode.'); + process.exit(1); + } + + const readline = await import('node:readline'); + const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); + const answer = await new Promise(resolve => { + rl.question('⚠ This will remove all annotations from source files. Continue? (y/N): ', resolve); + }); + rl.close(); + + if (answer.trim().toLowerCase() !== 'y') { + console.log('Cancelled.'); + return; + } + } + + // Actually clear + const result = await clearAnnotations({ + root, + dryRun: false, + includeDefinitions: opts.includeDefinitions, + }); + + console.log(`\n✓ Removed ${result.totalRemoved} annotation line(s) from ${result.modifiedFiles.length} file(s).`); + console.log(' Run: guardlink annotate to re-annotate from scratch.'); + }); + +// ─── sync ──────────────────────────────────────────────────────────── + +program + .command('sync') + .description('Sync agent instruction files with current threat model — keeps ALL coding agents up to date') + .argument('[dir]', 'Project directory', '.') + .option('--dry-run', 'Show what would be updated without modifying files') + .action(async (dir: string, opts: { dryRun?: boolean }) => { + const root = resolve(dir); + + // Parse the current model + const { model } = await parseProject({ root, project: basename(root) }); + + if (model.annotations_parsed === 0) { + console.log('No annotations found. Run: guardlink annotate to add annotations first.'); + console.log('Syncing agent files with base instructions (no model context)...\n'); + } + + const result = syncAgentFiles({ root, model, dryRun: opts.dryRun }); + + if (result.updated.length > 0) { + console.log(`${opts.dryRun ? '(dry run) Would update' : '✓ Updated'} ${result.updated.length} agent instruction file(s):\n`); + for (const f of result.updated) { + console.log(` ${f}`); + } + } + if (result.skipped.length > 0) { + console.log(`\nSkipped: ${result.skipped.join(', ')}`); + } + + if (!opts.dryRun && model.annotations_parsed > 0) { + console.log(`\n✓ All agent instruction files now include live threat model context.`); + console.log(` ${model.assets.length} assets, ${model.threats.length} threats, ${model.controls.length} controls, ${model.exposures.length} exposures.`); + console.log(' Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) will see these IDs.'); + } + }); + +// ─── unannotated ───────────────────────────────────────────────────── + +program + .command('unannotated') + .description('List source files with no GuardLink annotations') + .argument('[dir]', 'Project directory to scan', '.') + .option('-p, --project ', 'Project name', 'unknown') + .action(async (dir: string, opts: { project: string }) => { + const root = resolve(dir); + const { model } = await parseProject({ root, project: opts.project }); + printUnannotatedFiles(model); + }); + // ─── config ────────────────────────────────────────────────────────── program @@ -1064,6 +1201,8 @@ function printStatus(model: ThreatModel) { console.log(`GuardLink Status: ${model.project}`); console.log(`${'─'.repeat(40)}`); console.log(`Files scanned: ${model.source_files}`); + console.log(` Annotated: ${model.annotated_files.length}`); + console.log(` Not annotated: ${model.unannotated_files.length}`); console.log(`Annotations: ${model.annotations_parsed}`); console.log(`${'─'.repeat(40)}`); console.log(`Assets: ${model.assets.length}`); @@ -1084,3 +1223,14 @@ function printStatus(model: ThreatModel) { console.log(`Shields: ${model.shields.length}`); } +function printUnannotatedFiles(model: ThreatModel) { + if (model.unannotated_files.length === 0) { + console.log(`\n✓ All source files have GuardLink annotations.`); + return; + } + console.log(`\n⚠ ${model.unannotated_files.length} source file(s) with no annotations:`); + for (const f of model.unannotated_files) { + console.log(` ${f}`); + } +} + diff --git a/src/dashboard/generate.ts b/src/dashboard/generate.ts index 61db759..423fe24 100644 --- a/src/dashboard/generate.ts +++ b/src/dashboard/generate.ts @@ -107,7 +107,7 @@ ${renderSummaryPage(stats, severity, riskScore, unmitigated, exposures, model, m ${renderAIAnalysisPage(analyses || [])} ${renderThreatsPage(exposures, model)} ${renderDiagramsPage(threatGraph, dataFlow, attackSurface)} -${renderCodePage(fileAnnotations)} +${renderCodePage(fileAnnotations, model)} ${renderDataPage(model)} ${renderAssetsPage(heatmap)} @@ -863,7 +863,10 @@ function renderDiagramsPage(threatGraph: string, dataFlow: string, attackSurface `; } -function renderCodePage(fileAnnotations: FileAnnotationGroup[]): string { +function renderCodePage(fileAnnotations: FileAnnotationGroup[], model: ThreatModel): string { + const unannotated = model.unannotated_files || []; + const annotatedCount = model.annotated_files?.length || fileAnnotations.length; + const totalFiles = annotatedCount + unannotated.length; return `
</> Code & Annotations
@@ -894,6 +897,23 @@ function renderCodePage(fileAnnotations: FileAnnotationGroup[]): string {
`).join('')} `).join('') : '

No annotations found.

'} + + +
File Coverage
+
+ ${annotatedCount} of ${totalFiles} files + have GuardLink annotations +
+ ${totalFiles > 0 ? `
` : ''} + + ${unannotated.length > 0 ? ` +
⚠ Unannotated Files (${unannotated.length})
+

+ Source files with no GuardLink annotations. Not all files need annotations — only those touching security boundaries. +

+
+ ${unannotated.map(f => `
${esc(f)}
`).join('')} +
` : `

✓ All source files have annotations.

`} `; } diff --git a/src/init/index.ts b/src/init/index.ts index c030421..8d0b7ca 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -18,14 +18,18 @@ import { join, dirname } from 'node:path'; import { detectProject, type ProjectInfo, type AgentFile } from './detect.js'; import { agentInstructions, + agentInstructionsWithModel, cursorRulesContent, + cursorRulesContentWithModel, cursorMdcContent, + cursorMdcContentWithModel, definitionsContent, configContent, mcpConfig, referenceDocContent, GITIGNORE_ENTRY, } from './templates.js'; +import type { ThreatModel } from '../types/index.js'; import { AGENT_CHOICES } from './picker.js'; export { detectProject, type ProjectInfo, type AgentFile } from './detect.js'; @@ -161,7 +165,8 @@ function updateAgentFiles( const updated: string[] = []; const skipped: string[] = []; - const ids = agentIds ?? ['claude']; + // Default: write ALL agent files so switching agents is seamless + const ids = agentIds ?? AGENT_CHOICES.map(c => c.id); for (const id of ids) { const choice = AGENT_CHOICES.find(c => c.id === id); @@ -256,9 +261,7 @@ function injectIntoAgentFile( } function buildClaudeMdFromScratch(project: ProjectInfo): string { - return `# ${toPascalCase(project.name)} — Project Instructions - -${wrapMarkers(agentInstructions(project))}`; + return buildMdFromScratch(project, null); } // ─── Helpers ───────────────────────────────────────────────────────── @@ -296,3 +299,91 @@ function toPascalCase(s: string): string { .map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()) .join(''); } + +function buildMdFromScratch(project: ProjectInfo, model: ThreatModel | null): string { + return `# ${toPascalCase(project.name)} — Project Instructions + +${wrapMarkers(agentInstructionsWithModel(project, model))}`; +} + +// ─── Sync: regenerate agent files with live threat model ───────────── + +export interface SyncOptions { + root: string; + model: ThreatModel | null; + dryRun?: boolean; +} + +export interface SyncResult { + updated: string[]; + skipped: string[]; +} + +/** + * Regenerate ALL agent instruction files with live threat model context. + * Called after parse/validate/annotate to keep instructions up to date. + * Uses marker-based replacement so user content outside markers is preserved. + */ +export function syncAgentFiles(options: SyncOptions): SyncResult { + const { root, model, dryRun = false } = options; + const project = detectProject(root); + const updated: string[] = []; + const skipped: string[] = []; + + for (const choice of AGENT_CHOICES) { + const filePath = join(root, choice.file); + const exists = existsSync(filePath); + + if (!exists) { + // Create fresh with model context + if (choice.file.endsWith('.mdc')) { + if (!dryRun) { + ensureDir(dirname(filePath)); + writeFileSync(filePath, cursorMdcContentWithModel(project, model)); + } + updated.push(choice.file); + } else if (choice.file === '.cursorrules' || choice.file === '.windsurfrules' || choice.file === '.clinerules') { + if (!dryRun) { + ensureDir(dirname(filePath)); + writeFileSync(filePath, wrapMarkers(cursorRulesContentWithModel(project, model))); + } + updated.push(choice.file); + } else if (choice.file.endsWith('settings.json')) { + skipped.push(`${choice.file} (json format — not supported)`); + } else { + // Markdown-based: CLAUDE.md, AGENTS.md, copilot-instructions.md, etc. + if (!dryRun) { + ensureDir(dirname(filePath)); + writeFileSync(filePath, buildMdFromScratch(project, model)); + } + updated.push(choice.file); + } + } else { + // File exists — update the GuardLink block (marker-based replacement) + if (choice.file.endsWith('.mdc')) { + if (!dryRun) { + writeFileSync(filePath, cursorMdcContentWithModel(project, model)); + } + updated.push(choice.file); + } else if (choice.file === '.cursorrules' || choice.file === '.windsurfrules' || choice.file === '.clinerules') { + const existing = readFileSync(filePath, 'utf-8'); + if (!dryRun) { + const block = wrapMarkers(cursorRulesContentWithModel(project, model)); + writeFileSync(filePath, replaceOrAppend(existing, block)); + } + updated.push(choice.file); + } else if (choice.file.endsWith('settings.json')) { + skipped.push(`${choice.file} (json format — not supported)`); + } else { + const existing = readFileSync(filePath, 'utf-8'); + if (!dryRun) { + const block = wrapMarkers(agentInstructionsWithModel(project, model)); + writeFileSync(filePath, replaceOrAppend(existing, block)); + } + updated.push(choice.file); + } + } + } + + return { updated, skipped }; +} diff --git a/src/init/picker.ts b/src/init/picker.ts index 413dd2f..78cc8c0 100644 --- a/src/init/picker.ts +++ b/src/init/picker.ts @@ -82,11 +82,12 @@ export async function promptAgentSelection(agentFiles: AgentFile[]): Promise 0) { - console.error('\n Also add instructions for? (comma-separated numbers, Enter to skip)\n'); + console.error(`\n Also add instructions for? (comma-separated numbers, Enter to skip)\n`); for (let i = 0; i < optionalChoices.length; i++) { const c = optionalChoices[i]; console.error(` ${i + 1}. ${c.label.padEnd(18)} → ${c.file}`); } + console.error(` ${optionalChoices.length + 1}. ${'All of the above'.padEnd(18)}`); console.error(''); } @@ -104,12 +105,21 @@ export async function promptAgentSelection(agentFiles: AgentFile[]): Promise parseInt(s.trim(), 10)).filter(n => !isNaN(n)); - for (const n of nums) { - if (n >= 1 && n <= optionalChoices.length) { - selectedIds.push(optionalChoices[n - 1].id); + const trimmed = answer.trim().toLowerCase(); + const allIdx = optionalChoices.length + 1; + + if (trimmed) { + const nums = trimmed.split(/[,\s]+/).map(s => parseInt(s, 10)).filter(n => !isNaN(n)); + if (nums.includes(allIdx)) { + // "All of the above" selected + for (const c of optionalChoices) { + selectedIds.push(c.id); + } + } else { + for (const n of nums) { + if (n >= 1 && n <= optionalChoices.length) { + selectedIds.push(optionalChoices[n - 1].id); + } } } } else if (selectedIds.length === 0) { diff --git a/src/init/templates.ts b/src/init/templates.ts index 947a0e0..46b9560 100644 --- a/src/init/templates.ts +++ b/src/init/templates.ts @@ -3,6 +3,7 @@ */ import type { ProjectInfo } from './detect.js'; +import type { ThreatModel } from '../types/index.js'; // ─── Canonical reference document ──────────────────────────────────── @@ -25,7 +26,7 @@ DEFINE @asset (#id) -- "description" RELATE @mitigates against <#threat> using <#control> -- "how" @exposes to <#threat> [severity] cwe:CWE-NNN -- "what's wrong" - @accepts <#threat> on -- "why acceptable" + @accepts <#threat> on -- "HUMAN-ONLY — AI agents must use @audit instead" @transfers <#threat> from to -- "who handles it" FLOW @flows -> via -- "details" @@ -55,19 +56,20 @@ Append after severity: \`cwe:CWE-89\`, \`owasp:A03:2021\`, \`capec:CAPEC-66\`, \ ## Rules -1. **Define once, reference everywhere.** \`@asset\`, \`@threat\`, \`@control\` with \`(#id)\` go in \`.guardlink/definitions${project.definitionsExt}\`. Source files use relationship verbs only (\`@mitigates\`, \`@exposes\`, etc.). -2. **Read definitions before adding.** Check for existing IDs first — avoid duplicates. -3. **Every \`@exposes\` needs a response.** Match with \`@mitigates\` (fix exists), \`@audit\` (flag for review), or \`@accepts\` (human-only risk acceptance). AI agents must NEVER write \`@accepts\` — use \`@audit\` instead. -4. **Always annotate security-relevant code.** At minimum, add \`@comment\` to document intent. +1. **Annotate as you code.** When you write or modify security-relevant code (endpoints, auth, data access, validation, I/O, crypto, process spawning), add annotations in the same change. This is required, not optional. +2. **Define once, reference everywhere.** \`@asset\`, \`@threat\`, \`@control\` with \`(#id)\` go in \`.guardlink/definitions${project.definitionsExt}\`. Source files use relationship verbs only (\`@mitigates\`, \`@exposes\`, etc.). +3. **Read definitions before adding.** Check for existing IDs first — avoid duplicates. +4. **Every \`@exposes\` needs a response.** Match with \`@mitigates\` (fix exists) or \`@audit\` (flag for human review). AI agents must NEVER write \`@accepts\` — that is a human-only governance decision. Use \`@audit\` instead. 5. **Use the full verb set.** \`@flows\` for data movement, \`@handles\` for data classification, \`@boundary\` for trust boundaries. ## When Writing Code | Situation | Annotation | |-----------|------------| +| 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\` | -| Risk with no fix yet | \`@audit Asset\` + \`@comment\` explaining potential controls | +| Security gap exists | \`@exposes Asset to #threat\` + \`@audit Asset\` | +| 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\` | @@ -106,20 +108,26 @@ export function agentInstructions(project: ProjectInfo): string { This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. **Full reference: \`docs/GUARDLINK_REFERENCE.md\`** +### Core Requirement + +**Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change.** This includes: new endpoints, authentication/authorization logic, data validation, database queries, file I/O, external API calls, crypto operations, process spawning, user input handling, and configuration parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helper functions that never touch security boundaries. + ### Key Rules -1. Do not delete or mangle existing GuardLink annotations. Treat them as part of the code. Edit only when intentionally changing the threat model. -2. Definitions (\`@asset\`, \`@threat\`, \`@control\` with \`(#id)\`) live in \`.guardlink/definitions${project.definitionsExt}\`. Reuse existing \`#id\`s — never redefine. -3. Source files use relationship verbs only: \`@mitigates\`, \`@exposes\`, \`@flows\`, \`@handles\`, \`@boundary\`, \`@comment\`, \`@validates\`, \`@audit\`, \`@owns\`, \`@assumes\`, \`@transfers\`. -4. Every \`@exposes\` should be paired with \`@mitigates\` or \`@audit\` (for human review). NEVER write \`@accepts\` — that is a human-only governance decision. -5. Prefer coupled blocks that tell a complete story near the code they describe (risk + control + flow + note). -6. Avoid \`@shield\` unless a human explicitly asks to hide code from AI — it creates blind spots. +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${project.definitionsExt}\`. 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. ### Workflow (while coding) -- Before adding new annotations: skim \`.guardlink/definitions${project.definitionsExt}\` to reuse IDs. -- After changes: run \`guardlink validate .\` to catch syntax/dangling refs; run \`guardlink status .\` to check coverage; commit updates with the code. -- When adding features: add or update annotations in the same PR. +- Before writing code: skim \`.guardlink/definitions${project.definitionsExt}\` to understand existing assets, threats, and controls. +- While writing code: add annotations above or in the doc-block of security-relevant functions as you write them — not as a separate pass afterward. +- After changes: run \`guardlink validate .\` to catch syntax/dangling refs; run \`guardlink status .\` to check coverage; commit annotation updates with the code. +- After adding annotations: run \`guardlink sync\` to update all agent instruction files with the current threat model context. This ensures every agent sees the latest assets, threats, controls, and open exposures. ### Tools @@ -143,6 +151,120 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c `.trimStart(); } +// ─── Model-aware instruction block (for sync) ────────────────────── + +/** + * Build a threat model context section that gets embedded into agent instructions. + * Contains real asset/threat/control IDs, open exposures, and existing flows + * so any coding agent knows the current security posture. + */ +export function buildModelContext(model: ThreatModel): string { + const sections: string[] = []; + + // Existing defined IDs + const assetIds = model.assets.filter(a => a.id).map(a => `#${a.id} (${a.path})`); + const threatIds = model.threats.filter(t => t.id).map(t => `#${t.id} (${t.name})${t.severity ? ` [${t.severity}]` : ''}`); + const controlIds = model.controls.filter(c => c.id).map(c => `#${c.id} (${c.name})`); + + if (assetIds.length + threatIds.length + controlIds.length > 0) { + sections.push('### Current Definitions (REUSE these IDs — do NOT redefine)\n'); + if (assetIds.length) sections.push(`**Assets:** ${assetIds.join(', ')}`); + if (threatIds.length) sections.push(`**Threats:** ${threatIds.join(', ')}`); + if (controlIds.length) sections.push(`**Controls:** ${controlIds.join(', ')}`); + } + + // Open exposures (unmitigated) + const unmitigated = model.exposures.filter(e => + !model.mitigations.some(m => m.asset === e.asset && m.threat === e.threat) + ); + if (unmitigated.length > 0) { + sections.push('\n### Open Exposures (need @mitigates or @audit)\n'); + const lines = unmitigated.slice(0, 25).map(e => + `- ${e.asset} exposed to ${e.threat}${e.severity ? ` [${e.severity}]` : ''} (${e.location.file}:${e.location.line})` + ); + sections.push(lines.join('\n')); + if (unmitigated.length > 25) sections.push(`- ... and ${unmitigated.length - 25} more`); + } + + // Existing flows (top 20) + if (model.flows.length > 0) { + sections.push('\n### Existing Data Flows (extend, don\'t duplicate)\n'); + const flowLines = model.flows.slice(0, 20).map(f => + `- ${f.source} -> ${f.target}${f.mechanism ? ` via ${f.mechanism}` : ''}` + ); + sections.push(flowLines.join('\n')); + if (model.flows.length > 20) sections.push(`- ... and ${model.flows.length - 20} more`); + } + + // Summary stats + const stats = [ + `${model.annotations_parsed} annotations`, + `${model.assets.length} assets`, + `${model.threats.length} threats`, + `${model.controls.length} controls`, + `${model.exposures.length} exposures`, + `${model.mitigations.length} mitigations`, + `${model.flows.length} flows`, + ].join(', '); + sections.push(`\n### Model Stats\n\n${stats}`); + + return sections.join('\n'); +} + +/** + * Enhanced agent instructions that include live threat model context. + * Used by `guardlink sync` to keep all agent instruction files up to date. + */ +export function agentInstructionsWithModel(project: ProjectInfo, model: ThreatModel | null): string { + const base = agentInstructions(project); + + if (!model || model.annotations_parsed === 0) { + return base; + } + + const modelCtx = buildModelContext(model); + return `${base} +## Live Threat Model Context (auto-synced by \`guardlink sync\`) + +${modelCtx} + +> **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 +> and continue annotating new code using the same threat model vocabulary. +`; +} + +/** + * Enhanced cursor rules content with model context. + */ +export function cursorRulesContentWithModel(project: ProjectInfo, model: ThreatModel | null): string { + const base = cursorRulesContent(project); + + if (!model || model.annotations_parsed === 0) { + return base; + } + + const modelCtx = buildModelContext(model); + return `${base} +## Live Threat Model Context (auto-synced by \`guardlink sync\`) + +${modelCtx} +`; +} + +/** + * Enhanced cursor .mdc content with model context. + */ +export function cursorMdcContentWithModel(project: ProjectInfo, model: ThreatModel | null): string { + return `--- +description: GuardLink security annotation rules +globs: +alwaysApply: true +--- + +${cursorRulesContentWithModel(project, model)}`; +} + // ─── Cursor-specific format ────────────────────────────────────────── export function cursorRulesContent(project: ProjectInfo): string { @@ -152,17 +274,23 @@ export function cursorRulesContent(project: ProjectInfo): string { This project uses GuardLink annotations in source code comments. +## Core Requirement +Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change. This includes: new endpoints, auth logic, data validation, database queries, file I/O, external API calls, crypto, process spawning, user input handling, config parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helpers that never touch security boundaries. + ## Key Rules -- Preserve existing annotations — do not delete or mangle them. Edit only when intentionally changing the model. -- Definitions (@asset, @threat, @control with (#id)) live in .guardlink/definitions${project.definitionsExt}. Reuse IDs — never redefine. +- ANNOTATE NEW CODE. When you add a function or endpoint that handles user input, accesses data, or crosses a trust boundary — add @exposes, @mitigates, @flows, @handles, or at minimum @comment. This is not optional. +- NEVER write @accepts — that is a human-only governance decision. For risks with no mitigation: write @exposes + @audit + @comment suggesting potential controls. +- Preserve existing annotations — do not delete or mangle them. +- Definitions (@asset, @threat, @control with (#id)) live in .guardlink/definitions${project.definitionsExt}. Reuse IDs — never redefine. Add new definitions there first, then reference in source files. - Source files use relationship verbs: @mitigates, @exposes, @flows, @handles, @boundary, @comment, @validates, @audit, @owns, @assumes, @transfers. -- Pair @exposes with @mitigates or @audit. NEVER write @accepts — that is a human-only governance decision. -- When no mitigation exists, use @audit to flag for human review + @comment to suggest potential controls. +- Write coupled annotation blocks: risk + control (or audit) + data flow + context note. - Avoid @shield unless a human explicitly asks to hide code from AI. ## Workflow -- Before changes: skim .guardlink/definitions${project.definitionsExt}. +- Before writing code: skim .guardlink/definitions${project.definitionsExt} to understand existing IDs. +- While writing code: add annotations as you write, not as a separate pass afterward. - After changes: run \`guardlink validate .\` and \`guardlink status .\`. +- After adding annotations: run \`guardlink sync\` to update all agent instruction files with current threat model context. ## Quick Syntax - @exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 68bcbcd..208e75a 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -40,7 +40,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; -import { parseProject, findDanglingRefs, findUnmitigatedExposures } from '../parser/index.js'; +import { parseProject, findDanglingRefs, findUnmitigatedExposures, clearAnnotations } from '../parser/index.js'; import { generateSarif } from '../analyzer/index.js'; import { generateReport } from '../report/index.js'; import { generateDashboardHTML } from '../dashboard/index.js'; @@ -49,6 +49,7 @@ import { lookup, type LookupQuery } from './lookup.js'; import { suggestAnnotations } from './suggest.js'; import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, buildConfig, serializeModel, serializeModelCompact, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage, type AnalysisFramework } from '../analyze/index.js'; import { buildAnnotatePrompt } from '../agents/prompts.js'; +import { syncAgentFiles } from '../init/index.js'; import type { ThreatModel } from '../types/index.js'; // ─── Cached model ──────────────────────────────────────────────────── @@ -428,6 +429,82 @@ export function createServer(): McpServer { }, ); + // ── Tool: guardlink_sync ── + server.tool( + 'guardlink_sync', + 'Sync all agent instruction files (CLAUDE.md, .cursorrules, etc.) with the current threat model. Injects live asset/threat/control IDs, open exposures, and data flows so every coding agent knows the current security posture. Run after adding or changing annotations.', + { + root: z.string().describe('Project root directory').default('.'), + }, + async ({ root }) => { + const { model } = await getModel(root); + const result = syncAgentFiles({ root, model }); + const summary = [ + `Synced ${result.updated.length} agent instruction file(s): ${result.updated.join(', ')}`, + result.skipped.length > 0 ? `Skipped: ${result.skipped.join(', ')}` : '', + `Model: ${model.assets.length} assets, ${model.threats.length} threats, ${model.controls.length} controls, ${model.exposures.length} exposures`, + ].filter(Boolean).join('\n'); + return { + content: [{ type: 'text', text: summary }], + }; + }, + ); + + // ── Tool: guardlink_clear ── + server.tool( + 'guardlink_clear', + 'Remove all GuardLink annotations from source files. Use --dry-run to preview without modifying files. WARNING: destructive operation — requires explicit user confirmation before calling without dry-run.', + { + root: z.string().describe('Project root directory').default('.'), + dry_run: z.boolean().describe('If true, only show what would be removed').default(true), + include_definitions: z.boolean().describe('Also clear .guardlink/definitions files').default(false), + }, + async ({ root, dry_run, include_definitions }) => { + const result = await clearAnnotations({ + root, + dryRun: dry_run, + includeDefinitions: include_definitions, + }); + + if (result.totalRemoved === 0) { + return { content: [{ type: 'text', text: 'No GuardLink annotations found in source files.' }] }; + } + + const fileList = Array.from(result.perFile.entries()) + .map(([file, count]) => ` ${file} (${count} line${count > 1 ? 's' : ''})`) + .join('\n'); + + const mode = dry_run ? '(DRY RUN) Would remove' : 'Removed'; + return { + content: [{ type: 'text', text: `${mode} ${result.totalRemoved} annotation line(s) from ${result.modifiedFiles.length} file(s):\n${fileList}` }], + }; + }, + ); + + // ── Tool: guardlink_unannotated ── + server.tool( + 'guardlink_unannotated', + 'List source files that have no GuardLink annotations. Useful for identifying coverage gaps. Not all files need annotations — only those touching security boundaries (endpoints, auth, data access, I/O, crypto).', + { + root: z.string().describe('Project root directory').default('.'), + }, + async ({ root }) => { + const { model } = await getModel(root); + const unannotated = model.unannotated_files || []; + const annotated = model.annotated_files || []; + const total = annotated.length + unannotated.length; + + if (unannotated.length === 0) { + return { content: [{ type: 'text', text: `All ${total} source files have GuardLink annotations.` }] }; + } + + const fileList = unannotated.map(f => ` ${f}`).join('\n'); + return { + content: [{ type: 'text', text: `${annotated.length} of ${total} files annotated. ${unannotated.length} file(s) with no annotations:\n${fileList}\n\nNot all files need annotations — only those touching security boundaries.` }], + }; + }, + ); + // ── Resource: guardlink://model ── server.resource( 'threat-model', diff --git a/src/parser/clear.ts b/src/parser/clear.ts new file mode 100644 index 0000000..9d88f4c --- /dev/null +++ b/src/parser/clear.ts @@ -0,0 +1,188 @@ +/** + * GuardLink — Annotation clearing utility. + * Scans project source files and removes all GuardLink annotation comment lines. + * + * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. + */ + +import fg from 'fast-glob'; +import { readFile, writeFile } from 'node:fs/promises'; +import { relative } from 'node:path'; +import { stripCommentPrefix } from './comment-strip.js'; +import { parseLine } from './parse-line.js'; + +// ─── Known GuardLink verbs ────────────────────────────────────────── + +const GUARDLINK_VERBS = new Set([ + 'asset', 'threat', 'control', + 'mitigates', 'exposes', 'accepts', 'transfers', 'flows', 'boundary', + 'validates', 'audit', 'owns', 'handles', 'assumes', + 'comment', 'shield', 'shield:begin', 'shield:end', + // v1 compat + 'review', 'connects', +]); + +const DEFAULT_INCLUDE = [ + '**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', + '**/*.py', '**/*.rb', '**/*.go', '**/*.rs', + '**/*.java', '**/*.kt', '**/*.scala', + '**/*.c', '**/*.cpp', '**/*.cc', '**/*.h', '**/*.hpp', + '**/*.cs', '**/*.swift', '**/*.dart', + '**/*.sql', '**/*.lua', '**/*.hs', + '**/*.tf', '**/*.hcl', + '**/*.yaml', '**/*.yml', + '**/*.sh', '**/*.bash', + '**/*.html', '**/*.xml', '**/*.svg', + '**/*.css', + '**/*.ex', '**/*.exs', +]; + +const DEFAULT_EXCLUDE = [ + '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', + '**/__pycache__/**', '**/target/**', '**/vendor/**', '**/.next/**', + '**/tests/**', '**/test/**', '**/__tests__/**', +]; + +// ─── Types ────────────────────────────────────────────────────────── + +export interface ClearAnnotationsOptions { + root: string; + include?: string[]; + exclude?: string[]; + /** If true, don't write files — just report what would be removed */ + dryRun?: boolean; + /** If true, also clear .guardlink/definitions files */ + includeDefinitions?: boolean; +} + +export interface ClearAnnotationsResult { + /** Files that were modified (annotations removed) */ + modifiedFiles: string[]; + /** Total annotation lines removed across all files */ + totalRemoved: number; + /** Per-file breakdown: relative path → count of lines removed */ + perFile: Map; +} + +// ─── Core logic ───────────────────────────────────────────────────── + +/** + * Check if a source line contains a GuardLink annotation. + */ +function isGuardLinkAnnotationLine(line: string): boolean { + const inner = stripCommentPrefix(line); + if (inner === null) return false; + + const trimmed = inner.trim(); + if (!trimmed.startsWith('@')) return false; + + // Extract the verb + const verbMatch = trimmed.match(/^@(\S+)/); + if (!verbMatch) return false; + + const verb = verbMatch[1]; + return GUARDLINK_VERBS.has(verb); +} + +/** + * Check if a line is a continuation description line (-- "...") that follows + * a GuardLink annotation. + */ +function isContinuationLine(line: string): boolean { + const inner = stripCommentPrefix(line); + if (inner === null) return false; + return /^--\s*"/.test(inner.trim()); +} + +/** + * Remove all GuardLink annotation lines from a file's content. + * Returns the cleaned content and count of lines removed. + * + * Also removes: + * - Continuation lines (-- "...") that follow an annotation + * - Empty comment lines that are left between annotations (cleanup) + */ +function removeAnnotationsFromContent(content: string): { cleaned: string; removed: number } { + const lines = content.split('\n'); + const result: string[] = []; + let removed = 0; + let lastWasAnnotation = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (isGuardLinkAnnotationLine(line)) { + removed++; + lastWasAnnotation = true; + continue; + } + + // Remove continuation lines that follow an annotation + if (lastWasAnnotation && isContinuationLine(line)) { + removed++; + continue; + } + + lastWasAnnotation = false; + result.push(line); + } + + // Clean up: remove consecutive empty lines left behind (collapse to max 1) + const final: string[] = []; + let prevEmpty = false; + for (const line of result) { + const isEmpty = line.trim() === ''; + if (isEmpty && prevEmpty) continue; // skip consecutive empties + final.push(line); + prevEmpty = isEmpty; + } + + return { cleaned: final.join('\n'), removed }; +} + +/** + * Scan all project source files and remove GuardLink annotations. + */ +export async function clearAnnotations(options: ClearAnnotationsOptions): Promise { + const { + root, + include = DEFAULT_INCLUDE, + exclude = DEFAULT_EXCLUDE, + dryRun = false, + includeDefinitions = false, + } = options; + + // Build exclude list — skip .guardlink/ definitions unless explicitly included + const effectiveExclude = includeDefinitions + ? exclude + : [...exclude, '**/.guardlink/**']; + + const files = await fg(include, { + cwd: root, + ignore: effectiveExclude, + absolute: true, + dot: true, + }); + + const modifiedFiles: string[] = []; + const perFile = new Map(); + let totalRemoved = 0; + + for (const filePath of files) { + const content = await readFile(filePath, 'utf-8'); + const { cleaned, removed } = removeAnnotationsFromContent(content); + + if (removed > 0) { + const relPath = relative(root, filePath); + modifiedFiles.push(relPath); + perFile.set(relPath, removed); + totalRemoved += removed; + + if (!dryRun) { + await writeFile(filePath, cleaned); + } + } + } + + return { modifiedFiles, totalRemoved, perFile }; +} diff --git a/src/parser/index.ts b/src/parser/index.ts index 0ea0fd0..aa8f456 100644 --- a/src/parser/index.ts +++ b/src/parser/index.ts @@ -9,3 +9,5 @@ export { parseLine } from './parse-line.js'; export { normalizeName, resolveSeverity, unescapeDescription } from './normalize.js'; export { stripCommentPrefix, commentStyleForExt } from './comment-strip.js'; export { findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from './validate.js'; +export { clearAnnotations } from './clear.js'; +export type { ClearAnnotationsOptions, ClearAnnotationsResult } from './clear.js'; diff --git a/src/parser/parse-project.ts b/src/parser/parse-project.ts index 516e8d9..51916a8 100644 --- a/src/parser/parse-project.ts +++ b/src/parser/parse-project.ts @@ -82,15 +82,20 @@ export async function parseProject(options: ParseProjectOptions): Promise<{ // Parse all files const allAnnotations: Annotation[] = []; const allDiagnostics: ParseDiagnostic[] = []; + const filesWithAnnotations = new Set(); for (const file of files) { const result = await parseFile(file); + const relPath = relative(root, file); // Normalize file paths to relative for (const ann of result.annotations) { - ann.location.file = relative(root, ann.location.file); + ann.location.file = relPath; } for (const diag of result.diagnostics) { - diag.file = relative(root, diag.file); + diag.file = relPath; + } + if (result.annotations.length > 0) { + filesWithAnnotations.add(relPath); } allAnnotations.push(...result.annotations); allDiagnostics.push(...result.diagnostics); @@ -115,8 +120,15 @@ export async function parseProject(options: ParseProjectOptions): Promise<{ } } + // Compute annotated vs unannotated files (exclude .guardlink/ definitions from unannotated) + const allRelPaths = files.map(f => relative(root, f)); + const annotatedFiles = [...filesWithAnnotations].sort(); + const unannotatedFiles = allRelPaths + .filter(f => !filesWithAnnotations.has(f) && !f.startsWith('.guardlink/') && !f.startsWith('.guardlink\\')) + .sort(); + // Assemble ThreatModel - const model = assembleModel(allAnnotations, files.length, project); + const model = assembleModel(allAnnotations, files.length, project, annotatedFiles, unannotatedFiles); return { model, diagnostics: allDiagnostics }; } @@ -126,13 +138,15 @@ function getAnnotationId(ann: Annotation): string | undefined { return undefined; } -function assembleModel(annotations: Annotation[], fileCount: number, project: string): ThreatModel { +function assembleModel(annotations: Annotation[], fileCount: number, project: string, annotatedFiles: string[], unannotatedFiles: string[]): ThreatModel { const model: ThreatModel = { version: '1.1.0', project, generated_at: new Date().toISOString(), source_files: fileCount, annotations_parsed: annotations.length, + annotated_files: annotatedFiles, + unannotated_files: unannotatedFiles, assets: [], threats: [], controls: [], diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 06e25a6..3c82dcc 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -7,8 +7,8 @@ import { resolve, basename, isAbsolute } from 'node:path'; import { readFileSync, existsSync, writeFileSync, mkdirSync } from 'node:fs'; -import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from '../parser/index.js'; -import { initProject, detectProject, promptAgentSelection } from '../init/index.js'; +import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } from '../parser/index.js'; +import { initProject, detectProject, promptAgentSelection, syncAgentFiles } from '../init/index.js'; import { generateReport, generateMermaid } from '../report/index.js'; import { generateDashboardHTML } from '../dashboard/index.js'; import { computeStats, computeSeverity, computeExposures } from '../dashboard/data.js'; @@ -96,6 +96,8 @@ export function cmdHelp(): void { ['/threat-reports', 'List saved AI threat reports'], ['/annotate ', 'Launch coding agent to annotate codebase'], ['/model', 'Set AI provider (API or CLI agent: Claude Code, Codex, Gemini)'], + ['/clear', 'Remove all annotations from source files (start fresh)'], + ['/sync', 'Sync agent instruction files with current threat model'], ['(freeform text)', 'Chat about your threat model with AI'], ['', ''], ['/report', 'Generate markdown + JSON report'], @@ -303,7 +305,10 @@ export function cmdStatus(ctx: TuiContext): void { console.log(` ${C.dim('Assets:')} ${stats.assets} ${C.dim('Threats:')} ${stats.threats} ${C.dim('Controls:')} ${stats.controls}`); console.log(` ${C.dim('Flows:')} ${stats.flows} ${C.dim('Boundaries:')} ${stats.boundaries} ${C.dim('Annotations:')} ${stats.annotations}`); console.log(` ${C.dim('Coverage:')} ${stats.coverageAnnotated}/${stats.coverageTotal} symbols (${stats.coveragePercent}%)`); - + console.log(` ${C.dim('Files:')} ${m.annotated_files.length} annotated, ${m.unannotated_files.length} not annotated of ${m.source_files} scanned`); + if (m.unannotated_files.length > 0) { + console.log(` ${C.dim('Run')} /unannotated ${C.dim('to list files without annotations')}`); + } // Top threats if (m.exposures.length > 0) { const threatCounts = new Map(); @@ -778,6 +783,14 @@ export async function cmdParse(ctx: TuiContext): Promise { console.log(` ${C.success('✓')} Parsed ${C.bold(String(model.annotations_parsed))} annotations from ${model.source_files} files`); console.log(` ${model.assets.length} assets · ${model.threats.length} threats · ${model.controls.length} controls`); console.log(` ${model.exposures.length} exposures · ${model.mitigations.length} mitigations · Grade: ${gradeColored(grade)}`); + + // Auto-sync agent instruction files with updated model + if (model.annotations_parsed > 0) { + const syncResult = syncAgentFiles({ root: ctx.root, model }); + if (syncResult.updated.length > 0) { + console.log(C.dim(` ↻ Synced ${syncResult.updated.length} agent instruction file(s)`)); + } + } console.log(''); } catch (err: any) { console.log(C.error(` ✗ Parse failed: ${err.message}`)); @@ -1560,6 +1573,115 @@ Keep responses under 500 words unless the user asks for detail.`; } } +// ─── /clear ────────────────────────────────────────────────────── + +export async function cmdClear(args: string, ctx: TuiContext): Promise { + const includeDefinitions = args.includes('--include-definitions'); + const isDryRun = args.includes('--dry-run'); + + console.log(C.dim(' Scanning for annotations...')); + const preview = await clearAnnotations({ + root: ctx.root, + dryRun: true, + includeDefinitions, + }); + + if (preview.totalRemoved === 0) { + console.log(''); + console.log(C.dim(' No GuardLink annotations found in source files.')); + console.log(''); + return; + } + + console.log(''); + console.log(` Found ${C.bold(String(preview.totalRemoved))} annotation line(s) across ${C.bold(String(preview.modifiedFiles.length))} file(s):`); + console.log(''); + for (const [file, count] of preview.perFile) { + console.log(` ${file} ${C.dim(`(${count} line${count > 1 ? 's' : ''})`)}`); + } + console.log(''); + + if (isDryRun) { + console.log(C.dim(' (dry run) No files were modified.')); + console.log(''); + return; + } + + const answer = await ask(ctx, ` ${C.warn('⚠')} Remove all annotations? This cannot be undone. (y/N): `); + if (answer.trim().toLowerCase() !== 'y') { + console.log(C.dim(' Cancelled.')); + console.log(''); + return; + } + + const result = await clearAnnotations({ + root: ctx.root, + dryRun: false, + includeDefinitions, + }); + + console.log(''); + console.log(` ${C.success('✓')} Removed ${C.bold(String(result.totalRemoved))} annotation line(s) from ${result.modifiedFiles.length} file(s).`); + console.log(C.dim(' Run /annotate to re-annotate from scratch, or /parse to update the model.')); + + ctx.model = null; + ctx.lastExposures = []; + console.log(''); +} + +// ─── /sync ─────────────────────────────────────────────────────── + +export async function cmdSync(ctx: TuiContext): Promise { + if (!ctx.model) { + console.log(C.warn(' No threat model. Run /parse first.')); + return; + } + + console.log(C.dim(' Syncing agent instruction files with current threat model...')); + console.log(''); + + const result = syncAgentFiles({ root: ctx.root, model: ctx.model }); + + if (result.updated.length > 0) { + console.log(` ${C.success('✓')} Updated ${C.bold(String(result.updated.length))} agent instruction file(s):`); + console.log(''); + for (const f of result.updated) { + console.log(` ${f}`); + } + } + if (result.skipped.length > 0) { + console.log(''); + console.log(C.dim(` Skipped: ${result.skipped.join(', ')}`)); + } + + console.log(''); + console.log(` ${C.dim(`${ctx.model.assets.length} assets, ${ctx.model.threats.length} threats, ${ctx.model.controls.length} controls, ${ctx.model.exposures.length} exposures synced.`)}`); + console.log(C.dim(' Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) will see these IDs.')); + console.log(''); +} + +// ─── /unannotated ──────────────────────────────────────────────────── + +export function cmdUnannotated(ctx: TuiContext): void { + if (!ctx.model) { + console.log(C.warn(' No threat model. Run /parse first.')); + return; + } + + const files = ctx.model.unannotated_files; + if (files.length === 0) { + console.log(`\n ${C.success('✓')} All source files have GuardLink annotations.\n`); + return; + } + + console.log(`\n ${C.warn('⚠')} ${C.bold(String(files.length))} source file(s) with no annotations:\n`); + for (const f of files) { + console.log(` ${f}`); + } + console.log(`\n ${C.dim('Not all files need annotations — only those that touch security boundaries.')}`); + console.log(''); +} + // ─── /report ───────────────────────────────────────────────────────── export async function cmdReport(ctx: TuiContext): Promise { diff --git a/src/tui/index.ts b/src/tui/index.ts index b3ae35e..ad6c893 100644 --- a/src/tui/index.ts +++ b/src/tui/index.ts @@ -46,6 +46,9 @@ import { cmdThreatReports, cmdAnnotate, cmdChat, + cmdClear, + cmdSync, + cmdUnannotated, cmdReport, cmdDashboard, cmdGal, @@ -59,6 +62,7 @@ const COMMANDS = [ '/exposures', '/show', '/scan', '/assets', '/files', '/view', '/threat-report', '/threat-reports', '/annotate', '/model', + '/clear', '/sync', '/unannotated', '/report', '/dashboard', '/quit', ]; @@ -89,6 +93,9 @@ const PALETTE_COMMANDS: CommandEntry[] = [ { command: '/threat-reports', label: 'List saved threat reports' }, { command: '/annotate', label: 'Launch coding agent' }, { command: '/model', label: 'Set AI provider' }, + { command: '/clear', label: 'Remove all annotations from source files' }, + { command: '/sync', label: 'Sync agent instructions with current threat model' }, + { command: '/unannotated', label: 'List source files with no annotations' }, { command: '/report', label: 'Generate markdown report' }, { command: '/dashboard', label: 'HTML dashboard' }, { command: '/diff', label: 'Compare vs git ref' }, @@ -297,6 +304,9 @@ function printCommandList(): void { ['/threat-reports','List saved reports'], ['/annotate', 'Launch coding agent'], ['/model', 'Set AI provider'], + ['/clear', 'Clear all annotations'], + ['/sync', 'Sync agent instructions'], + ['/unannotated', 'List files without annotations'], ['/report', 'Generate reports'], ['/dashboard', 'HTML dashboard'], ['/diff [ref]', 'Compare vs git ref'], @@ -356,6 +366,9 @@ async function dispatch(input: string, ctx: TuiContext): Promise { case '/threat-report': await cmdThreatReport(args, ctx); break; case '/threat-reports': cmdThreatReports(ctx); break; case '/annotate': await cmdAnnotate(args, ctx); break; + case '/clear': await cmdClear(args, ctx); break; + case '/sync': await cmdSync(ctx); break; + case '/unannotated': cmdUnannotated(ctx); break; case '/report': await cmdReport(ctx); break; case '/dashboard': await cmdDashboard(ctx); break; default: diff --git a/src/types/index.ts b/src/types/index.ts index a659cdf..4ffb920 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -165,6 +165,8 @@ export interface ThreatModel { generated_at: string; source_files: number; annotations_parsed: number; + annotated_files: string[]; + unannotated_files: string[]; assets: ThreatModelAsset[]; threats: ThreatModelThreat[]; From dbbdb104abc6ec5f78aac6095a81afc109e18970 Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 21:14:38 -0600 Subject: [PATCH 2/7] chore: clear all source annotations for re-annotation with updated templates Removed 108 annotation lines from 16 source files using guardlink clear. Preserved: .guardlink/definitions.ts (17 assets, 20 threats, 15 controls), tests/fixtures/, agents/prompts.ts (example annotations in prompt templates). Previous annotations had systemic issues: - 11 @accepts with zero @audit pairs (rubber-stamped risk acceptances) - Duplicate acceptances across files for same risk - @accepts where @mitigates was correct (cli/index.ts arbitrary-write) - Missing @exposes for accepted risks (mcp path-traversal) Ready for re-annotation via Claude Code using updated templates that: - Prohibit @accepts (agent writes @audit + @comment instead) - Require coupled annotation blocks - Include sync guidance in workflow --- .clinerules | 87 ++++++++++++++++++++++++++++++ .cursor/rules/guardlink.mdc | 90 +++++++++++++++++++++++++++++++ .gemini/GEMINI.md | 105 ++++++++++++++++++++++++++++++++++++ .windsurfrules | 87 ++++++++++++++++++++++++++++++ AGENTS.md | 105 ++++++++++++++++++++++++++++++++++++ src/agents/launcher.ts | 8 --- src/analyze/index.ts | 7 --- src/analyze/llm.ts | 10 ---- src/analyze/tools.ts | 4 -- src/analyzer/sarif.ts | 6 --- src/cli/index.ts | 10 ---- src/dashboard/generate.ts | 5 -- src/diff/git.ts | 7 --- src/init/index.ts | 6 --- src/mcp/server.ts | 13 ----- src/mcp/suggest.ts | 6 --- src/parser/parse-file.ts | 2 - src/parser/parse-line.ts | 3 -- src/parser/parse-project.ts | 7 --- src/report/report.ts | 7 --- src/tui/index.ts | 8 --- 21 files changed, 474 insertions(+), 109 deletions(-) create mode 100644 .clinerules create mode 100644 .cursor/rules/guardlink.mdc create mode 100644 .gemini/GEMINI.md create mode 100644 .windsurfrules create mode 100644 AGENTS.md diff --git a/.clinerules b/.clinerules new file mode 100644 index 0000000..c88f4cf --- /dev/null +++ b/.clinerules @@ -0,0 +1,87 @@ + +# GuardLink Security Annotations + +This project uses GuardLink annotations in source code comments. + +## Core Requirement +Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change. This includes: new endpoints, auth logic, data validation, database queries, file I/O, external API calls, crypto, process spawning, user input handling, config parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helpers that never touch security boundaries. + +## Key Rules +- ANNOTATE NEW CODE. When you add a function or endpoint that handles user input, accesses data, or crosses a trust boundary — add @exposes, @mitigates, @flows, @handles, or at minimum @comment. This is not optional. +- NEVER write @accepts — that is a human-only governance decision. For risks with no mitigation: write @exposes + @audit + @comment suggesting potential controls. +- Preserve existing annotations — do not delete or mangle them. +- Definitions (@asset, @threat, @control with (#id)) live in .guardlink/definitions.ts. Reuse IDs — never redefine. Add new definitions there first, then reference in source files. +- Source files use relationship verbs: @mitigates, @exposes, @flows, @handles, @boundary, @comment, @validates, @audit, @owns, @assumes, @transfers. +- Write coupled annotation blocks: risk + control (or audit) + data flow + context note. +- Avoid @shield unless a human explicitly asks to hide code from AI. + +## Workflow +- Before writing code: skim .guardlink/definitions.ts to understand existing IDs. +- While writing code: add annotations as you write, not as a separate pass afterward. +- After changes: run `guardlink validate .` and `guardlink status .`. +- After adding annotations: run `guardlink sync` to update all agent instruction files with current threat model context. + +## Quick Syntax +- @exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +- @mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +- @audit App.API -- "Timing attack risk — needs human review" +- @flows User -> App.API via HTTPS -- "Login request" +- @boundary between #api and #db (#data-boundary) -- "Trust change" +- @handles pii on App.API -- "Processes email, token" +- @validates #prepared-stmts for App.API -- "CI test ensures placeholders" +- @audit App.API -- "Token rotation review" +- @owns security-team for App.API -- "Team responsible" +- @comment -- "Rate limit: 100 req/15min" + +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows + + diff --git a/.cursor/rules/guardlink.mdc b/.cursor/rules/guardlink.mdc new file mode 100644 index 0000000..4111110 --- /dev/null +++ b/.cursor/rules/guardlink.mdc @@ -0,0 +1,90 @@ +--- +description: GuardLink security annotation rules +globs: +alwaysApply: true +--- + +# GuardLink Security Annotations + +This project uses GuardLink annotations in source code comments. + +## Core Requirement +Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change. This includes: new endpoints, auth logic, data validation, database queries, file I/O, external API calls, crypto, process spawning, user input handling, config parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helpers that never touch security boundaries. + +## Key Rules +- ANNOTATE NEW CODE. When you add a function or endpoint that handles user input, accesses data, or crosses a trust boundary — add @exposes, @mitigates, @flows, @handles, or at minimum @comment. This is not optional. +- NEVER write @accepts — that is a human-only governance decision. For risks with no mitigation: write @exposes + @audit + @comment suggesting potential controls. +- Preserve existing annotations — do not delete or mangle them. +- Definitions (@asset, @threat, @control with (#id)) live in .guardlink/definitions.ts. Reuse IDs — never redefine. Add new definitions there first, then reference in source files. +- Source files use relationship verbs: @mitigates, @exposes, @flows, @handles, @boundary, @comment, @validates, @audit, @owns, @assumes, @transfers. +- Write coupled annotation blocks: risk + control (or audit) + data flow + context note. +- Avoid @shield unless a human explicitly asks to hide code from AI. + +## Workflow +- Before writing code: skim .guardlink/definitions.ts to understand existing IDs. +- While writing code: add annotations as you write, not as a separate pass afterward. +- After changes: run `guardlink validate .` and `guardlink status .`. +- After adding annotations: run `guardlink sync` to update all agent instruction files with current threat model context. + +## Quick Syntax +- @exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +- @mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +- @audit App.API -- "Timing attack risk — needs human review" +- @flows User -> App.API via HTTPS -- "Login request" +- @boundary between #api and #db (#data-boundary) -- "Trust change" +- @handles pii on App.API -- "Processes email, token" +- @validates #prepared-stmts for App.API -- "CI test ensures placeholders" +- @audit App.API -- "Token rotation review" +- @owns security-team for App.API -- "Team responsible" +- @comment -- "Rate limit: 100 req/15min" + +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md new file mode 100644 index 0000000..3703305 --- /dev/null +++ b/.gemini/GEMINI.md @@ -0,0 +1,105 @@ +# Guardlink — Project Instructions + + +## GuardLink — Security Annotations (Required) + +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +**Full reference: `docs/GUARDLINK_REFERENCE.md`** + +### Core Requirement + +**Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change.** This includes: new endpoints, authentication/authorization logic, data validation, database queries, file I/O, external API calls, crypto operations, process spawning, user input handling, and configuration parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helper functions that never touch security boundaries. + +### Key Rules + +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. + +### Workflow (while coding) + +- Before writing code: skim `.guardlink/definitions.ts` to understand existing assets, threats, and controls. +- While writing code: add annotations above or in the doc-block of security-relevant functions as you write them — not as a separate pass afterward. +- After changes: run `guardlink validate .` to catch syntax/dangling refs; run `guardlink status .` to check coverage; commit annotation updates with the code. +- After adding annotations: run `guardlink sync` to update all agent instruction files with the current threat model context. This ensures every agent sees the latest assets, threats, controls, and open exposures. + +### Tools + +- MCP tools (when available, e.g., Claude Code): `guardlink_lookup`, `guardlink_validate`, `guardlink_status`, `guardlink_parse`, `guardlink_suggest `. +- CLI equivalents (always available): `guardlink validate .`, `guardlink status .`, `guardlink parse .`. + +### Quick Syntax (common verbs) + +``` +@exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +@mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +@audit App.API -- "Timing attack risk — needs human review to assess bcrypt constant-time comparison" +@flows User -> App.API via HTTPS -- "Login request path" +@boundary between #api and #db (#data-boundary) -- "App → DB trust change" +@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" +@owns security-team for App.API -- "Team responsible for reviews" +@comment -- "Rate limit: 100 req/15min via express-rate-limit" +``` + +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows + +> **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 +> and continue annotating new code using the same threat model vocabulary. + + diff --git a/.windsurfrules b/.windsurfrules new file mode 100644 index 0000000..c88f4cf --- /dev/null +++ b/.windsurfrules @@ -0,0 +1,87 @@ + +# GuardLink Security Annotations + +This project uses GuardLink annotations in source code comments. + +## Core Requirement +Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change. This includes: new endpoints, auth logic, data validation, database queries, file I/O, external API calls, crypto, process spawning, user input handling, config parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helpers that never touch security boundaries. + +## Key Rules +- ANNOTATE NEW CODE. When you add a function or endpoint that handles user input, accesses data, or crosses a trust boundary — add @exposes, @mitigates, @flows, @handles, or at minimum @comment. This is not optional. +- NEVER write @accepts — that is a human-only governance decision. For risks with no mitigation: write @exposes + @audit + @comment suggesting potential controls. +- Preserve existing annotations — do not delete or mangle them. +- Definitions (@asset, @threat, @control with (#id)) live in .guardlink/definitions.ts. Reuse IDs — never redefine. Add new definitions there first, then reference in source files. +- Source files use relationship verbs: @mitigates, @exposes, @flows, @handles, @boundary, @comment, @validates, @audit, @owns, @assumes, @transfers. +- Write coupled annotation blocks: risk + control (or audit) + data flow + context note. +- Avoid @shield unless a human explicitly asks to hide code from AI. + +## Workflow +- Before writing code: skim .guardlink/definitions.ts to understand existing IDs. +- While writing code: add annotations as you write, not as a separate pass afterward. +- After changes: run `guardlink validate .` and `guardlink status .`. +- After adding annotations: run `guardlink sync` to update all agent instruction files with current threat model context. + +## Quick Syntax +- @exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +- @mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +- @audit App.API -- "Timing attack risk — needs human review" +- @flows User -> App.API via HTTPS -- "Login request" +- @boundary between #api and #db (#data-boundary) -- "Trust change" +- @handles pii on App.API -- "Processes email, token" +- @validates #prepared-stmts for App.API -- "CI test ensures placeholders" +- @audit App.API -- "Token rotation review" +- @owns security-team for App.API -- "Team responsible" +- @comment -- "Rate limit: 100 req/15min" + +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows + + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3703305 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,105 @@ +# Guardlink — Project Instructions + + +## GuardLink — Security Annotations (Required) + +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +**Full reference: `docs/GUARDLINK_REFERENCE.md`** + +### Core Requirement + +**Every time you write or modify code that touches security-relevant behavior, you MUST add GuardLink annotations in the same change.** This includes: new endpoints, authentication/authorization logic, data validation, database queries, file I/O, external API calls, crypto operations, process spawning, user input handling, and configuration parsing. Do NOT annotate pure business logic, formatting utilities, UI components, or helper functions that never touch security boundaries. + +### Key Rules + +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. + +### Workflow (while coding) + +- Before writing code: skim `.guardlink/definitions.ts` to understand existing assets, threats, and controls. +- While writing code: add annotations above or in the doc-block of security-relevant functions as you write them — not as a separate pass afterward. +- After changes: run `guardlink validate .` to catch syntax/dangling refs; run `guardlink status .` to check coverage; commit annotation updates with the code. +- After adding annotations: run `guardlink sync` to update all agent instruction files with the current threat model context. This ensures every agent sees the latest assets, threats, controls, and open exposures. + +### Tools + +- MCP tools (when available, e.g., Claude Code): `guardlink_lookup`, `guardlink_validate`, `guardlink_status`, `guardlink_parse`, `guardlink_suggest `. +- CLI equivalents (always available): `guardlink validate .`, `guardlink status .`, `guardlink parse .`. + +### Quick Syntax (common verbs) + +``` +@exposes App.API to #sqli [P0] cwe:CWE-89 -- "req.body.email concatenated into SQL" +@mitigates App.API against #sqli using #prepared-stmts -- "Parameterized queries via pg" +@audit App.API -- "Timing attack risk — needs human review to assess bcrypt constant-time comparison" +@flows User -> App.API via HTTPS -- "Login request path" +@boundary between #api and #db (#data-boundary) -- "App → DB trust change" +@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" +@owns security-team for App.API -- "Team responsible for reviews" +@comment -- "Rate limit: 100 req/15min via express-rate-limit" +``` + +## Live Threat Model Context (auto-synced by `guardlink sync`) + +### 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), #auth (Server,Auth) +**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], #sqli (SQL_Injection) [critical] +**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), #prepared-stmts (Prepared_Statements) + +### Open Exposures (need @mitigates or @audit) + +- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) +- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) +- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) +- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) +- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) +- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) +- #report exposed to #info-disclosure [low] (src/report/report.ts:7) +- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) + +### Existing Data Flows (extend, don't duplicate) + +- #cli -> #agent-launcher via launchAgent +- #agent-launcher -> External_Process via spawnSync +- User_Input -> #auth-api via POST./login +- #auth-api -> #user-db via TypeORM.findOne +- User_Browser -> #api-gateway via HTTPS +- #api-gateway -> #auth-service via internal.gRPC +- #auth-service -> #user-db via pg.query +- #auth-service -> #session-store via redis.set +- #auth-service -> User_Browser via Set-Cookie +- req.body.username -> db.query via string-concat +- #parser -> #sarif via ThreatModel +- #sarif -> External_Security_Tools via SARIF_JSON +- #parser -> #llm-client via ThreatModel +- #llm-client -> Filesystem via writeFileSync +- #llm-client -> External_LLM_APIs via fetch +- External_LLM_APIs -> #llm-client via response +- External_LLM_APIs -> #llm-tools via tool_call +- #llm-tools -> External_LLM_APIs via tool_result +- User -> #cli via argv +- #cli -> #parser via parseProject +- ... and 20 more + +### Model Stats + +193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows + +> **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 +> and continue annotating new code using the same threat model vocabulary. + + diff --git a/src/agents/launcher.ts b/src/agents/launcher.ts index cdc8334..47ac799 100644 --- a/src/agents/launcher.ts +++ b/src/agents/launcher.ts @@ -7,14 +7,6 @@ * * Clipboard copy is always performed first regardless of agent type. * - * @exposes #agent-launcher to #child-proc-injection [high] cwe:CWE-78 -- "Spawns child processes for AI coding agents" - * @exposes #agent-launcher to #cmd-injection [critical] cwe:CWE-78 -- "Windows launch uses shell:true in spawnSync" - * @mitigates #agent-launcher against #child-proc-injection using #process-sandbox -- "Agent commands are fixed binaries (claude, codex), not user-controlled" - * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "spawnSync with args array on macOS/Linux, only Windows uses shell" - * @flows #cli -> #agent-launcher via launchAgent -- "CLI invokes agent with prompt and cwd" - * @flows #agent-launcher -> External_Process via spawnSync -- "Spawns claude, codex, cursor, etc." - * @boundary between #agent-launcher and External_AI_Agents (#agent-boundary) -- "Process spawn crosses trust boundary to external AI tools" - * @comment -- "copyToClipboard uses platform-specific clipboard commands (pbcopy, xclip, clip)" */ import { spawnSync, spawn } from 'node:child_process'; diff --git a/src/analyze/index.ts b/src/analyze/index.ts index 7a41165..86f20db 100644 --- a/src/analyze/index.ts +++ b/src/analyze/index.ts @@ -5,13 +5,6 @@ * specific prompt, streams the response, and saves timestamped results * to .guardlink/threat-reports/. * - * @exposes #llm-client to #arbitrary-write [high] cwe:CWE-73 -- "Writes threat reports to .guardlink/threat-reports/" - * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "Serialized threat model embedded in LLM prompt" - * @accepts #prompt-injection on #llm-client -- "Core feature: threat model serialized as LLM prompt for analysis" - * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Reports written to fixed .guardlink/threat-reports/ subdirectory" - * @flows #parser -> #llm-client via ThreatModel -- "Parsed model data serialized for LLM analysis" - * @flows #llm-client -> Filesystem via writeFileSync -- "Analysis results saved as markdown files" - * @handles internal on #llm-client -- "Processes security-sensitive threat model for AI analysis" */ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; diff --git a/src/analyze/llm.ts b/src/analyze/llm.ts index 84f1e45..a234d13 100644 --- a/src/analyze/llm.ts +++ b/src/analyze/llm.ts @@ -10,16 +10,6 @@ * * Zero dependencies — uses Node 20+ built-in fetch. * - * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "Reads API keys from environment variables" - * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "Makes HTTP requests to configurable provider URLs" - * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "Sends threat model content as LLM prompt" - * @accepts #prompt-injection on #llm-client -- "Core feature: threat model data is sent to LLM for analysis" - * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded to known providers" - * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys read from env, not logged" - * @handles secrets on #llm-client -- "API keys held in memory during request lifecycle" - * @boundary between #llm-client and External_LLM_APIs (#llm-boundary) -- "HTTP requests cross network trust boundary to external AI providers" - * @flows #llm-client -> External_LLM_APIs via fetch -- "HTTP POST with auth headers and prompt payload" - * @flows External_LLM_APIs -> #llm-client via response -- "Streaming or complete response from LLM provider" */ export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'openrouter' | 'deepseek' | 'ollama'; diff --git a/src/analyze/tools.ts b/src/analyze/tools.ts index 10fbbaf..2583ec8 100644 --- a/src/analyze/tools.ts +++ b/src/analyze/tools.ts @@ -6,10 +6,6 @@ * - validate_finding: Cross-reference a finding against the parsed model * - search_codebase: Search project files for patterns * - * @flows External_LLM_APIs -> #llm-tools via tool_call -- "LLM requests tool execution" - * @flows #llm-tools -> External_LLM_APIs via tool_result -- "Tool results returned to LLM" - * @exposes #llm-tools to #ssrf [medium] cwe:CWE-918 -- "lookup_cve fetches external URLs" - * @mitigates #llm-tools against #ssrf using #url-allowlist -- "Only fetches from known CVE databases" */ import { readFileSync, readdirSync, statSync } from 'node:fs'; diff --git a/src/analyzer/sarif.ts b/src/analyzer/sarif.ts index 4fbd439..c120a96 100644 --- a/src/analyzer/sarif.ts +++ b/src/analyzer/sarif.ts @@ -12,12 +12,6 @@ * 2. Parse errors (annotation syntax problems) * 3. Dangling references (broken #id refs) * - * @exposes #sarif to #info-disclosure [low] cwe:CWE-200 -- "SARIF output contains detailed threat model findings" - * @accepts #info-disclosure on #sarif -- "SARIF export for security tools is the intended feature" - * @exposes #sarif to #arbitrary-write [high] cwe:CWE-73 -- "SARIF written to user-specified output path" - * @mitigates #sarif against #arbitrary-write using #path-validation -- "CLI resolves output path before write" - * @flows #parser -> #sarif via ThreatModel -- "SARIF generator receives parsed threat model" - * @flows #sarif -> External_Security_Tools via SARIF_JSON -- "Output consumed by GitHub, VS Code, etc." */ import type { ThreatModel, ThreatModelExposure, ParseDiagnostic, Severity } from '../types/index.js'; diff --git a/src/cli/index.ts b/src/cli/index.ts index 332e1cc..f14e9e8 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,15 +20,6 @@ * guardlink tui [dir] Interactive TUI with slash commands + AI chat * guardlink gal Display GAL annotation language quick reference * - * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "Accepts directory paths from command line arguments" - * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes reports and SARIF to user-specified output paths" - * @accepts #arbitrary-write on #cli -- "Intentional feature: users specify output paths for reports" - * @mitigates #cli against #path-traversal using #path-validation -- "resolve() normalizes paths before passing to submodules" - * @boundary between #cli and #parser (#cli-parser-boundary) -- "CLI is the primary user input trust boundary" - * @flows User -> #cli via argv -- "User provides directory paths and options via command line" - * @flows #cli -> #parser via parseProject -- "CLI dispatches parsed commands to parser" - * @flows #cli -> #report via generateReport -- "CLI writes report output" - * @flows #cli -> #init via initProject -- "CLI initializes project structure" */ import { Command } from 'commander'; @@ -1233,4 +1224,3 @@ function printUnannotatedFiles(model: ThreatModel) { console.log(` ${f}`); } } - diff --git a/src/dashboard/generate.ts b/src/dashboard/generate.ts index 423fe24..a8d06b8 100644 --- a/src/dashboard/generate.ts +++ b/src/dashboard/generate.ts @@ -5,11 +5,6 @@ * 7 pages: Summary, AI Analysis, Threats, Diagrams, Code, Data, Assets. * Mermaid.js via CDN for diagrams. Zero build step. * - * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Interpolates threat model data into HTML output" - * @mitigates #dashboard against #xss using #output-encoding -- "esc() function HTML-encodes all dynamic content" - * @flows #parser -> #dashboard via ThreatModel -- "Dashboard receives parsed threat model for visualization" - * @flows #dashboard -> Filesystem via writeFile -- "Generated HTML written to disk" - * @comment -- "esc() defined at line ~399 and ~1016 performs HTML entity encoding" */ import type { ThreatModel } from '../types/index.js'; diff --git a/src/diff/git.ts b/src/diff/git.ts index 47dfee8..baca960 100644 --- a/src/diff/git.ts +++ b/src/diff/git.ts @@ -3,13 +3,6 @@ * Resolves git refs to threat models by checking out files at a given commit * and parsing them in a temp directory. * - * @exposes #diff to #cmd-injection [critical] cwe:CWE-78 -- "Invokes git commands with execSync using user-provided ref" - * @exposes #diff to #path-traversal [high] cwe:CWE-22 -- "Writes files to temp directory based on git ls-tree output" - * @mitigates #diff against #cmd-injection using #param-commands -- "Git ref validated via git rev-parse before use in other commands" - * @mitigates #diff against #path-traversal using #resource-limits -- "Files written only to mkdtempSync temp directory" - * @flows #cli -> #diff via parseAtRef -- "CLI passes git ref from user arguments" - * @flows #diff -> git via execSync -- "Executes git rev-parse, git ls-tree, git show commands" - * @comment -- "Temp directory cleaned up in finally block after parsing" */ import { execSync } from 'node:child_process'; diff --git a/src/init/index.ts b/src/init/index.ts index 8d0b7ca..46ed461 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -5,12 +5,6 @@ * directory with shared definitions, and injects GuardLink instructions * into agent instruction files (CLAUDE.md, .cursorrules, etc.). * - * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Writes config files and agent instructions to user-specified root" - * @exposes #init to #path-traversal [high] cwe:CWE-22 -- "Creates directories and files based on user-provided root path" - * @mitigates #init against #arbitrary-write using #path-validation -- "Files written only to .guardlink/ subdirectory and known agent file locations" - * @mitigates #init against #path-traversal using #path-validation -- "join() used with fixed relative paths within root" - * @flows #cli -> #init via initProject -- "CLI passes user-specified root directory" - * @flows #init -> Filesystem via writeFileSync -- "Creates config.json, definitions.ts, agent instruction files" */ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'; diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 208e75a..4463a3f 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -22,19 +22,6 @@ * * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.) * - * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "All tools accept root param from external AI agents" - * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "guardlink_suggest output fed back to calling LLM" - * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "guardlink_report and guardlink_dashboard write files" - * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Exposes threat model details to connected agents" - * @accepts #path-traversal on #mcp -- "MCP clients (Claude Code, Cursor) are trusted local agents" - * @accepts #arbitrary-write on #mcp -- "MCP clients are trusted local agents with filesystem access" - * @accepts #prompt-injection on #mcp -- "Suggest output is intended for LLM consumption" - * @accepts #data-exposure on #mcp -- "Exposing threat model to agents is the core MCP feature" - * @boundary between #mcp and External_AI_Agents (#mcp-boundary) -- "Primary trust boundary: external AI agents invoke tools over stdio" - * @flows External_AI_Agents -> #mcp via stdio -- "Tool calls received from AI agent over stdio transport" - * @flows #mcp -> #parser via getModel -- "MCP tools invoke parser to build threat model" - * @flows #mcp -> External_AI_Agents via response -- "Tool results returned to calling agent" - * @handles internal on #mcp -- "Processes and exposes security-sensitive threat model data" */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; diff --git a/src/mcp/suggest.ts b/src/mcp/suggest.ts index bb96d6b..ddda7a0 100644 --- a/src/mcp/suggest.ts +++ b/src/mcp/suggest.ts @@ -9,12 +9,6 @@ * * Designed for both file-based and diff-based analysis (§8.2). * - * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "Reads files from user-specified paths for analysis" - * @exposes #suggest to #prompt-injection [medium] cwe:CWE-77 -- "Suggestion output may be fed back to LLM agents" - * @accepts #prompt-injection on #suggest -- "Suggestions are intended for LLM consumption" - * @mitigates #suggest against #path-traversal using #path-validation -- "join() combines root with relative file paths" - * @flows #mcp -> #suggest via suggestAnnotations -- "MCP tool passes file path from agent request" - * @flows #suggest -> #mcp via suggestions -- "Suggestions returned to calling agent" */ import { readFileSync, existsSync } from 'node:fs'; diff --git a/src/parser/parse-file.ts b/src/parser/parse-file.ts index eb17c9a..82126b0 100644 --- a/src/parser/parse-file.ts +++ b/src/parser/parse-file.ts @@ -2,8 +2,6 @@ * GuardLink — File-level parser. * Reads source files and extracts all GuardLink annotations. * - * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Reads files from disk using provided file path" - * @comment -- "File paths are controlled by parse-project glob results, not direct user input" */ import { readFile } from 'node:fs/promises'; diff --git a/src/parser/parse-line.ts b/src/parser/parse-line.ts index ecf35bf..e692d7b 100644 --- a/src/parser/parse-line.ts +++ b/src/parser/parse-line.ts @@ -2,9 +2,6 @@ * GuardLink — Line-level annotation parser. * Parses a single comment line into a typed Annotation. * - * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns run against untrusted annotation content" - * @mitigates #parser against #redos using #regex-anchoring -- "All patterns use ^ anchor and defined character classes" - * @comment -- "PATTERNS object contains all annotation regex; patterns are pre-compiled for performance" */ import type { diff --git a/src/parser/parse-project.ts b/src/parser/parse-project.ts index 51916a8..7db0658 100644 --- a/src/parser/parse-project.ts +++ b/src/parser/parse-project.ts @@ -2,13 +2,6 @@ * GuardLink — Project-level parser. * Walks a directory, parses all source files, and assembles a ThreatModel. * - * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Accepts user-provided root directory for file scanning" - * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Iterates all files in directory tree, may exhaust memory on large repos" - * @mitigates #parser against #path-traversal using #glob-filtering -- "fast-glob with explicit exclude patterns prevents access outside project" - * @mitigates #parser against #dos using #glob-filtering -- "Default excludes (node_modules, dist, .git) limit scope" - * @flows #cli -> #parser via parseProject -- "CLI passes user-specified root directory to parser" - * @flows #mcp -> #parser via parseProject -- "MCP server passes root param from AI agents" - * @handles internal on #parser -- "Processes source files to extract security annotation metadata" */ import fg from 'fast-glob'; diff --git a/src/report/report.ts b/src/report/report.ts index 9f0898a..a64a095 100644 --- a/src/report/report.ts +++ b/src/report/report.ts @@ -3,13 +3,6 @@ * Produces a human-readable threat model report with * embedded Mermaid diagram, finding tables, and coverage stats. * - * @exposes #report to #arbitrary-write [high] cwe:CWE-73 -- "Report written to user-specified output path" - * @exposes #report to #info-disclosure [low] cwe:CWE-200 -- "Report contains detailed threat model information" - * @accepts #info-disclosure on #report -- "Detailed threat model report is the intended output" - * @mitigates #report against #arbitrary-write using #path-validation -- "CLI resolves output path before passing to report generator" - * @flows #parser -> #report via ThreatModel -- "Report generator receives parsed threat model" - * @flows #report -> Filesystem via writeFile -- "Generated markdown written to disk by CLI" - * @handles internal on #report -- "Processes and formats security-sensitive threat model data" */ import type { ThreatModel, ThreatModelExposure, Severity } from '../types/index.js'; diff --git a/src/tui/index.ts b/src/tui/index.ts index ad6c893..54165c1 100644 --- a/src/tui/index.ts +++ b/src/tui/index.ts @@ -6,14 +6,6 @@ * Claude Code-style inline REPL: stays in your terminal, * slash commands + freeform AI chat, Ctrl+C to exit. * - * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "Accepts directory path as startup argument" - * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "User input passed to AI chat mode" - * @accepts #prompt-injection on #tui -- "Freeform AI chat is an intentional feature" - * @mitigates #tui against #path-traversal using #path-validation -- "resolve() normalizes directory path" - * @boundary between #tui and #llm-client (#tui-llm-boundary) -- "Freeform chat input crosses trust boundary to LLM" - * @flows User -> #tui via readline -- "Interactive command input from terminal" - * @flows #tui -> #parser via parseProject -- "Commands trigger parsing operations" - * @flows #tui -> #llm-client via cmdChat -- "Freeform text dispatched to AI provider" */ import { createInterface, type Interface } from 'node:readline'; From b0ed7285bc65eac8ad79c706f23cd1e919a760f4 Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 21:43:51 -0600 Subject: [PATCH 3/7] fix: parser skips annotations inside @shield blocks Shield blocks (@shield:begin/@shield:end) now properly exclude their content from the threat model. Previously, example annotations inside template literal strings in prompts.ts were parsed as real annotations, causing duplicate ID errors and dangling reference warnings. The shield markers themselves are still emitted (for counting), but all content between them is skipped during parsing. --- src/parser/parse-file.ts | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/parser/parse-file.ts b/src/parser/parse-file.ts index 82126b0..a87d343 100644 --- a/src/parser/parse-file.ts +++ b/src/parser/parse-file.ts @@ -2,6 +2,11 @@ * GuardLink — File-level parser. * Reads source files and extracts all GuardLink annotations. * + * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "File path from caller read via readFile; no validation here" + * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large files loaded entirely into memory" + * @audit #parser -- "Path validation delegated to callers (CLI/MCP validate root)" + * @flows FilePath -> #parser via readFile -- "Disk read path" + * @flows #parser -> Annotations via parseString -- "Parsed annotation output" */ import { readFile } from 'node:fs/promises'; @@ -28,6 +33,7 @@ export function parseString(content: string, filePath: string = ''): Pars const annotations: Annotation[] = []; const diagnostics: ParseDiagnostic[] = []; let lastAnnotation: Annotation | null = null; + let inShield = false; for (let i = 0; i < lines.length; i++) { const lineNum = i + 1; // 1-indexed @@ -40,6 +46,28 @@ export function parseString(content: string, filePath: string = ''): Pars continue; } + // Check for shield block boundaries — always parse these even inside shields + const trimmed = inner.trim(); + if (trimmed.startsWith('@shield:end')) { + const location = { file: filePath, line: lineNum }; + const result = parseLine(inner, location); + if (result.annotation) annotations.push(result.annotation); + inShield = false; + lastAnnotation = null; + continue; + } + if (trimmed.startsWith('@shield:begin')) { + const location = { file: filePath, line: lineNum }; + const result = parseLine(inner, location); + if (result.annotation) annotations.push(result.annotation); + inShield = true; + lastAnnotation = null; + continue; + } + + // Skip all content inside shield blocks — these are excluded from the model + if (inShield) continue; + // Check for continuation line: -- "..." const contMatch = inner.match(/^--\s*"((?:[^"\\]|\\.)*)"/); if (contMatch && lastAnnotation) { From fa661d1a053e5cb7e7f1e54e2b4a5ce38970c93b Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 21:44:03 -0600 Subject: [PATCH 4/7] feat: re-annotate codebase with updated template rules 24 source files annotated via Claude Code using updated templates that prohibit @accepts and require coupled annotation blocks. Results: 336 annotations, 64 exposures (46 mitigated, 15 open with @audit), 70 flows, 14 boundaries, 21 audits, 0 acceptances in source. Agent instruction files auto-synced with current threat model context. --- .clinerules | 82 ++++++++++++++++++--------------- .cursor/rules/guardlink.mdc | 77 ++++++++++++++++--------------- .gemini/GEMINI.md | 82 ++++++++++++++++++--------------- .github/copilot-instructions.md | 82 ++++++++++++++++++--------------- .windsurfrules | 82 ++++++++++++++++++--------------- AGENTS.md | 82 ++++++++++++++++++--------------- CLAUDE.md | 82 ++++++++++++++++++--------------- src/agents/config.ts | 11 +++++ src/agents/index.ts | 3 ++ src/agents/launcher.ts | 11 +++++ src/agents/prompts.ts | 9 ++++ src/analyze/index.ts | 10 ++++ src/analyze/llm.ts | 11 +++++ src/analyze/prompts.ts | 3 ++ src/analyze/tools.ts | 10 ++++ src/analyzer/index.ts | 3 ++ src/analyzer/sarif.ts | 5 ++ src/cli/index.ts | 12 +++++ src/dashboard/generate.ts | 8 ++++ src/dashboard/index.ts | 5 ++ src/diff/git.ts | 10 ++++ src/diff/index.ts | 4 ++ src/init/detect.ts | 5 ++ src/init/index.ts | 10 ++++ src/mcp/index.ts | 5 ++ src/mcp/lookup.ts | 5 ++ src/mcp/server.ts | 16 +++++++ src/mcp/suggest.ts | 8 ++++ src/parser/clear.ts | 8 ++++ src/parser/parse-line.ts | 3 ++ src/parser/parse-project.ts | 7 +++ src/report/index.ts | 3 ++ src/report/report.ts | 4 ++ src/tui/commands.ts | 16 +++++++ src/tui/config.ts | 6 +++ src/tui/index.ts | 8 ++++ src/tui/input.ts | 6 +++ 37 files changed, 535 insertions(+), 259 deletions(-) diff --git a/.clinerules b/.clinerules index c88f4cf..b56b508 100644 --- a/.clinerules +++ b/.clinerules @@ -37,51 +37,59 @@ Every time you write or modify code that touches security-relevant behavior, you ### 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows + + + + + diff --git a/.cursor/rules/guardlink.mdc b/.cursor/rules/guardlink.mdc index 4111110..49a9598 100644 --- a/.cursor/rules/guardlink.mdc +++ b/.cursor/rules/guardlink.mdc @@ -42,49 +42,52 @@ Every time you write or modify code that touches security-relevant behavior, you ### 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 3703305..6763160 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -51,55 +51,63 @@ 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows > **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 > and continue annotating new code using the same threat model vocabulary. + + + + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index b3885eb..94a7765 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -51,52 +51,55 @@ 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows > **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 @@ -104,3 +107,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + diff --git a/.windsurfrules b/.windsurfrules index c88f4cf..b56b508 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -37,51 +37,59 @@ Every time you write or modify code that touches security-relevant behavior, you ### 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows + + + + + diff --git a/AGENTS.md b/AGENTS.md index 3703305..6763160 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,55 +51,63 @@ 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows > **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 > and continue annotating new code using the same threat model vocabulary. + + + + + diff --git a/CLAUDE.md b/CLAUDE.md index b3885eb..94a7765 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,52 +51,55 @@ 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), #auth (Server,Auth) -**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], #sqli (SQL_Injection) [critical] -**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), #prepared-stmts (Prepared_Statements) +**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) +**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] +**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) ### Open Exposures (need @mitigates or @audit) -- #ai-endpoint exposed to #prompt-injection [high] (src/agents/prompts.ts:247) -- #sarif exposed to #info-disclosure [low] (src/analyzer/sarif.ts:15) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/index.ts:9) -- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:15) -- #cli exposed to #arbitrary-write [high] (src/cli/index.ts:24) -- #mcp exposed to #path-traversal [high] (src/mcp/server.ts:25) -- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:26) -- #mcp exposed to #arbitrary-write [high] (src/mcp/server.ts:27) -- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:28) -- #suggest exposed to #prompt-injection [medium] (src/mcp/suggest.ts:13) -- #report exposed to #info-disclosure [low] (src/report/report.ts:7) -- #tui exposed to #prompt-injection [medium] (src/tui/index.ts:10) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) +- #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) +- #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) +- #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) +- #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) +- #init exposed to #data-exposure [low] (src/init/index.ts:12) +- #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) +- #mcp exposed to #prompt-injection [medium] (src/mcp/server.ts:29) +- #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:33) +- #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) ### Existing Data Flows (extend, don't duplicate) -- #cli -> #agent-launcher via launchAgent -- #agent-launcher -> External_Process via spawnSync -- User_Input -> #auth-api via POST./login -- #auth-api -> #user-db via TypeORM.findOne -- User_Browser -> #api-gateway via HTTPS -- #api-gateway -> #auth-service via internal.gRPC -- #auth-service -> #user-db via pg.query -- #auth-service -> #session-store via redis.set -- #auth-service -> User_Browser via Set-Cookie -- req.body.username -> db.query via string-concat -- #parser -> #sarif via ThreatModel -- #sarif -> External_Security_Tools via SARIF_JSON -- #parser -> #llm-client via ThreatModel -- #llm-client -> Filesystem via writeFileSync -- #llm-client -> External_LLM_APIs via fetch -- External_LLM_APIs -> #llm-client via response -- External_LLM_APIs -> #llm-tools via tool_call -- #llm-tools -> External_LLM_APIs via tool_result -- User -> #cli via argv -- #cli -> #parser via parseProject -- ... and 20 more +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return +- EnvVars -> #agent-launcher via process.env +- ConfigFile -> #agent-launcher via readFileSync +- #agent-launcher -> ConfigFile via writeFileSync +- UserPrompt -> #agent-launcher via launchAgent +- #agent-launcher -> AgentProcess via spawn +- AgentProcess -> #agent-launcher via stdout +- UserPrompt -> #agent-launcher via buildAnnotatePrompt +- 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 +- 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 +- ... and 40 more ### Model Stats -193 annotations, 13 assets, 14 threats, 11 controls, 35 exposures, 21 mitigations, 40 flows +266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows > **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 @@ -104,3 +107,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + diff --git a/src/agents/config.ts b/src/agents/config.ts index 5900376..90c9161 100644 --- a/src/agents/config.ts +++ b/src/agents/config.ts @@ -9,6 +9,17 @@ * 5. Global config: ~/.config/guardlink/config.json * * Replaces the fragmented tui-config.json / CLI flag / env var resolution. + * + * @exposes #agent-launcher to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from env vars, files; stored in config.json" + * @mitigates #agent-launcher against #api-key-exposure using #key-redaction -- "maskKey() redacts keys for display; keys never logged" + * @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Config paths resolved from root and homedir" + * @mitigates #agent-launcher against #path-traversal using #path-validation -- "join() with known base dirs constrains paths" + * @exposes #agent-launcher to #arbitrary-write [medium] cwe:CWE-73 -- "saveProjectConfig writes to .guardlink/config.json" + * @mitigates #agent-launcher against #arbitrary-write using #path-validation -- "Output path is fixed relative to project root" + * @flows EnvVars -> #agent-launcher via process.env -- "Environment variable input" + * @flows ConfigFile -> #agent-launcher via readFileSync -- "Config file read" + * @flows #agent-launcher -> ConfigFile via writeFileSync -- "Config file write" + * @handles secrets on #agent-launcher -- "Processes and stores LLM API keys" */ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; diff --git a/src/agents/index.ts b/src/agents/index.ts index 47dacc6..a1bf5c5 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -3,6 +3,9 @@ * * Used by CLI, TUI, and MCP to identify and resolve coding agents * (Claude Code, Codex, Cursor, Windsurf, Gemini, clipboard). + * + * @comment -- "Agent binaries are hardcoded; no user-controlled binary names" + * @comment -- "parseAgentFlag extracts flags from args; no injection risk" */ // ─── Agent registry ────────────────────────────────────────────────── diff --git a/src/agents/launcher.ts b/src/agents/launcher.ts index 47ac799..76ce8de 100644 --- a/src/agents/launcher.ts +++ b/src/agents/launcher.ts @@ -7,6 +7,17 @@ * * Clipboard copy is always performed first regardless of agent type. * + * @exposes #agent-launcher to #child-proc-injection [critical] cwe:CWE-78 -- "spawn/spawnSync execute external binaries" + * @mitigates #agent-launcher against #child-proc-injection using #param-commands -- "Binary names from hardcoded AGENTS registry; no shell interpolation" + * @mitigates #agent-launcher against #cmd-injection using #param-commands -- "Arguments passed as array, not shell string" + * @exposes #agent-launcher to #prompt-injection [medium] cwe:CWE-77 -- "User prompt passed to agent CLI as argument" + * @audit #agent-launcher -- "Prompt content is opaque to agent binary; injection risk depends on agent implementation" + * @exposes #agent-launcher to #dos [low] cwe:CWE-400 -- "No timeout on foreground spawn; agent controls duration" + * @comment -- "Timeout intentionally omitted for interactive sessions; inline mode has implicit control" + * @flows UserPrompt -> #agent-launcher via launchAgent -- "Prompt input path" + * @flows #agent-launcher -> AgentProcess via spawn -- "Process spawn path" + * @flows AgentProcess -> #agent-launcher via stdout -- "Agent output capture" + * @boundary #agent-launcher and AgentProcess (#agent-boundary) -- "Trust boundary at process spawn" */ import { spawnSync, spawn } from 'node:child_process'; diff --git a/src/agents/prompts.ts b/src/agents/prompts.ts index 7fa155f..646b7fa 100644 --- a/src/agents/prompts.ts +++ b/src/agents/prompts.ts @@ -2,6 +2,15 @@ * GuardLink Agents — Prompt builders for annotation and analysis. * * Extracted from tui/commands.ts for shared use across CLI, TUI, MCP. + * + * @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text" + * @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" + * @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction 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" */ import { existsSync, readFileSync } from 'node:fs'; diff --git a/src/analyze/index.ts b/src/analyze/index.ts index 86f20db..22ca41e 100644 --- a/src/analyze/index.ts +++ b/src/analyze/index.ts @@ -5,6 +5,16 @@ * specific prompt, streams the response, and saves timestamped results * to .guardlink/threat-reports/. * + * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths" + * @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access" + * @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/" + * @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/" + * @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM" + * @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis" + * @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input" + * @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read" + * @flows #llm-client -> ReportFile via writeFileSync -- "Report output" + * @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets" */ import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs'; diff --git a/src/analyze/llm.ts b/src/analyze/llm.ts index a234d13..0cb361c 100644 --- a/src/analyze/llm.ts +++ b/src/analyze/llm.ts @@ -10,6 +10,17 @@ * * Zero dependencies — uses Node 20+ built-in fetch. * + * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "fetch() calls external LLM API endpoints" + * @mitigates #llm-client against #ssrf using #config-validation -- "BASE_URLS are hardcoded; baseUrl override is optional config" + * @exposes #llm-client to #api-key-exposure [high] cwe:CWE-798 -- "API keys passed in Authorization headers" + * @mitigates #llm-client against #api-key-exposure using #key-redaction -- "Keys never logged; passed directly to API" + * @exposes #llm-client to #prompt-injection [medium] cwe:CWE-77 -- "User prompts sent to LLM API" + * @audit #llm-client -- "Prompt injection mitigated by LLM provider safety; local code is read-only" + * @flows LLMConfig -> #llm-client via chatCompletion -- "Config and prompt input" + * @flows #llm-client -> LLMProvider via fetch -- "API request output" + * @flows LLMProvider -> #llm-client via response -- "API response input" + * @boundary #llm-client and LLMProvider (#llm-api-boundary) -- "Trust boundary at external API call" + * @handles secrets on #llm-client -- "Processes API keys for authentication" */ export type LLMProvider = 'anthropic' | 'openai' | 'google' | 'openrouter' | 'deepseek' | 'ollama'; diff --git a/src/analyze/prompts.ts b/src/analyze/prompts.ts index 5738180..e2a48fe 100644 --- a/src/analyze/prompts.ts +++ b/src/analyze/prompts.ts @@ -3,6 +3,9 @@ * * Each framework produces a structured security analysis from the * serialized threat model. The LLM acts as a senior security architect. + * + * @comment -- "Prompt templates are static; no user input interpolation in system prompts" + * @comment -- "customPrompt is appended to user message, not system prompt — bounded injection risk" */ export type AnalysisFramework = 'stride' | 'dread' | 'pasta' | 'attacker' | 'rapid' | 'general'; diff --git a/src/analyze/tools.ts b/src/analyze/tools.ts index 2583ec8..29fefbc 100644 --- a/src/analyze/tools.ts +++ b/src/analyze/tools.ts @@ -6,6 +6,16 @@ * - validate_finding: Cross-reference a finding against the parsed model * - search_codebase: Search project files for patterns * + * @exposes #llm-client to #ssrf [medium] cwe:CWE-918 -- "lookupCve fetches from NVD API with user-controlled CVE ID" + * @mitigates #llm-client against #ssrf using #input-sanitize -- "CVE ID validated with strict regex; URL hardcoded to NVD" + * @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "searchCodebase reads files from project root" + * @mitigates #llm-client against #path-traversal using #glob-filtering -- "skipDirs excludes sensitive directories; relative() bounds output" + * @exposes #llm-client to #dos [low] cwe:CWE-400 -- "searchCodebase reads many files; bounded by maxResults" + * @mitigates #llm-client against #dos using #resource-limits -- "maxResults caps output; stat.size < 500KB filter" + * @flows LLMToolCall -> #llm-client via createToolExecutor -- "Tool invocation input" + * @flows #llm-client -> NVD via fetch -- "CVE lookup API call" + * @flows ProjectFiles -> #llm-client via readFileSync -- "Codebase search reads" + * @boundary #llm-client and NVD (#nvd-api-boundary) -- "Trust boundary at external API" */ import { readFileSync, readdirSync, statSync } from 'node:fs'; diff --git a/src/analyzer/index.ts b/src/analyzer/index.ts index f490f93..8142636 100644 --- a/src/analyzer/index.ts +++ b/src/analyzer/index.ts @@ -1,5 +1,8 @@ /** * GuardLink Analyzer — exports. + * + * @comment -- "SARIF generation is pure transformation; no I/O in this module" + * @comment -- "File writes handled by CLI/MCP callers" */ export { generateSarif, type SarifOptions } from './sarif.js'; diff --git a/src/analyzer/sarif.ts b/src/analyzer/sarif.ts index c120a96..a172da6 100644 --- a/src/analyzer/sarif.ts +++ b/src/analyzer/sarif.ts @@ -12,6 +12,11 @@ * 2. Parse errors (annotation syntax problems) * 3. 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" + * @comment -- "Pure function: transforms ThreatModel to SARIF JSON; no I/O" + * @flows ThreatModel -> #sarif via generateSarif -- "Model input" + * @flows #sarif -> SarifLog via return -- "SARIF output" */ import type { ThreatModel, ThreatModelExposure, ParseDiagnostic, Severity } from '../types/index.js'; diff --git a/src/cli/index.ts b/src/cli/index.ts index f14e9e8..53a65ce 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -20,6 +20,18 @@ * guardlink tui [dir] Interactive TUI with slash commands + AI chat * guardlink gal Display GAL annotation language quick reference * + * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" + * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design" + * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths" + * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" + * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands" + * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output" + * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes" + * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args" + * @flows UserArgs -> #cli via process.argv -- "CLI argument input path" + * @flows #cli -> FileSystem via writeFile -- "Report/config output path" + * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing" + * @handles secrets on #cli -- "Processes API keys via config commands" */ import { Command } from 'commander'; diff --git a/src/dashboard/generate.ts b/src/dashboard/generate.ts index a8d06b8..0b0a612 100644 --- a/src/dashboard/generate.ts +++ b/src/dashboard/generate.ts @@ -5,6 +5,14 @@ * 7 pages: Summary, AI Analysis, Threats, Diagrams, Code, Data, Assets. * Mermaid.js via CDN for diagrams. Zero build step. * + * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Generates HTML with user-controlled threat model data" + * @mitigates #dashboard against #xss using #output-encoding -- "esc() HTML-encodes all interpolated values" + * @exposes #dashboard to #path-traversal [medium] cwe:CWE-22 -- "readFileSync reads code files for annotation context" + * @mitigates #dashboard against #path-traversal using #path-validation -- "resolve() with root constrains file access" + * @flows ThreatModel -> #dashboard via computeStats -- "Model statistics input" + * @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" */ import type { ThreatModel } from '../types/index.js'; diff --git a/src/dashboard/index.ts b/src/dashboard/index.ts index 858a852..37b6891 100644 --- a/src/dashboard/index.ts +++ b/src/dashboard/index.ts @@ -1,5 +1,10 @@ /** * GuardLink Dashboard — Self-contained HTML threat model dashboard. + * + * @exposes #dashboard to #xss [high] cwe:CWE-79 -- "Generates HTML with threat model data" + * @mitigates #dashboard against #xss using #output-encoding -- "esc() function encodes all interpolated values" + * @flows ThreatModel -> #dashboard via generateDashboardHTML -- "Model to HTML transformation" + * @comment -- "Self-contained HTML; no external data injection after generation" */ export { generateDashboardHTML } from './generate.js'; diff --git a/src/diff/git.ts b/src/diff/git.ts index baca960..a8b58ab 100644 --- a/src/diff/git.ts +++ b/src/diff/git.ts @@ -3,6 +3,16 @@ * Resolves git refs to threat models by checking out files at a given commit * and parsing them in a temp directory. * + * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "execSync runs git commands with ref argument" + * @mitigates #diff against #cmd-injection using #input-sanitize -- "rev-parse validates ref exists before use in other commands" + * @exposes #diff to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync creates files in temp directory" + * @mitigates #diff against #arbitrary-write using #path-validation -- "mkdtempSync creates isolated temp dir; rmSync cleans up" + * @exposes #diff to #path-traversal [medium] cwe:CWE-22 -- "git show extracts files based on ls-tree output" + * @mitigates #diff against #path-traversal using #glob-filtering -- "Files constrained to relevantFiles from git ls-tree" + * @flows GitRef -> #diff via execSync -- "Git command execution" + * @flows #diff -> TempDir via writeFileSync -- "Extracted file writes" + * @flows #diff -> ThreatModel via parseProject -- "Parsed model output" + * @boundary #diff and GitRepo (#git-boundary) -- "Trust boundary at git command execution" */ import { execSync } from 'node:child_process'; diff --git a/src/diff/index.ts b/src/diff/index.ts index 1c341e5..b2b3df0 100644 --- a/src/diff/index.ts +++ b/src/diff/index.ts @@ -1,5 +1,9 @@ /** * GuardLink Diff — exports. + * + * @exposes #diff to #cmd-injection [high] cwe:CWE-78 -- "git.ts uses execSync with ref argument" + * @audit #diff -- "Git commands use execSync; ref is validated with rev-parse before use" + * @flows GitRef -> #diff via parseAtRef -- "Git reference input" */ export { diffModels, type ThreatModelDiff, type DiffSummary, type Change, type ChangeKind } from './engine.js'; diff --git a/src/init/detect.ts b/src/init/detect.ts index e17b563..d52d0e8 100644 --- a/src/init/detect.ts +++ b/src/init/detect.ts @@ -1,6 +1,11 @@ /** * GuardLink init — Project detection utilities. * Detects language, project name, and existing agent instruction files. + * + * @exposes #init to #path-traversal [low] cwe:CWE-22 -- "Reads package.json, pyproject.toml, etc. from root" + * @mitigates #init against #path-traversal using #path-validation -- "join() with root constrains; reads well-known files only" + * @flows ProjectRoot -> #init via detectProject -- "Project detection input" + * @comment -- "Detection is read-only; no file writes" */ import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; diff --git a/src/init/index.ts b/src/init/index.ts index 46ed461..388a7fc 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -5,6 +5,16 @@ * directory with shared definitions, and injects GuardLink instructions * into agent instruction files (CLAUDE.md, .cursorrules, etc.). * + * @exposes #init to #arbitrary-write [high] cwe:CWE-73 -- "Creates/modifies files: .guardlink/, CLAUDE.md, .cursorrules, etc." + * @mitigates #init against #arbitrary-write using #path-validation -- "All paths are relative to root; join() constrains" + * @exposes #init to #path-traversal [medium] cwe:CWE-22 -- "Reads/writes files based on root argument" + * @mitigates #init against #path-traversal using #path-validation -- "join() with explicit root constrains file access" + * @exposes #init to #data-exposure [low] cwe:CWE-200 -- "Writes API key config to .guardlink/config.json" + * @audit #init -- "Config file may contain API keys; .gitignore entry added automatically" + * @flows ProjectRoot -> #init via options.root -- "Project root input" + * @flows #init -> AgentFiles via writeFileSync -- "Agent instruction file writes" + * @flows #init -> ConfigFile via writeFileSync -- "Config file write" + * @handles internal on #init -- "Generates definitions and agent instruction content" */ import { existsSync, readFileSync, mkdirSync, writeFileSync, appendFileSync } from 'node:fs'; diff --git a/src/mcp/index.ts b/src/mcp/index.ts index 827132a..f8d4dbe 100644 --- a/src/mcp/index.ts +++ b/src/mcp/index.ts @@ -1,5 +1,10 @@ /** * GuardLink MCP Server — exports and stdio entry point. + * + * @exposes #mcp to #cmd-injection [high] cwe:CWE-78 -- "Accepts tool calls from external MCP clients" + * @audit #mcp -- "All tool calls validated by server.ts before execution" + * @flows MCPClient -> #mcp via stdio -- "MCP protocol transport" + * @boundary #mcp and MCPClient (#mcp-boundary) -- "Trust boundary at MCP protocol" */ export { createServer } from './server.js'; diff --git a/src/mcp/lookup.ts b/src/mcp/lookup.ts index 8e8e798..554d99c 100644 --- a/src/mcp/lookup.ts +++ b/src/mcp/lookup.ts @@ -12,6 +12,11 @@ * - "unmitigated" → all unmitigated exposures * - "boundary #config" → boundaries involving asset * - Free text → fuzzy match across assets, threats, controls + * + * @exposes #mcp to #redos [low] cwe:CWE-1333 -- "Regex patterns applied to query strings" + * @mitigates #mcp against #redos using #regex-anchoring -- "Patterns are simple and bounded" + * @flows QueryString -> #mcp via lookup -- "Query input path" + * @comment -- "Pure function; no I/O; operates on in-memory ThreatModel" */ import type { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 4463a3f..6e4ca42 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -22,6 +22,22 @@ * * Transport: stdio (for Claude Code .mcp.json, Cursor, etc.) * + * @exposes #mcp to #path-traversal [high] cwe:CWE-22 -- "Tool arguments include 'root' directory path from external client" + * @mitigates #mcp against #path-traversal using #path-validation -- "Zod schema validates root; resolve() canonicalizes" + * @exposes #mcp to #arbitrary-write [high] cwe:CWE-73 -- "report, dashboard, sarif tools write files" + * @mitigates #mcp against #arbitrary-write using #path-validation -- "Output paths resolved relative to validated root" + * @exposes #mcp to #prompt-injection [medium] cwe:CWE-77 -- "annotate and threat_report tools pass user prompts to LLM" + * @audit #mcp -- "User prompts passed to LLM; model context is read-only" + * @exposes #mcp to #api-key-exposure [medium] cwe:CWE-798 -- "threat_report tool uses API keys from environment" + * @mitigates #mcp against #api-key-exposure using #key-redaction -- "Keys from env only; never logged or returned" + * @exposes #mcp to #data-exposure [medium] cwe:CWE-200 -- "Resources expose full threat model to MCP clients" + * @audit #mcp -- "Threat model data intentionally exposed to connected agents" + * @flows MCPClient -> #mcp via tool_call -- "Tool invocation input" + * @flows #mcp -> FileSystem via writeFile -- "Report/dashboard output" + * @flows #mcp -> #llm-client via generateThreatReport -- "LLM API call path" + * @flows #mcp -> MCPClient via resource -- "Threat model data output" + * @boundary #mcp and MCPClient (#mcp-tool-boundary) -- "Trust boundary at tool argument parsing" + * @handles internal on #mcp -- "Processes project annotations and threat model data" */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; diff --git a/src/mcp/suggest.ts b/src/mcp/suggest.ts index ddda7a0..c6d544c 100644 --- a/src/mcp/suggest.ts +++ b/src/mcp/suggest.ts @@ -9,6 +9,14 @@ * * Designed for both file-based and diff-based analysis (§8.2). * + * @exposes #suggest to #path-traversal [high] cwe:CWE-22 -- "File path from MCP client joined with root" + * @mitigates #suggest against #path-traversal using #path-validation -- "join() with validated root constrains access" + * @exposes #suggest to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to source code" + * @mitigates #suggest against #redos using #regex-anchoring -- "Patterns designed with bounded quantifiers" + * @exposes #suggest to #dos [low] cwe:CWE-400 -- "Large files loaded into memory for pattern scanning" + * @flows FilePath -> #suggest via readFileSync -- "File read path" + * @flows #suggest -> Suggestions via suggestAnnotations -- "Suggestion output" + * @comment -- "Skips node_modules and .guardlink directories" */ import { readFileSync, existsSync } from 'node:fs'; diff --git a/src/parser/clear.ts b/src/parser/clear.ts index 9d88f4c..416c22b 100644 --- a/src/parser/clear.ts +++ b/src/parser/clear.ts @@ -3,6 +3,14 @@ * Scans project source files and removes all GuardLink annotation comment lines. * * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. + * + * @exposes #parser to #arbitrary-write [high] cwe:CWE-73 -- "Writes modified content back to discovered files" + * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns determine which files are modified" + * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks sensitive dirs; cwd constrains scope" + * @audit #parser -- "Destructive operation requires explicit user confirmation via dryRun flag" + * @flows ProjectRoot -> #parser via fast-glob -- "File discovery path" + * @flows #parser -> SourceFiles via writeFile -- "Modified file write path" + * @handles internal on #parser -- "Operates on project source files only" */ import fg from 'fast-glob'; diff --git a/src/parser/parse-line.ts b/src/parser/parse-line.ts index e692d7b..c205c47 100644 --- a/src/parser/parse-line.ts +++ b/src/parser/parse-line.ts @@ -2,6 +2,9 @@ * GuardLink — Line-level annotation parser. * Parses a single comment line into a typed Annotation. * + * @exposes #parser to #redos [medium] cwe:CWE-1333 -- "Complex regex patterns applied to annotation text" + * @mitigates #parser against #redos using #regex-anchoring -- "All patterns are anchored (^...$) to prevent backtracking" + * @comment -- "Regex patterns designed with bounded quantifiers and explicit structure" */ import type { diff --git a/src/parser/parse-project.ts b/src/parser/parse-project.ts index 7db0658..8835768 100644 --- a/src/parser/parse-project.ts +++ b/src/parser/parse-project.ts @@ -2,6 +2,13 @@ * GuardLink — Project-level parser. * Walks a directory, parses all source files, and assembles a ThreatModel. * + * @exposes #parser to #path-traversal [high] cwe:CWE-22 -- "Glob patterns could escape root directory" + * @mitigates #parser against #path-traversal using #glob-filtering -- "DEFAULT_EXCLUDE blocks node_modules, .git; fast-glob cwd constrains scan" + * @exposes #parser to #dos [medium] cwe:CWE-400 -- "Large projects with many files could exhaust memory" + * @mitigates #parser against #dos using #resource-limits -- "DEFAULT_EXCLUDE skips build artifacts, tests; limits effective file count" + * @flows ProjectRoot -> #parser via fast-glob -- "Directory traversal path" + * @flows #parser -> ThreatModel via assembleModel -- "Aggregated threat model output" + * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" */ import fg from 'fast-glob'; diff --git a/src/report/index.ts b/src/report/index.ts index 9bea0a1..da559da 100644 --- a/src/report/index.ts +++ b/src/report/index.ts @@ -1,5 +1,8 @@ /** * GuardLink Report — exports. + * + * @comment -- "Report generation is pure transformation; no I/O in this module" + * @comment -- "File writes handled by CLI/MCP callers" */ export { generateMermaid } from './mermaid.js'; diff --git a/src/report/report.ts b/src/report/report.ts index a64a095..9aba246 100644 --- a/src/report/report.ts +++ b/src/report/report.ts @@ -3,6 +3,10 @@ * Produces a human-readable threat model report with * embedded Mermaid diagram, finding tables, and coverage stats. * + * @comment -- "Pure function: transforms ThreatModel to markdown string" + * @comment -- "No file I/O; caller (CLI/MCP) handles write" + * @flows ThreatModel -> #report via generateReport -- "Model input" + * @flows #report -> Markdown via return -- "Report output" */ import type { ThreatModel, ThreatModelExposure, Severity } from '../types/index.js'; diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 3c82dcc..3b9bd32 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -3,6 +3,22 @@ * * Each command function takes (args, ctx) and prints output directly. * Returns void. Throws on fatal errors. + * + * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o" + * @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access" + * @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files" + * @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root" + * @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes" + * @audit #tui -- "Child process spawning delegated to agents/launcher.ts" + * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage" + * @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output" + * @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM" + * @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only" + * @flows UserArgs -> #tui via args -- "Command argument input" + * @flows #tui -> FileSystem via writeFile -- "Report/config output" + * @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path" + * @flows #tui -> #llm-client via chatCompletion -- "LLM API call path" + * @handles secrets on #tui -- "Processes and stores API keys via /model" */ import { resolve, basename, isAbsolute } from 'node:path'; diff --git a/src/tui/config.ts b/src/tui/config.ts index 447999b..2fb30e3 100644 --- a/src/tui/config.ts +++ b/src/tui/config.ts @@ -3,6 +3,12 @@ * * Now delegates to the unified agents/config.ts resolution chain. * Keeps backward compatibility with tui-config.json (legacy). + * + * @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "API keys loaded from and saved to config files" + * @mitigates #tui against #api-key-exposure using #key-redaction -- "Delegates to agents/config.ts with masking" + * @flows ConfigFile -> #tui via loadProjectConfig -- "Config load path" + * @flows #tui -> ConfigFile via saveProjectConfig -- "Config save path" + * @handles secrets on #tui -- "API keys stored in .guardlink/config.json" */ import type { LLMConfig, LLMProvider } from '../analyze/llm.js'; diff --git a/src/tui/index.ts b/src/tui/index.ts index 54165c1..4094136 100644 --- a/src/tui/index.ts +++ b/src/tui/index.ts @@ -6,6 +6,14 @@ * Claude Code-style inline REPL: stays in your terminal, * slash commands + freeform AI chat, Ctrl+C to exit. * + * @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve" + * @mitigates #tui against #path-traversal using #path-validation -- "resolve() canonicalizes paths; starts from cwd" + * @exposes #tui to #api-key-exposure [medium] cwe:CWE-798 -- "API keys displayed in banner via resolveLLMConfig" + * @audit #tui -- "API keys masked via maskKey() in banner display" + * @flows UserInput -> #tui via readline -- "Interactive command input" + * @flows #tui -> Commands via dispatch -- "Command routing" + * @boundary #tui and UserInput (#tui-input-boundary) -- "Trust boundary at interactive input" + * @handles secrets on #tui -- "Displays LLM config including masked API keys" */ import { createInterface, type Interface } from 'node:readline'; diff --git a/src/tui/input.ts b/src/tui/input.ts index 03c8c3b..8a36cc8 100644 --- a/src/tui/input.ts +++ b/src/tui/input.ts @@ -11,6 +11,12 @@ * /assets Asset tree * * Uses raw stdin mode for full keystroke control. + * + * @exposes #tui to #dos [low] cwe:CWE-400 -- "Rapid keystrokes could consume CPU in raw mode" + * @mitigates #tui against #dos using #resource-limits -- "Keystroke buffer bounded by terminal width" + * @flows RawStdin -> #tui via process.stdin -- "Raw keystroke input" + * @flows #tui -> Terminal via process.stdout -- "ANSI escape sequence output" + * @comment -- "Raw mode enables full keystroke control for command palette" */ import chalk from 'chalk'; From fb11e3d6cc1feeb583cc462ecf73ef99cb0d07ac Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 22:27:24 -0600 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20guardlink=20review=20=E2=80=94=20in?= =?UTF-8?q?teractive=20governance=20workflow=20for=20unmitigated=20exposur?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New 'review' command across all surfaces: CLI: guardlink review [--list] [--severity critical,high] TUI: /review [severity] MCP: guardlink_review_list + guardlink_review_accept Users walk through unmitigated exposures sorted by severity and choose: accept — writes @accepts + @audit with timestamped governance trail remediate — writes @audit with planned-fix note skip — leaves open for next review Core module: src/review/index.ts - getReviewableExposures() — filters test fixtures, sorts by severity - applyReviewAction() — inserts annotations after coupled block - detectCommentStyle() — matches JSDoc, //, #, -- comment styles - Mandatory justification for accept and remediate (no rubber-stamping) - Auto-syncs agent files after annotations written MCP tools enforce human-in-the-loop: tool descriptions explicitly state acceptance decisions require human confirmation before calling. --- .clinerules | 13 +- .cursor/rules/guardlink.mdc | 10 +- .gemini/GEMINI.md | 13 +- .github/copilot-instructions.md | 13 +- .windsurfrules | 13 +- AGENTS.md | 13 +- CLAUDE.md | 13 +- src/cli/index.ts | 106 ++++++++++++ src/mcp/server.ts | 83 +++++++++ src/review/index.ts | 295 ++++++++++++++++++++++++++++++++ src/tui/commands.ts | 87 ++++++++++ src/tui/index.ts | 5 +- 12 files changed, 628 insertions(+), 36 deletions(-) create mode 100644 src/review/index.ts diff --git a/.clinerules b/.clinerules index b56b508..910bbfc 100644 --- a/.clinerules +++ b/.clinerules @@ -43,10 +43,10 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -61,8 +61,6 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -72,6 +70,8 @@ Every time you write or modify code that touches security-relevant behavior, you - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -81,11 +81,11 @@ Every time you write or modify code that touches security-relevant behavior, you - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows @@ -93,3 +93,6 @@ Every time you write or modify code that touches security-relevant behavior, you + + + diff --git a/.cursor/rules/guardlink.mdc b/.cursor/rules/guardlink.mdc index 49a9598..c1fa951 100644 --- a/.cursor/rules/guardlink.mdc +++ b/.cursor/rules/guardlink.mdc @@ -48,10 +48,10 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -66,8 +66,6 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -77,6 +75,8 @@ Every time you write or modify code that touches security-relevant behavior, you - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -86,8 +86,8 @@ Every time you write or modify code that touches security-relevant behavior, you - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 6763160..b5394e4 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -86,6 +84,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -95,11 +95,11 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows > **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 @@ -111,3 +111,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 94a7765..0eeab2b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -86,6 +84,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -95,11 +95,11 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows > **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 @@ -112,3 +112,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + diff --git a/.windsurfrules b/.windsurfrules index b56b508..910bbfc 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -43,10 +43,10 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -61,8 +61,6 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -72,6 +70,8 @@ Every time you write or modify code that touches security-relevant behavior, you - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -81,11 +81,11 @@ Every time you write or modify code that touches security-relevant behavior, you - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows @@ -93,3 +93,6 @@ Every time you write or modify code that touches security-relevant behavior, you + + + diff --git a/AGENTS.md b/AGENTS.md index 6763160..b5394e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -86,6 +84,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -95,11 +95,11 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows > **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 @@ -111,3 +111,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + diff --git a/CLAUDE.md b/CLAUDE.md index 94a7765..0eeab2b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -86,6 +84,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -95,11 +95,11 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync -- ... and 40 more +- ... and 42 more ### Model Stats -266 annotations, 12 assets, 13 threats, 10 controls, 59 exposures, 41 mitigations, 60 flows +272 annotations, 12 assets, 13 threats, 10 controls, 60 exposures, 42 mitigations, 62 flows > **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 @@ -112,3 +112,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + diff --git a/src/cli/index.ts b/src/cli/index.ts index 53a65ce..b3cbe5c 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -47,6 +47,7 @@ import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, import { generateDashboardHTML } from '../dashboard/index.js'; import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt } 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 type { ThreatModel, ParseDiagnostic } from '../types/index.js'; import gradient from 'gradient-string'; @@ -840,6 +841,111 @@ program printUnannotatedFiles(model); }); +// ─── review ────────────────────────────────────────────────────────── + +program + .command('review') + .description('Interactive governance review of unmitigated exposures — accept, remediate, or skip') + .argument('[dir]', 'Project directory to scan', '.') + .option('-p, --project ', 'Project name', 'unknown') + .option('--severity ', 'Filter by severity: critical,high,medium,low', undefined) + .option('--list', 'Just list reviewable exposures without prompting') + .action(async (dir: string, opts: { project: string; severity?: string; list?: boolean }) => { + const root = resolve(dir); + const { model } = await parseProject({ root, project: opts.project }); + let exposures = getReviewableExposures(model); + + // Filter by severity if requested + if (opts.severity) { + const allowed = new Set(opts.severity.split(',').map(s => s.trim().toLowerCase())); + exposures = exposures.filter(e => allowed.has(e.exposure.severity || 'low')); + // Re-index after filtering + exposures = exposures.map((e, i) => ({ ...e, index: i + 1 })); + } + + if (exposures.length === 0) { + console.error('✓ No unmitigated exposures to review.'); + return; + } + + // List-only mode + if (opts.list) { + console.error(`\n${exposures.length} unmitigated exposure(s):\n`); + for (const r of exposures) { + const e = r.exposure; + console.error(` ${r.index}. ${e.asset} → ${e.threat} [${e.severity || '?'}] (${e.location.file}:${e.location.line})`); + } + console.error(''); + return; + } + + // Interactive review + const { createInterface } = await import('node:readline'); + const rl = createInterface({ input: process.stdin, output: process.stderr }); + const ask = (q: string): Promise => + new Promise(resolve => rl.question(q, resolve)); + + console.error(`\n guardlink review — ${exposures.length} unmitigated exposure(s)\n`); + + const results: ReviewResult[] = []; + + for (const reviewable of exposures) { + console.error(formatExposureForReview(reviewable, exposures.length)); + console.error(''); + console.error(' (a) Accept — risk acknowledged and intentional'); + console.error(' (r) Remediate — mark as planned fix'); + console.error(' (s) Skip — leave open for now'); + console.error(' (q) Quit review'); + console.error(''); + + const choice = (await ask(' Choice [a/r/s/q]: ')).trim().toLowerCase(); + + if (choice === 'q') { + console.error('\n Review ended.\n'); + break; + } + + if (choice === 'a') { + let justification = ''; + while (!justification) { + justification = (await ask(' Justification (required): ')).trim(); + if (!justification) console.error(' ⚠ Justification is mandatory for acceptance.'); + } + const result = await applyReviewAction(root, reviewable, { decision: 'accept', justification }); + results.push(result); + console.error(` ✓ Accepted — ${result.linesInserted} line(s) written to ${reviewable.exposure.location.file}\n`); + } else if (choice === 'r') { + let note = ''; + while (!note) { + note = (await ask(' Remediation note (required): ')).trim(); + if (!note) console.error(' ⚠ Remediation note is mandatory.'); + } + const result = await applyReviewAction(root, reviewable, { decision: 'remediate', justification: note }); + results.push(result); + console.error(` ✓ Marked for remediation — ${result.linesInserted} line(s) written to ${reviewable.exposure.location.file}\n`); + } else { + results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0 }); + console.error(' — Skipped\n'); + } + } + + rl.close(); + + if (results.length > 0) { + console.error(summarizeReview(results)); + + // Auto-sync agent files if any annotations were written + if (results.some(r => r.linesInserted > 0)) { + try { + // Re-parse to get updated model + const { model: newModel } = await parseProject({ root, project: opts.project }); + const syncResult = syncAgentFiles({ root, model: newModel }); + if (syncResult.updated.length > 0) console.error(`↻ Synced ${syncResult.updated.length} agent instruction file(s)`); + } catch {} + } + } + }); + // ─── config ────────────────────────────────────────────────────────── program diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 6e4ca42..05745aa 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -44,6 +44,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { z } from 'zod'; import { parseProject, findDanglingRefs, findUnmitigatedExposures, clearAnnotations } from '../parser/index.js'; +import { getReviewableExposures, applyReviewAction, type ReviewableExposure } from '../review/index.js'; import { generateSarif } from '../analyzer/index.js'; import { generateReport } from '../report/index.js'; import { generateDashboardHTML } from '../dashboard/index.js'; @@ -508,6 +509,88 @@ export function createServer(): McpServer { }, ); + // ── Tool: guardlink_review_list ── + server.tool( + 'guardlink_review_list', + 'List all unmitigated exposures eligible for governance review, sorted by severity. Returns exposure IDs, details, and severity. Use guardlink_review_accept to record decisions. IMPORTANT: Acceptance decisions require explicit human confirmation — do not accept exposures without asking the user first.', + { + root: z.string().describe('Project root directory').default('.'), + severity: z.string().optional().describe('Filter by severity: "critical,high" etc.'), + }, + async ({ root, severity }) => { + invalidateCache(); + const { model } = await getModel(root); + let exposures = getReviewableExposures(model); + + if (severity) { + const allowed = new Set(severity.split(',').map((s: string) => s.trim().toLowerCase())); + exposures = exposures.filter(e => allowed.has(e.exposure.severity || 'low')); + exposures = exposures.map((e, i) => ({ ...e, index: i + 1 })); + } + + if (exposures.length === 0) { + return { content: [{ type: 'text', text: 'No unmitigated exposures to review.' }] }; + } + + const items = exposures.map(r => ({ + id: r.id, + index: r.index, + asset: r.exposure.asset, + threat: r.exposure.threat, + severity: r.exposure.severity, + file: r.exposure.location.file, + line: r.exposure.location.line, + description: r.exposure.description, + })); + + return { content: [{ type: 'text', text: JSON.stringify(items, null, 2) }] }; + }, + ); + + // ── Tool: guardlink_review_accept ── + server.tool( + 'guardlink_review_accept', + 'Record a governance decision for an unmitigated exposure. Writes @accepts + @audit (for accept) or @audit (for remediate) directly into the source file. IMPORTANT: This modifies source files. Only call after explicit human confirmation of the decision and justification.', + { + root: z.string().describe('Project root directory').default('.'), + exposure_id: z.string().describe('Exposure ID from guardlink_review_list (format: "file:line")'), + decision: z.enum(['accept', 'remediate', 'skip']).describe('accept = risk acknowledged; remediate = planned fix; skip = no action'), + justification: z.string().describe('Required explanation for accept/remediate decisions'), + }, + async ({ root, exposure_id, decision, justification }) => { + if (decision !== 'skip' && !justification.trim()) { + return { content: [{ type: 'text', text: 'Error: Justification is required for accept and remediate decisions.' }] }; + } + + invalidateCache(); + const { model } = await getModel(root); + const exposures = getReviewableExposures(model); + const target = exposures.find(e => e.id === exposure_id); + + if (!target) { + return { content: [{ type: 'text', text: `Error: Exposure "${exposure_id}" not found. Use guardlink_review_list to get valid IDs.` }] }; + } + + const result = await applyReviewAction(root, target, { decision, justification }); + invalidateCache(); + + if (decision === 'skip') { + return { content: [{ type: 'text', text: `Skipped: ${target.exposure.asset} → ${target.exposure.threat}` }] }; + } + + // Sync agent files after modification + try { + const { model: newModel } = await getModel(root); + syncAgentFiles({ root, model: newModel }); + } catch {} + + const verb = decision === 'accept' ? 'Accepted' : 'Marked for remediation'; + return { + content: [{ type: 'text', text: `${verb}: ${target.exposure.asset} → ${target.exposure.threat} [${target.exposure.severity}]\nJustification: ${justification}\n${result.linesInserted} annotation line(s) written to ${target.exposure.location.file}` }], + }; + }, + ); + // ── Resource: guardlink://model ── server.resource( 'threat-model', diff --git a/src/review/index.ts b/src/review/index.ts new file mode 100644 index 0000000..e84d3ef --- /dev/null +++ b/src/review/index.ts @@ -0,0 +1,295 @@ +/** + * GuardLink — Review module. + * + * Interactive governance workflow for unmitigated exposures. + * Users walk through the GAL (Governance Acceptance List) and decide: + * accept — write @accepts + @audit (risk acknowledged, intentional) + * remediate — write @audit with planned-fix note + * skip — leave open for now + * + * @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files" + * @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project" + * @audit #cli -- "Review decisions require human justification; no empty accepts allowed" + * @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input" + * @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output" + * @handles internal on #cli -- "Processes exposure metadata and user justification text" + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { stripCommentPrefix } from '../parser/comment-strip.js'; +import { findUnmitigatedExposures } from '../parser/validate.js'; +import type { ThreatModel, ThreatModelExposure, Severity } from '../types/index.js'; + +// ─── Types ────────────────────────────────────────────────────────── + +export type ReviewDecision = 'accept' | 'remediate' | 'skip'; + +export interface ReviewableExposure { + /** 1-based index in the review list */ + index: number; + exposure: ThreatModelExposure; + /** Stable ID for MCP: "file:line" */ + id: string; +} + +export interface ReviewAction { + decision: ReviewDecision; + justification: string; +} + +export interface ReviewResult { + exposure: ReviewableExposure; + action: ReviewAction; + /** Lines inserted into the file (empty for skip) */ + linesInserted: number; +} + +// ─── Severity ordering ────────────────────────────────────────────── + +const SEVERITY_ORDER: Record = { + critical: 0, high: 1, medium: 2, low: 3, +}; + +// ─── Core logic ───────────────────────────────────────────────────── + + +/** + * Get all unmitigated exposures eligible for review, sorted by severity. + * Excludes test fixtures and files outside the src/ tree. + */ +export function getReviewableExposures(model: ThreatModel): ReviewableExposure[] { + const unmitigated = findUnmitigatedExposures(model); + + // Filter out test fixtures and non-source files + const filtered = unmitigated.filter(e => { + const f = e.location.file; + return !f.startsWith('tests/') && !f.startsWith('test/') && !f.includes('__tests__/') && !f.includes('fixtures/'); + }); + + // Sort: critical → high → medium → low, then by file + filtered.sort((a, b) => { + const sa = SEVERITY_ORDER[a.severity || 'low'] ?? 3; + const sb = SEVERITY_ORDER[b.severity || 'low'] ?? 3; + if (sa !== sb) return sa - sb; + return a.location.file.localeCompare(b.location.file); + }); + + return filtered.map((exposure, i) => ({ + index: i + 1, + exposure, + id: `${exposure.location.file}:${exposure.location.line}`, + })); +} + +/** + * Format a severity tag with color hint for display. + */ +export function severityLabel(s?: Severity): string { + if (!s) return '[?]'; + return `[${s}]`; +} + +// ─── Comment style detection ──────────────────────────────────────── + +interface CommentStyle { + /** The prefix to use for new annotation lines */ + prefix: string; + /** Indentation (leading whitespace) to match */ + indent: string; +} + +/** + * Detect the comment style and indentation from the @exposes source line. + * Supports JSDoc ( * @...), single-line (// @...), and hash (# @...) styles. + */ +function detectCommentStyle(rawLine: string): CommentStyle { + const indent = rawLine.match(/^(\s*)/)?.[1] || ''; + const trimmed = rawLine.trimStart(); + + if (trimmed.startsWith('* @') || trimmed.startsWith('* @')) { + return { prefix: '* ', indent }; + } + if (trimmed.startsWith('// @')) { + return { prefix: '// ', indent }; + } + if (trimmed.startsWith('# @')) { + return { prefix: '# ', indent }; + } + if (trimmed.startsWith('-- @')) { + return { prefix: '-- ', indent }; + } + // Fallback: single-line JS style + return { prefix: '// ', indent }; +} + +/** + * Check if a source line is a GuardLink annotation (used to walk past coupled blocks). + */ +function isAnnotationLine(line: string): boolean { + const inner = stripCommentPrefix(line); + if (inner === null) return false; + const trimmed = inner.trim(); + // Annotation line: starts with @verb + if (trimmed.startsWith('@')) return true; + // Continuation line: -- "..." + if (/^--\s*"/.test(trimmed)) return true; + return false; +} + +/** + * Find the insertion point after the coupled annotation block that contains + * the @exposes line at `exposureLine` (1-indexed). + * + * Walks forward from the exposure line past consecutive annotation lines + * to find the end of the block, then returns the 0-indexed line to insert after. + */ +function findInsertionIndex(lines: string[], exposureLine: number): number { + // exposureLine is 1-indexed, convert to 0-indexed + let idx = exposureLine - 1; + + // Walk forward past consecutive annotation lines + while (idx + 1 < lines.length && isAnnotationLine(lines[idx + 1])) { + idx++; + } + + // Insert after the last annotation line in the block + return idx + 1; +} + +// ─── Annotation builders ──────────────────────────────────────────── + +function todayISO(): string { + return new Date().toISOString().slice(0, 10); +} + +/** + * Build the annotation lines to insert for an "accept" decision. + * Returns lines WITHOUT trailing newline. + */ +function buildAcceptLines(style: CommentStyle, exposure: ThreatModelExposure, justification: string): string[] { + const { prefix, indent } = style; + const date = todayISO(); + return [ + `${indent}${prefix}@accepts ${exposure.threat} on ${exposure.asset} -- "${escapeDesc(justification)}"`, + `${indent}${prefix}@audit ${exposure.asset} -- "Accepted via guardlink review on ${date}"`, + ]; +} + +/** + * Build the annotation line to insert for a "remediate" decision. + */ +function buildRemediateLines(style: CommentStyle, exposure: ThreatModelExposure, note: string): string[] { + const { prefix, indent } = style; + const date = todayISO(); + return [ + `${indent}${prefix}@audit ${exposure.asset} -- "Planned remediation: ${escapeDesc(note)} — flagged via guardlink review on ${date}"`, + ]; +} + +/** Escape double quotes in description strings */ +function escapeDesc(s: string): string { + return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +// ─── File modification ────────────────────────────────────────────── + +/** + * Insert annotation lines into a source file after the coupled block + * containing the given @exposes annotation. + * + * Returns the number of lines inserted. + */ +async function insertAnnotations( + root: string, + exposure: ThreatModelExposure, + newLines: string[], +): Promise { + const filePath = resolve(root, exposure.location.file); + const content = await readFile(filePath, 'utf-8'); + const lines = content.split('\n'); + + // Validate that the exposure line exists and looks right + const exposureIdx = exposure.location.line - 1; // 0-indexed + if (exposureIdx < 0 || exposureIdx >= lines.length) { + throw new Error(`Line ${exposure.location.line} out of range in ${exposure.location.file}`); + } + + const insertIdx = findInsertionIndex(lines, exposure.location.line); + + // Splice in the new lines + lines.splice(insertIdx, 0, ...newLines); + + await writeFile(filePath, lines.join('\n')); + return newLines.length; +} + +// ─── Public API ───────────────────────────────────────────────────── + +/** + * Apply a review decision to an exposure. + * For 'accept': inserts @accepts + @audit after the coupled block. + * For 'remediate': inserts @audit with planned-fix note. + * For 'skip': does nothing. + * + * Returns the result including lines inserted. + */ +export async function applyReviewAction( + root: string, + reviewable: ReviewableExposure, + action: ReviewAction, +): Promise { + if (action.decision === 'skip') { + return { exposure: reviewable, action, linesInserted: 0 }; + } + + const { exposure } = reviewable; + const filePath = resolve(root, exposure.location.file); + const content = await readFile(filePath, 'utf-8'); + const lines = content.split('\n'); + + // Detect comment style from the @exposes line + const exposureIdx = exposure.location.line - 1; + const style = detectCommentStyle(lines[exposureIdx]); + + let newLines: string[]; + if (action.decision === 'accept') { + newLines = buildAcceptLines(style, exposure, action.justification); + } else { + newLines = buildRemediateLines(style, exposure, action.justification); + } + + const linesInserted = await insertAnnotations(root, exposure, newLines); + return { exposure: reviewable, action, linesInserted }; +} + +/** + * Format an exposure for display in CLI/TUI review UI. + */ +export function formatExposureForReview(r: ReviewableExposure, total: number): string { + const e = r.exposure; + const sev = e.severity || 'unknown'; + const desc = e.description || '(no description)'; + return [ + `[${r.index}/${total}] ${e.asset} → ${e.threat} [${sev}]`, + ` File: ${e.location.file}:${e.location.line}`, + ` Exposure: "${desc}"`, + ].join('\n'); +} + +/** + * Summarize review session results. + */ +export function summarizeReview(results: ReviewResult[]): string { + const accepted = results.filter(r => r.action.decision === 'accept').length; + const remediated = results.filter(r => r.action.decision === 'remediate').length; + const skipped = results.filter(r => r.action.decision === 'skip').length; + const totalLines = results.reduce((sum, r) => sum + r.linesInserted, 0); + + const parts: string[] = []; + if (accepted > 0) parts.push(`${accepted} accepted`); + if (remediated > 0) parts.push(`${remediated} marked for remediation`); + if (skipped > 0) parts.push(`${skipped} skipped`); + + return `Review complete: ${parts.join(', ')}. ${totalLines} annotation line(s) written.`; +} diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 3b9bd32..e63343f 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -36,6 +36,7 @@ import { C, severityBadge, severityText, severityTextPad, severityOrder, compute import { resolveLLMConfig, saveTuiConfig, loadTuiConfig } from './config.js'; import { AGENTS, parseAgentFlag, launchAgent, launchAgentInline, copyToClipboard, buildAnnotatePrompt, type AgentEntry } from '../agents/index.js'; import { describeConfigSource } from '../agents/config.js'; +import { getReviewableExposures, applyReviewAction, formatExposureForReview, summarizeReview, type ReviewResult } from '../review/index.js'; // ─── Shared context ────────────────────────────────────────────────── @@ -114,6 +115,7 @@ export function cmdHelp(): void { ['/model', 'Set AI provider (API or CLI agent: Claude Code, Codex, Gemini)'], ['/clear', 'Remove all annotations from source files (start fresh)'], ['/sync', 'Sync agent instruction files with current threat model'], + ['/review [severity]', 'Interactive governance review of unmitigated exposures'], ['(freeform text)', 'Chat about your threat model with AI'], ['', ''], ['/report', 'Generate markdown + JSON report'], @@ -1698,6 +1700,91 @@ export function cmdUnannotated(ctx: TuiContext): void { console.log(''); } +// ─── /review ───────────────────────────────────────────────────────── + +export async function cmdReview(args: string, ctx: TuiContext): Promise { + if (!ctx.model) { + console.log(C.warn(' No threat model. Run /parse first.')); + return; + } + + let exposures = getReviewableExposures(ctx.model); + + // Parse severity filter from args (e.g., "/review critical,high") + if (args) { + const allowed = new Set(args.split(',').map(s => s.trim().toLowerCase())); + exposures = exposures.filter(e => allowed.has(e.exposure.severity || 'low')); + exposures = exposures.map((e, i) => ({ ...e, index: i + 1 })); + } + + if (exposures.length === 0) { + console.log(`\n ${C.success('✓')} No unmitigated exposures to review.\n`); + return; + } + + console.log(`\n ${C.bold('guardlink review')} — ${exposures.length} unmitigated exposure(s)\n`); + + const results: ReviewResult[] = []; + + for (const reviewable of exposures) { + const e = reviewable.exposure; + const sev = severityText(e.severity || 'low'); + console.log(` ${C.bold(`[${reviewable.index}/${exposures.length}]`)} ${e.asset} → ${e.threat} ${sev}`); + console.log(` File: ${fileLink(e.location.file, e.location.line)}`); + console.log(` Exposure: ${C.dim('"' + (e.description || 'no description') + '"')}`); + console.log(''); + console.log(` ${C.bold('a')} Accept ${C.dim('— risk acknowledged and intentional')}`); + console.log(` ${C.bold('r')} Remediate ${C.dim('— mark as planned fix')}`); + console.log(` ${C.bold('s')} Skip ${C.dim('— leave open for now')}`); + console.log(` ${C.bold('q')} Quit`); + console.log(''); + + const choice = (await ask(ctx, ' Choice [a/r/s/q]: ')).toLowerCase(); + + if (choice === 'q') { + console.log(`\n ${C.dim('Review ended.')}\n`); + break; + } + + if (choice === 'a') { + let justification = ''; + while (!justification) { + justification = await ask(ctx, ' Justification (required): '); + if (!justification) console.log(C.warn(' ⚠ Justification is mandatory for acceptance.')); + } + const result = await applyReviewAction(ctx.root, reviewable, { decision: 'accept', justification }); + results.push(result); + console.log(` ${C.success('✓')} Accepted — ${result.linesInserted} line(s) written\n`); + } else if (choice === 'r') { + let note = ''; + while (!note) { + note = await ask(ctx, ' Remediation note (required): '); + if (!note) console.log(C.warn(' ⚠ Remediation note is mandatory.')); + } + const result = await applyReviewAction(ctx.root, reviewable, { decision: 'remediate', justification: note }); + results.push(result); + console.log(` ${C.success('✓')} Marked for remediation — ${result.linesInserted} line(s) written\n`); + } else { + results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0 }); + console.log(` ${C.dim('— Skipped')}\n`); + } + } + + if (results.length > 0) { + console.log(`\n ${summarizeReview(results)}`); + + // Re-parse and sync if annotations were written + if (results.some(r => r.linesInserted > 0)) { + await refreshModel(ctx); + try { + const syncResult = syncAgentFiles({ root: ctx.root, model: ctx.model }); + if (syncResult.updated.length > 0) console.log(` ${C.dim('↻ Synced')} ${syncResult.updated.length} agent instruction file(s)`); + } catch {} + } + } + console.log(''); +} + // ─── /report ───────────────────────────────────────────────────────── export async function cmdReport(ctx: TuiContext): Promise { diff --git a/src/tui/index.ts b/src/tui/index.ts index 4094136..012adc2 100644 --- a/src/tui/index.ts +++ b/src/tui/index.ts @@ -49,6 +49,7 @@ import { cmdClear, cmdSync, cmdUnannotated, + cmdReview, cmdReport, cmdDashboard, cmdGal, @@ -62,7 +63,7 @@ const COMMANDS = [ '/exposures', '/show', '/scan', '/assets', '/files', '/view', '/threat-report', '/threat-reports', '/annotate', '/model', - '/clear', '/sync', '/unannotated', + '/clear', '/sync', '/unannotated', '/review', '/report', '/dashboard', '/quit', ]; @@ -96,6 +97,7 @@ const PALETTE_COMMANDS: CommandEntry[] = [ { command: '/clear', label: 'Remove all annotations from source files' }, { command: '/sync', label: 'Sync agent instructions with current threat model' }, { command: '/unannotated', label: 'List source files with no annotations' }, + { command: '/review', label: 'Review unmitigated exposures — accept, remediate, or skip' }, { command: '/report', label: 'Generate markdown report' }, { command: '/dashboard', label: 'HTML dashboard' }, { command: '/diff', label: 'Compare vs git ref' }, @@ -369,6 +371,7 @@ async function dispatch(input: string, ctx: TuiContext): Promise { case '/clear': await cmdClear(args, ctx); break; case '/sync': await cmdSync(ctx); break; case '/unannotated': cmdUnannotated(ctx); break; + case '/review': await cmdReview(args, ctx); break; case '/report': await cmdReport(ctx); break; case '/dashboard': await cmdDashboard(ctx); break; default: From 72a80ae8ee5a2cff05c88b55a65c2b787991f19f Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 22:37:47 -0600 Subject: [PATCH 6/7] chore: sync agent instruction files with current threat model --- .clinerules | 7 ++++--- .cursor/rules/guardlink.mdc | 6 +++--- .gemini/GEMINI.md | 7 ++++--- .github/copilot-instructions.md | 7 ++++--- .windsurfrules | 7 ++++--- AGENTS.md | 7 ++++--- CLAUDE.md | 7 ++++--- 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/.clinerules b/.clinerules index 910bbfc..8db5463 100644 --- a/.clinerules +++ b/.clinerules @@ -43,10 +43,10 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -61,6 +61,8 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -70,8 +72,6 @@ Every time you write or modify code that touches security-relevant behavior, you - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -96,3 +96,4 @@ Every time you write or modify code that touches security-relevant behavior, you + diff --git a/.cursor/rules/guardlink.mdc b/.cursor/rules/guardlink.mdc index c1fa951..760d64e 100644 --- a/.cursor/rules/guardlink.mdc +++ b/.cursor/rules/guardlink.mdc @@ -48,10 +48,10 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -66,6 +66,8 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -75,8 +77,6 @@ Every time you write or modify code that touches security-relevant behavior, you - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index b5394e4..490f906 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,6 +75,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -84,8 +86,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -114,3 +114,4 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 0eeab2b..fcc1ad2 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,6 +75,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -84,8 +86,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -115,3 +115,4 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + diff --git a/.windsurfrules b/.windsurfrules index 910bbfc..8db5463 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -43,10 +43,10 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -61,6 +61,8 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -70,8 +72,6 @@ Every time you write or modify code that touches security-relevant behavior, you - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -96,3 +96,4 @@ Every time you write or modify code that touches security-relevant behavior, you + diff --git a/AGENTS.md b/AGENTS.md index b5394e4..490f906 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,6 +75,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -84,8 +86,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -114,3 +114,4 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + diff --git a/CLAUDE.md b/CLAUDE.md index 0eeab2b..fcc1ad2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,10 +57,10 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #llm-client exposed to #data-exposure [low] (src/analyze/index.ts:12) - #llm-client exposed to #prompt-injection [medium] (src/analyze/llm.ts:17) - #cli exposed to #cmd-injection [critical] (src/cli/index.ts:29) @@ -75,6 +75,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -84,8 +86,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - UserPrompt -> #agent-launcher via buildAnnotatePrompt - ThreatModel -> #agent-launcher via model - #agent-launcher -> AgentPrompt via return -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - ThreatModel -> #llm-client via serializeModel - ProjectFiles -> #llm-client via readFileSync - #llm-client -> ReportFile via writeFileSync @@ -115,3 +115,4 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + From 695f49d190b05c91ae1b1275a80352d0f0e682a4 Mon Sep 17 00:00:00 2001 From: Animesh Srivastava Date: Thu, 26 Feb 2026 22:50:26 -0600 Subject: [PATCH 7/7] =?UTF-8?q?chore:=20bump=20to=20v1.3.0=20=E2=80=94=20c?= =?UTF-8?q?hangelog,=20readme,=20docs,=20package=20version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - package.json / package-lock.json → 1.3.0 - MCP server version → 1.3.0 - CHANGELOG.md: full v1.3.0 entry (review, clear, sync, unannotated, shield fix) - README.md: added review/clear/sync/unannotated to command table, updated annotation count - GUARDLINK_REFERENCE.md: added Governance & Maintenance section - Agent instruction files synced with updated threat model --- .clinerules | 8 +++++--- .cursor/rules/guardlink.mdc | 6 +++--- .gemini/GEMINI.md | 8 +++++--- .github/copilot-instructions.md | 8 +++++--- .windsurfrules | 8 +++++--- AGENTS.md | 8 +++++--- CHANGELOG.md | 25 +++++++++++++++++++++++++ CLAUDE.md | 8 +++++--- README.md | 7 ++++++- docs/GUARDLINK_REFERENCE.md | 7 +++++++ package-lock.json | 4 ++-- package.json | 2 +- src/mcp/server.ts | 2 +- 13 files changed, 75 insertions(+), 26 deletions(-) diff --git a/.clinerules b/.clinerules index 8db5463..74b1765 100644 --- a/.clinerules +++ b/.clinerules @@ -43,12 +43,12 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -61,8 +61,6 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -81,6 +79,8 @@ Every time you write or modify code that touches security-relevant behavior, you - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats @@ -97,3 +97,5 @@ Every time you write or modify code that touches security-relevant behavior, you + + diff --git a/.cursor/rules/guardlink.mdc b/.cursor/rules/guardlink.mdc index 760d64e..d26d558 100644 --- a/.cursor/rules/guardlink.mdc +++ b/.cursor/rules/guardlink.mdc @@ -48,12 +48,12 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -66,8 +66,6 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -86,6 +84,8 @@ Every time you write or modify code that touches security-relevant behavior, you - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index 490f906..0d96456 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -57,12 +57,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -95,6 +93,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats @@ -115,3 +115,5 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index fcc1ad2..039528f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -57,12 +57,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -95,6 +93,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats @@ -116,3 +116,5 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + diff --git a/.windsurfrules b/.windsurfrules index 8db5463..74b1765 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -43,12 +43,12 @@ Every time you write or modify code that touches security-relevant behavior, you ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -61,8 +61,6 @@ Every time you write or modify code that touches security-relevant behavior, you ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -81,6 +79,8 @@ Every time you write or modify code that touches security-relevant behavior, you - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats @@ -97,3 +97,5 @@ Every time you write or modify code that touches security-relevant behavior, you + + diff --git a/AGENTS.md b/AGENTS.md index 490f906..0d96456 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,12 +57,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -95,6 +93,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats @@ -115,3 +115,5 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + diff --git a/CHANGELOG.md b/CHANGELOG.md index a231797..1f9bcd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ 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.3.0] — 2026-02-27 + +### Added + +- **Review**: `guardlink review` — interactive governance workflow for unmitigated exposures across CLI, TUI (`/review`), and MCP (`guardlink_review_list` + `guardlink_review_accept`). Users walk through exposures sorted by severity and choose: accept (writes `@accepts` + `@audit`), remediate (writes `@audit` with planned-fix note), or skip. Mandatory justification prevents rubber-stamping; timestamped audit trail for compliance. +- **CLI**: `guardlink clear` — remove all annotations from source files to start fresh, with `--dry-run` preview and `--include-definitions` option +- **CLI**: `guardlink unannotated` — list source files with no annotations, showing coverage ratio +- **CLI**: `guardlink sync` — standalone command to sync agent instruction files with current threat model (previously only available via MCP/TUI) +- **TUI**: `/review`, `/clear`, `/sync`, `/unannotated` commands +- **MCP**: `guardlink_review_list`, `guardlink_review_accept`, `guardlink_unannotated`, `guardlink_clear`, `guardlink_sync` tools +- **Dashboard**: File Coverage section on Code & Annotations page with progress bar and collapsible unannotated file list +- **Parser**: `annotated_files` and `unannotated_files` fields added to ThreatModel +- **Templates**: Sync guidance in workflow section for all 7 agent instruction formats +- **Templates**: Tightened negative guardrail — agents prohibited from writing `@accepts` (human-only via `guardlink review`) +- **Auto-sync**: `status` and `validate` commands now auto-sync agent instruction files after parsing + +### Fixed + +- **Parser**: `@shield:begin`/`@shield:end` blocks now properly exclude content from the threat model. Previously, example annotations inside shielded blocks were parsed as real annotations, causing duplicate ID errors and dangling reference warnings. +- **Init**: Picker "All of the above" now uses a numbered option instead of `a` shortcut for consistency + +### Changed + +- **MCP**: Server version bumped to 1.3.0 + ## [1.2.0] — 2026-02-22 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index fcc1ad2..039528f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,12 +57,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Open Exposures (need @mitigates or @audit) -- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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) - #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:29) - #init exposed to #data-exposure [low] (src/init/index.ts:12) - #mcp exposed to #cmd-injection [high] (src/mcp/index.ts:4) @@ -75,8 +75,6 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c ### Existing Data Flows (extend, don't duplicate) -- ThreatModel -> #sarif via generateSarif -- #sarif -> SarifLog via return - EnvVars -> #agent-launcher via process.env - ConfigFile -> #agent-launcher via readFileSync - #agent-launcher -> ConfigFile via writeFileSync @@ -95,6 +93,8 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - LLMToolCall -> #llm-client via createToolExecutor - #llm-client -> NVD via fetch - ProjectFiles -> #llm-client via readFileSync +- ThreatModel -> #sarif via generateSarif +- #sarif -> SarifLog via return - ... and 42 more ### Model Stats @@ -116,3 +116,5 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + diff --git a/README.md b/README.md index 6d41d88..8291d03 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ **Security annotations that live in your code. Your threat model updates when your code changes.** -> **This repository is secured by GuardLink.** Run `guardlink status .` to see 152 annotations across 12 assets, 13 threats, and 10 controls — maintained by AI agents, validated in CI. +> **This repository is secured by GuardLink.** Run `guardlink status .` to see 272 annotations across 12 assets, 13 threats, and 10 controls — maintained by AI agents, validated in CI. ```javascript // @asset PaymentService (#payments) -- "Handles card transactions" @@ -174,6 +174,11 @@ GuardLink ships an MCP server and behavioral directives for AI coding agents. Af | `guardlink threat-report [fw]` | AI threat report (stride/dread/pasta/attacker/rapid/general) | | `guardlink threat-reports` | List saved AI threat reports | | `guardlink annotate [prompt]` | Launch a coding agent to add annotations | +| `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) | +| `guardlink sync [dir]` | Sync agent instruction files with current threat model | +| `guardlink unannotated [dir]` | List source files with no annotations | | `guardlink config` | Set AI provider and API key | | `guardlink mcp` | Start MCP server for AI agent integration | diff --git a/docs/GUARDLINK_REFERENCE.md b/docs/GUARDLINK_REFERENCE.md index 7f01742..5d059c9 100644 --- a/docs/GUARDLINK_REFERENCE.md +++ b/docs/GUARDLINK_REFERENCE.md @@ -82,6 +82,13 @@ guardlink threat-reports # List saved threat reports guardlink annotate # Launch coding agent to add annotations guardlink config # Manage LLM provider / CLI agent configuration +# Governance & Maintenance +guardlink review [dir] # Interactive review of unmitigated exposures (accept/remediate/skip) +guardlink review --list [--severity X] # List reviewable exposures without prompting +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 + # Interactive guardlink tui [dir] # Interactive TUI: slash commands + AI chat guardlink mcp # Start MCP server (stdio) for Claude Code, Cursor, etc. diff --git a/package-lock.json b/package-lock.json index 8dd85db..1598cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "guardlink", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "guardlink", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", diff --git a/package.json b/package.json index 4d76f6f..a63cc36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "guardlink", - "version": "1.2.0", + "version": "1.3.0", "description": "GuardLink — Security annotations for code. Threat modeling that lives in your codebase.", "type": "module", "bin": { diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 05745aa..fc826b3 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -83,7 +83,7 @@ function invalidateCache() { export function createServer(): McpServer { const server = new McpServer({ name: 'guardlink', - version: '1.1.0', + version: '1.3.0', }); // ── Tool: guardlink_parse ──