From 0253fa9f1d041a20c98b170706710a342565abc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jordi=20Murg=C3=B3?= <152895006+jordi-murgo@users.noreply.github.com> Date: Thu, 23 Apr 2026 06:30:32 +0200 Subject: [PATCH] feat: externalized annotations (external mode) + --stdout flag (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: externalized annotations support — parser, review, types, tests - Parser: support .gal files with @source blocks, comment-strip improvements, parse-file handles external annotation files, parse-project scans .guardlink/annotations/ - Review: improved exposure formatting and reviewable exposure types - Types: new fields for externalized annotation tracking - Diff: git diff integration improvements - Templates: updated agent instruction content for external mode - Tests: parser, diff, review test suites - Docs: SPEC and GUARDLINK_REFERENCE updated for external mode - Agent files: sync with updated threat model context - Bump package version * feat: inline/external annotation mode, --stdout flag for annotate - Rename annotation mode 'gal' → 'external' (clean break, no alias) - guardlink init --mode external: restricts all writes to .guardlink/ (no agent files, no root .mcp.json, no docs/; reference doc and mcp config template placed inside .guardlink/ instead) - guardlink annotate --stdout: prints prompt to stdout for piping, skips clipboard and all console output - MCP guardlink_annotate: mode enum updated to inline|external - TUI /annotate: help text updated - Tests: agents and prompts test suites covering new mode values --------- Co-authored-by: jpmo --- .clinerules | 20 +- .cursor/rules/guardlink.mdc | 14 +- .gemini/GEMINI.md | 20 +- .github/copilot-instructions.md | 20 +- .windsurfrules | 20 +- AGENTS.md | 20 +- CLAUDE.md | 20 +- README.md | 19 +- bun.lock | 470 ++++++++++++++++++++++++++++++++ docs/GUARDLINK_REFERENCE.md | 19 +- docs/SPEC.md | 22 +- package-lock.json | 4 +- package.json | 2 +- src/agents/index.ts | 39 +++ src/agents/launcher.ts | 6 + src/agents/prompts.ts | 82 +++++- src/analyze/prompts.ts | 2 +- src/cli/index.ts | 45 ++- src/diff/git.ts | 6 +- src/init/index.ts | 83 ++++-- src/init/templates.ts | 10 +- src/mcp/server.ts | 21 +- src/parser/clear.ts | 51 ++-- src/parser/comment-strip.ts | 10 + src/parser/parse-file.ts | 43 ++- src/parser/parse-line.ts | 39 ++- src/parser/parse-project.ts | 18 +- src/review/index.ts | 120 +++++--- src/tui/commands.ts | 23 +- src/types/index.ts | 2 + tests/agents.test.ts | 27 ++ tests/diff.test.ts | 42 +++ tests/parser.test.ts | 131 +++++++++ tests/prompts.test.ts | 34 +++ tests/review.test.ts | 241 ++++++++++++++++ 35 files changed, 1526 insertions(+), 219 deletions(-) create mode 100644 bun.lock create mode 100644 tests/agents.test.ts create mode 100644 tests/diff.test.ts create mode 100644 tests/prompts.test.ts create mode 100644 tests/review.test.ts diff --git a/.clinerules b/.clinerules index 8d40c54..56a3031 100644 --- a/.clinerules +++ b/.clinerules @@ -1,7 +1,7 @@ # GuardLink Security Annotations -This project uses GuardLink annotations in source code comments. +This project uses GuardLink annotations in source code comments or standalone .gal files. ## 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. @@ -11,7 +11,7 @@ Every time you write or modify code that touches security-relevant behavior, you - 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. +- Relationship annotations use 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. @@ -48,14 +48,14 @@ Every time you write or modify code that touches security-relevant behavior, you - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -79,13 +79,13 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows @@ -102,6 +102,12 @@ 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 aacb2c4..7c98ccf 100644 --- a/.cursor/rules/guardlink.mdc +++ b/.cursor/rules/guardlink.mdc @@ -6,7 +6,7 @@ alwaysApply: true # GuardLink Security Annotations -This project uses GuardLink annotations in source code comments. +This project uses GuardLink annotations in source code comments or standalone .gal files. ## 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. @@ -16,7 +16,7 @@ Every time you write or modify code that touches security-relevant behavior, you - 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. +- Relationship annotations use 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. @@ -53,14 +53,14 @@ Every time you write or modify code that touches security-relevant behavior, you - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -84,10 +84,10 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows diff --git a/.gemini/GEMINI.md b/.gemini/GEMINI.md index bf121a4..edc93ce 100644 --- a/.gemini/GEMINI.md +++ b/.gemini/GEMINI.md @@ -3,7 +3,7 @@ ## GuardLink — Security Annotations (Required) -This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments or standalone `.gal` files. **Full reference: `docs/GUARDLINK_REFERENCE.md`** ### Core Requirement @@ -16,7 +16,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 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`. +5. Relationship annotations use verbs like: `@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. @@ -62,14 +62,14 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -93,13 +93,13 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 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 @@ -120,6 +120,12 @@ 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 68c3a9f..6332e83 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -3,7 +3,7 @@ ## GuardLink — Security Annotations (Required) -This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments or standalone `.gal` files. **Full reference: `docs/GUARDLINK_REFERENCE.md`** ### Core Requirement @@ -16,7 +16,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 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`. +5. Relationship annotations use verbs like: `@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. @@ -62,14 +62,14 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -93,13 +93,13 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 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 @@ -120,6 +120,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + + diff --git a/.windsurfrules b/.windsurfrules index 8d40c54..56a3031 100644 --- a/.windsurfrules +++ b/.windsurfrules @@ -1,7 +1,7 @@ # GuardLink Security Annotations -This project uses GuardLink annotations in source code comments. +This project uses GuardLink annotations in source code comments or standalone .gal files. ## 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. @@ -11,7 +11,7 @@ Every time you write or modify code that touches security-relevant behavior, you - 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. +- Relationship annotations use 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. @@ -48,14 +48,14 @@ Every time you write or modify code that touches security-relevant behavior, you - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -79,13 +79,13 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows @@ -102,6 +102,12 @@ Every time you write or modify code that touches security-relevant behavior, you + + + + + + diff --git a/AGENTS.md b/AGENTS.md index bf121a4..edc93ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ ## GuardLink — Security Annotations (Required) -This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments or standalone `.gal` files. **Full reference: `docs/GUARDLINK_REFERENCE.md`** ### Core Requirement @@ -16,7 +16,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 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`. +5. Relationship annotations use verbs like: `@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. @@ -62,14 +62,14 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -93,13 +93,13 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 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 @@ -120,6 +120,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + + diff --git a/CLAUDE.md b/CLAUDE.md index 68c3a9f..6332e83 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -3,7 +3,7 @@ ## GuardLink — Security Annotations (Required) -This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments or standalone `.gal` files. **Full reference: `docs/GUARDLINK_REFERENCE.md`** ### Core Requirement @@ -16,7 +16,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 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`. +5. Relationship annotations use verbs like: `@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. @@ -62,14 +62,14 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c - #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:31) +- #sarif exposed to #data-exposure [low] (src/analyzer/sarif.ts:15) - #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:30) - #mcp exposed to #data-exposure [medium] (src/mcp/server.ts:34) - #suggest exposed to #dos [low] (src/mcp/suggest.ts:16) -- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:7) +- #parser exposed to #arbitrary-write [high] (src/parser/clear.ts:8) - #tui exposed to #cmd-injection [high] (src/tui/commands.ts:11) - #tui exposed to #prompt-injection [medium] (src/tui/commands.ts:15) @@ -93,13 +93,13 @@ 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 +- UserArgs -> #cli via process.argv +- #cli -> FileSystem via writeFile - ... and 48 more ### Model Stats -290 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 flows +291 annotations, 16 assets, 15 threats, 12 controls, 60 exposures, 44 mitigations, 68 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 @@ -120,6 +120,12 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c + + + + + + diff --git a/README.md b/README.md index aab6572..e31ad15 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ To uninstall: `npm unlink -g guardlink` guardlink init # Let AI annotate your project - Launch a coding agent to add annotations -guardlink annotate [prompt] +guardlink annotate [prompt] [--mode inline|gal] # Let your AI coding agent annotate, or write annotations manually # Then validate @@ -149,7 +149,7 @@ GuardLink ships an MCP server and behavioral directives for AI coding agents. Af | `guardlink_suggest` | Suggest annotations for a code snippet | | `guardlink_lookup` | Query threats, controls, flows by keyword | | `guardlink_threat_report` | AI threat report (STRIDE, DREAD, etc.) | -| `guardlink_annotate` | Build annotation prompt for the agent | +| `guardlink_annotate` | Build annotation prompt for the agent, with inline or `.gal` mode | | `guardlink_report` | Generate markdown report | | `guardlink_dashboard` | Generate HTML dashboard | | `guardlink_sarif` | Export SARIF 2.1.0 | @@ -165,7 +165,7 @@ GuardLink ships an MCP server and behavioral directives for AI coding agents. Af | Command | Description | |---------|-------------| | `guardlink init [dir]` | Initialize project with definitions, config, and agent integration | -| `guardlink annotate [prompt]` | Launch a coding agent to add annotations | +| `guardlink annotate [prompt] [--mode inline\|gal]` | Launch a coding agent to add inline annotations or associated `.gal` files | | `guardlink parse [dir]` | Parse all annotations, output ThreatModel JSON | | `guardlink status [dir]` | Coverage summary: assets, threats, mitigations, exposures | | `guardlink validate [dir]` | Check for syntax errors, dangling refs, duplicate IDs | @@ -195,7 +195,9 @@ GuardLink ships an MCP server and behavioral directives for AI coding agents. Af ## Annotation Reference -GuardLink annotations go in comments in any language. The parser supports `//`, `#`, `--`, `/* */`, `""" """`, and 25+ comment styles. +GuardLink annotations can live in source comments in any language or in standalone `.gal` files. The parser supports `//`, `#`, `--`, `/* */`, `""" """`, and 25+ comment styles for inline annotations, plus raw GAL lines for externalized files. + +> In standalone `.gal` files, drop the host-language comment prefix. `// @exposes ...` becomes `@exposes ...`. Keep definitions in `.guardlink/definitions.*`; use `.gal` files for externalized relationship annotations. Use `@source file: line: [symbol:]` to point the following annotations at the real code location. ### Definitions (shared, in `.guardlink/definitions.js`) @@ -214,6 +216,15 @@ GuardLink annotations go in comments in any language. The parser supports `//`, # @transfers #sqli from #api to #database -- "DB handles untrusted input" ``` +### Externalized relationships (in `.gal` files) + +```text +@source file:src/auth/login.ts line:42 symbol:authenticate +@exposes #api to #xss [P1] cwe:CWE-79 -- "User bio rendered without escaping" +@audit #api -- "Review sanitization before release" +@comment -- "Same GAL syntax as inline comments, but without // or # prefixes" +``` + ### Data Flow & Architecture ```go diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..aa8f10b --- /dev/null +++ b/bun.lock @@ -0,0 +1,470 @@ +{ + "lockfileVersion": 1, + "configVersion": 0, + "workspaces": { + "": { + "name": "guardlink", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.26.0", + "chalk": "^5.4.0", + "commander": "^13.0.0", + "fast-glob": "^3.3.0", + "gradient-string": "^3.0.0", + }, + "devDependencies": { + "@types/gradient-string": "^1.1.6", + "@types/node": "^22.0.0", + "tsx": "^4.0.0", + "typescript": "^5.7.0", + "vitest": "^3.0.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="], + + "@hono/node-server": ["@hono/node-server@1.19.9", "", { "peerDependencies": { "hono": "^4" } }, "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.26.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="], + + "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/gradient-string": ["@types/gradient-string@1.1.6", "", { "dependencies": { "@types/tinycolor2": "*" } }, "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ=="], + + "@types/node": ["@types/node@22.19.11", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w=="], + + "@types/tinycolor2": ["@types/tinycolor2@1.4.6", "", {}, "sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" }, "peerDependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "body-parser": ["body-parser@2.2.2", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.3", "http-errors": "^2.0.0", "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.1", "raw-body": "^3.0.1", "type-is": "^2.0.1" } }, "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "content-disposition": ["content-disposition@1.0.1", "", {}, "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": "bin/esbuild" }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], + + "expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.2.1", "", { "dependencies": { "ip-address": "10.0.1" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.0.1", "", {}, "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": "bin/nanoid.cjs" }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@8.3.0", "", {}, "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.14.2", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rollup": ["rollup@4.57.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": "dist/cli.mjs" }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + + "type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "yaml"], "bin": "bin/vite.js" }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], + + "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": "vite-node.mjs" }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": "vitest.mjs" }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": "cli.js" }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "tinyglobby/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "vite/fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "vite/picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + } +} diff --git a/docs/GUARDLINK_REFERENCE.md b/docs/GUARDLINK_REFERENCE.md index 5d059c9..4602e7f 100644 --- a/docs/GUARDLINK_REFERENCE.md +++ b/docs/GUARDLINK_REFERENCE.md @@ -43,11 +43,23 @@ Append after severity: `cwe:CWE-89`, `owasp:A03:2021`, `capec:CAPEC-66`, `attack ## Rules 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.). +2. **Define once, reference everywhere.** `@asset`, `@threat`, `@control` with `(#id)` go in `.guardlink/definitions.ts`. Relationship annotations can live inline in source comments or in standalone `.gal` files. 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. +### Standalone `.gal` Files + +Use the same GAL syntax without language comment prefixes. Definitions still belong in `.guardlink/definitions.*`; `.gal` files are for externalized relationship annotations: + +```text +@source file:src/auth/login.ts line:42 symbol:authenticate +@exposes #api to #xss [P1] cwe:CWE-79 -- "User bio rendered without escaping" +@audit #api -- "Review sanitization before release" +``` + +`@source` sets the logical code location for the following annotations until the next `@source`. `symbol:` is optional. + ## When Writing Code | Situation | Annotation | @@ -79,7 +91,7 @@ guardlink diff [ref] # Compare threat model against a git ref # AI-Powered Analysis guardlink threat-report # AI threat report (see frameworks below) guardlink threat-reports # List saved threat reports -guardlink annotate # Launch coding agent to add annotations +guardlink annotate [--mode inline|gal] # Launch coding agent to add annotations guardlink config # Manage LLM provider / CLI agent configuration # Governance & Maintenance @@ -118,6 +130,7 @@ All AI commands (`threat-report`, `annotate`) support: --cursor # Open Cursor IDE with prompt on clipboard --windsurf # Open Windsurf IDE with prompt on clipboard --clipboard # Copy prompt to clipboard only +--mode # Annotation placement mode: inline or gal ``` Additional `threat-report` flags: @@ -146,7 +159,7 @@ Run `guardlink tui` for the interactive terminal interface: /view Show all annotations in a file with code context /threat-report AI threat report (frameworks above or custom text) /threat-reports List saved reports -/annotate Launch coding agent to annotate codebase +/annotate Launch coding agent to annotate codebase (use --mode gal for .gal files) /model Set AI provider (API or CLI agent) /report Generate markdown + JSON report /dashboard Generate HTML dashboard + open browser diff --git a/docs/SPEC.md b/docs/SPEC.md index 02318cf..9fb9752 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -20,7 +20,7 @@ GuardLink extends the original ThreatSpec specification (dormant since 2020) wit **1.1. Annotations are English verbs.** Every annotation reads as a natural-language statement. A developer encountering `@mitigates AuthService against SQL_Injection` understands it without documentation. Branded prefixes, product names, and abbreviations are avoided in the core syntax. -**1.2. Annotations live in comments.** GuardLink annotations are embedded in source code comments using the host language's comment syntax. They travel with the code through version control, appear in pull request diffs, and are reviewed alongside the code they describe. +**1.2. Annotations live with the codebase.** GuardLink annotations are usually embedded in source code comments using the host language's comment syntax, but they may also be stored in standalone `.gal` files when inline comments are impractical. In both cases they travel with the code through version control, appear in pull request diffs, and are reviewed alongside the code they describe. **1.3. Annotations are structured data.** While readable as English, every annotation has a deterministic grammar that can be parsed by regex into typed data structures. Ambiguous or free-form syntax is avoided. @@ -39,17 +39,27 @@ GuardLink extends the original ThreatSpec specification (dormant since 2020) wit All GuardLink annotations follow this pattern: ``` - @ [] [] [-- ""] +[] @ [] [] [-- ""] ``` Where: -- `` is the host language's comment syntax (`//`, `#`, `--`, `/*`, etc.) +- `` is the host language's comment syntax (`//`, `#`, `--`, `/*`, etc.) and is required for inline source annotations - `@` is one of the defined annotation verbs (§3) - `` are verb-specific, positional (§3) - `` are optional metadata in brackets: severity (§2.5) or classification (§2.6) - `` are optional references to external taxonomies (§2.8) - `-- ""` is an optional human-readable explanation (§2.7) +Standalone `.gal` files omit the comment prefix and store raw annotation lines directly. Definition annotations still live in `.guardlink/definitions.*`; `.gal` files are for externalized relationship annotations: + +```text +@source file:src/auth/login.ts line:42 symbol:authenticate +@exposes #api to #xss [high] cwe:CWE-79 -- "User bio rendered without escaping" +@audit #api -- "Review sanitization before release" +``` + +In standalone `.gal` files, `@source file: line: [symbol:]` updates the logical source location for the following annotations until another `@source` appears. + ### 2.2. Component Paths Components are referenced using dot-separated paths that mirror the system's logical architecture: @@ -652,7 +662,9 @@ Every annotation carries a source location: "file": "src/auth/login.ts", "line": 15, "end_line": null, - "parent_symbol": "authenticate" + "parent_symbol": "authenticate", + "origin_file": ".guardlink/annotations/auth.gal", + "origin_line": 8 } ``` @@ -660,6 +672,8 @@ Every annotation carries a source location: - `line`: 1-indexed line number of the annotation comment - `end_line`: For block annotations (`@shield:begin/end`), the closing line - `parent_symbol`: Best-effort detection of the enclosing function, method, or class name. `null` when detection fails. Tools must not rely on this field for correctness — it is metadata for human readability. +- `origin_file`: For externalized `.gal` annotations, the physical annotation file where the GAL line lives +- `origin_line`: For externalized `.gal` annotations, the 1-indexed line in the `.gal` file where the annotation was declared ### 5.3. Graph Interpretation diff --git a/package-lock.json b/package-lock.json index 1598cd4..7305626 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "guardlink", - "version": "1.3.0", + "version": "1.4.1-gal", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "guardlink", - "version": "1.3.0", + "version": "1.4.1-gal", "license": "MIT", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", diff --git a/package.json b/package.json index 7874316..64e7ff5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "guardlink", - "version": "1.4.1", + "version": "1.4.1-gal", "description": "GuardLink — Security annotations for code. Threat modeling that lives in your codebase.", "type": "module", "bin": { diff --git a/src/agents/index.ts b/src/agents/index.ts index a1bf5c5..cab6bbd 100644 --- a/src/agents/index.ts +++ b/src/agents/index.ts @@ -18,6 +18,8 @@ export interface AgentEntry { flag: string; // CLI flag (--claude-code, --cursor, etc.) } +export type AnnotationMode = 'inline' | 'external'; + export const AGENTS: readonly AgentEntry[] = [ { id: 'claude-code', name: 'Claude Code', cmd: 'claude', app: null, flag: '--claude-code' }, { id: 'cursor', name: 'Cursor', cmd: null, app: 'Cursor', flag: '--cursor' }, @@ -25,6 +27,7 @@ export const AGENTS: readonly AgentEntry[] = [ { id: 'codex', name: 'Codex CLI', cmd: 'codex', app: null, flag: '--codex' }, { id: 'gemini', name: 'Gemini CLI', cmd: 'gemini', app: null, flag: '--gemini' }, { id: 'clipboard', name: 'Clipboard', cmd: null, app: null, flag: '--clipboard' }, + { id: 'stdout', name: 'Stdout', cmd: null, app: null, flag: '--stdout' }, ] as const; /** Parse --agent flags from a raw args string (TUI slash commands). */ @@ -37,6 +40,41 @@ export function parseAgentFlag(args: string): { agent: AgentEntry | null; cleanA return { agent: null, cleanArgs: args }; } +/** Parse annotation placement mode from raw args (CLI/TUI). */ +export function parseAnnotationModeFlag(args: string): { mode: AnnotationMode; cleanArgs: string; error?: string } { + const eqMatch = args.match(/(?:^|\s)--mode=(inline|external)(?=\s|$)/); + if (eqMatch) { + return { + mode: eqMatch[1] as AnnotationMode, + cleanArgs: args.replace(eqMatch[0], ' ').replace(/\s+/g, ' ').trim(), + }; + } + + const spacedMatch = args.match(/(?:^|\s)--mode\s+(inline|external)(?=\s|$)/); + if (spacedMatch) { + return { + mode: spacedMatch[1] as AnnotationMode, + cleanArgs: args.replace(spacedMatch[0], ' ').replace(/\s+/g, ' ').trim(), + }; + } + + if (/(?:^|\s)--mode(?:\s|=|$)/.test(args)) { + return { + mode: 'inline', + cleanArgs: args, + error: 'Invalid --mode value. Use --mode inline or --mode external.', + }; + } + + return { mode: 'inline', cleanArgs: args }; +} + +export function resolveAnnotationMode(mode: string | undefined): AnnotationMode { + if (!mode || mode === 'inline') return 'inline'; + if (mode === 'external') return 'external'; + throw new Error(`Invalid annotation mode "${mode}". Use "inline" or "external".`); +} + /** Resolve agent from Commander option booleans (CLI commands). */ export function agentFromOpts(opts: Record): AgentEntry | null { if (opts.claudeCode) return AGENTS.find(a => a.id === 'claude-code')!; @@ -45,6 +83,7 @@ export function agentFromOpts(opts: Record): AgentEntry | null { if (opts.codex) return AGENTS.find(a => a.id === 'codex')!; if (opts.gemini) return AGENTS.find(a => a.id === 'gemini')!; if (opts.clipboard) return AGENTS.find(a => a.id === 'clipboard')!; + if (opts.stdout) return AGENTS.find(a => a.id === 'stdout')!; return null; } diff --git a/src/agents/launcher.ts b/src/agents/launcher.ts index 77f1200..ad79ccc 100644 --- a/src/agents/launcher.ts +++ b/src/agents/launcher.ts @@ -302,6 +302,12 @@ export interface LaunchResult { * For clipboard: copy only. */ export function launchAgent(agent: AgentEntry, prompt: string, cwd: string): LaunchResult { + // stdout-only mode: write raw prompt to stdout, skip clipboard (keeps output pipeable) + if (agent.id === 'stdout') { + process.stdout.write(prompt); + return { launched: true, clipboardCopied: false }; + } + // Step 1: Always copy to clipboard const clipboardCopied = copyToClipboard(prompt); diff --git a/src/agents/prompts.ts b/src/agents/prompts.ts index 646b7fa..fac0696 100644 --- a/src/agents/prompts.ts +++ b/src/agents/prompts.ts @@ -17,6 +17,35 @@ import { existsSync, readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import type { ThreatModel } from '../types/index.js'; +export type AnnotationMode = 'inline' | 'external'; + +function annotationModeLabel(mode: AnnotationMode): string { + return mode === 'external' ? 'externalized .gal files' : 'inline source comments'; +} + +function annotationModeInstructions(mode: AnnotationMode): string { + if (mode === 'external') { + return `## Annotation Placement Mode +You MUST write annotations into associated standalone \`.gal\` files, not inline in the source code. + +- Keep definitions in \`.guardlink/definitions.*\` +- For each annotated source file, create or update an associated file under \`.guardlink/annotations/\` +- Mirror the source path in the annotation file path (example: \`src/auth/login.ts\` -> \`.guardlink/annotations/src/auth/login.ts.gal\`) +- Group annotations under \`@source file: line: [symbol:]\` so each block points at the real code location +- In \`.gal\` files, write raw GAL lines without \`//\` or \`#\` prefixes +- Do NOT modify source files just to add comments when this mode is selected +`; + } + + return `## Annotation Placement Mode +You MUST write annotations inline in the source code comments. + +- Place annotations in the file doc-block or directly above the security-relevant code +- Use the host language comment syntax (\`//\`, \`#\`, \`--\`, etc.) +- Do NOT externalize annotations into \`.gal\` files when this mode is selected +`; +} + /** * Build a prompt for annotation agents. * @@ -27,6 +56,7 @@ export function buildAnnotatePrompt( userPrompt: string, root: string, model: ThreatModel | null, + annotationMode: AnnotationMode = 'inline', ): string { // Read the reference doc if available let refDoc = ''; @@ -96,6 +126,7 @@ export function buildAnnotatePrompt( return `You are an expert security engineer performing threat modeling as code. Your job is to read this codebase deeply, understand how code flows between components, and annotate it with GuardLink (GAL) security annotations that accurately represent the security posture. +This run MUST produce annotations as ${annotationModeLabel(annotationMode)}. This is NOT a vulnerability scanner. You are building a living threat model embedded in the code itself. Annotations capture what COULD go wrong, what controls exist, and how data moves — not just confirmed bugs. @@ -106,6 +137,8 @@ ${modelSummary}${existingIds}${existingFlows}${existingExposures} ## Your Task ${userPrompt} +${annotationModeInstructions(annotationMode)} + ## HOW TO THINK — Flow-First Threat Modeling Before writing ANY annotation, you MUST understand the code deeply: @@ -208,19 +241,33 @@ Place @boundary annotations where trust level changes between two components: \`\`\` ### Where to Place Annotations -Annotations go in the file's top doc-block comment OR directly above the security-relevant code: +${annotationMode === 'external' + ? 'Annotations go in associated `.gal` files, grouped by `@source` blocks that point at the real code location:' + : "Annotations go in the file's top doc-block comment OR directly above the security-relevant code:"} \`\`\` -// @shield:begin -- "Placement examples, excluded from parsing" -// -// FILE-LEVEL (top doc-block) — for module-wide security properties: -// Place @exposes, @mitigates, @flows, @handles, @boundary that describe the module as a whole -// -// INLINE (above specific functions/methods) — for function-specific concerns: -// Place @exposes, @mitigates above the exact function where the risk or control lives -// Place @comment above tricky security-relevant code to explain intent -// -// @shield:end +${annotationMode === 'external' + ? [ + '@source file:src/auth/login.ts line:42 symbol:authenticate', + '@exposes #auth-api to #sqli [P1] cwe:CWE-89 -- "User-supplied email reaches query builder"', + '@mitigates #auth-api against #sqli using #input-validation -- "Zod schema validates email before query"', + '@comment -- "Externalized annotations for src/auth/login.ts"', + '', + '@source file:src/auth/session.ts line:88 symbol:issueToken', + '@handles secrets on #auth-api -- "Issues session token"', + ].join('\n') + : [ + '// @shield:begin -- "Placement examples, excluded from parsing"', + '//', + '// FILE-LEVEL (top doc-block) — for module-wide security properties:', + '// Place @exposes, @mitigates, @flows, @handles, @boundary that describe the module as a whole', + '//', + '// INLINE (above specific functions/methods) — for function-specific concerns:', + '// Place @exposes, @mitigates above the exact function where the risk or control lives', + '// Place @comment above tricky security-relevant code to explain intent', + '//', + '// @shield:end', + ].join('\n')} \`\`\` ### Severity — Be Honest, Not Alarmist @@ -270,7 +317,7 @@ Adding @shield on your own initiative would actively harm the threat model by cr ## PRECISE GAL Syntax -Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only relationship verbs. +Definitions go in .guardlink/definitions.{ts,js,py,rs}. Relationship annotations can live in source comments or standalone .gal files. ### Definitions (in .guardlink/definitions file) \`\`\` @@ -299,6 +346,14 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re // @shield:end \`\`\` +### Relationships (in standalone .gal files) +\`\`\` +@source file:src/auth/login.ts line:42 symbol:authenticate +@exposes #auth to #sqli [P0] cwe:CWE-89 owasp:A03:2021 -- "User input concatenated into query" +@mitigates #auth against #sqli using #prepared-stmts -- "Uses parameterized queries via sqlx" +@audit #auth -- "Timing attack risk — needs human review" +\`\`\` + ## CRITICAL SYNTAX RULES (violations cause parse errors) 1. **@boundary requires TWO assets**: \`@boundary between #A and #B\` or \`@boundary #A | #B\`. @@ -333,6 +388,7 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re A bare \`@comment\` without description is valid but useless. Always include context. 10. **One annotation per comment line.** Do NOT put two @verbs on the same line. +11. **In external mode, use \`@source\` before each block** so the annotations point at the intended file and line. ## Workflow @@ -350,7 +406,7 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re Think: "what's the risk, what's the defense, how does data flow here, and what should the next developer know?" NEVER write @accepts — that is a human-only governance decision. Use @audit to flag unmitigated risks for review. -5. **Use the project's comment style** (// for JS/TS/Go/Rust, # for Python/Ruby/Shell, etc.) +5. **Use the selected annotation mode consistently.** Inline mode writes source comments; external mode writes associated \`.gal\` files with \`@source\` blocks. 6. **Run validation** via guardlink_validate (MCP) or \`guardlink validate\` to check for errors. diff --git a/src/analyze/prompts.ts b/src/analyze/prompts.ts index e2a48fe..5b3f045 100644 --- a/src/analyze/prompts.ts +++ b/src/analyze/prompts.ts @@ -27,7 +27,7 @@ Your job is to **produce a complete, standalone threat model** for a real codeba You will receive: 1. **Project context** — language/framework, dependencies, deployment signals (Dockerfile, CI config, etc.) -2. **Annotation graph** — structured security metadata extracted from source code comments (GuardLink annotations) +2. **Annotation graph** — structured security metadata extracted from GuardLink annotations in source comments or standalone \`.gal\` files 3. **Code snippets** — the actual source lines surrounding each annotation, so you can validate what developers claimed ## How to use these inputs diff --git a/src/cli/index.ts b/src/cli/index.ts index ec93948..4cd3ff2 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -47,7 +47,7 @@ import { generateSarif } from '../analyzer/index.js'; import { startStdioServer } from '../mcp/index.js'; import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage, type AnalysisFramework } from '../analyze/index.js'; import { generateDashboardHTML } from '../dashboard/index.js'; -import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt } from '../agents/index.js'; +import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt, resolveAnnotationMode } from '../agents/index.js'; import { resolveConfig, saveProjectConfig, saveGlobalConfig, loadProjectConfig, loadGlobalConfig, maskKey, describeConfigSource } from '../agents/config.js'; import { getReviewableExposures, applyReviewAction, formatExposureForReview, summarizeReview, type ReviewResult } from '../review/index.js'; import { populateMetadata, mergeReports, formatMergeSummary, diffMergedReports, formatDiffSummary, linkProject, addToWorkspace, removeFromWorkspace } from '../workspace/index.js'; @@ -97,7 +97,7 @@ function detectProjectName(root: string, explicit?: string): string { program .name('guardlink') .description('GuardLink — Security annotations for code. Threat modeling that lives in your codebase.') - .version('1.4.1') + .version('1.4.1-gal') .addHelpText('before', gradient(['#00ff41', '#00d4ff'])(ASCII_LOGO)); // ─── init ──────────────────────────────────────────────────────────── @@ -108,10 +108,11 @@ program .argument('[dir]', 'Project directory', '.') .option('-p, --project ', 'Override project name') .option('-a, --agent ', 'Agent(s) to create files for: claude,cursor,codex,copilot,windsurf,cline,none (comma-separated)') + .option('--mode ', 'Annotation mode: inline (default) or external. external restricts all writes to .guardlink/ — no agent files, no .mcp.json at root', 'inline') .option('--skip-agent-files', 'Only create .guardlink/, skip agent file updates') .option('--force', 'Overwrite existing GuardLink config and instructions') .option('--dry-run', 'Show what would be created without writing files') - .action(async (dir: string, opts: { project?: string; agent?: string; skipAgentFiles?: boolean; force?: boolean; dryRun?: boolean }) => { + .action(async (dir: string, opts: { project?: string; agent?: string; mode?: string; skipAgentFiles?: boolean; force?: boolean; dryRun?: boolean }) => { const root = resolve(dir); // Show detection results first @@ -148,6 +149,7 @@ program const result = initProject({ root, project: opts.project, + mode: resolveAnnotationMode(opts.mode), skipAgentFiles: opts.skipAgentFiles, force: opts.force, dryRun: opts.dryRun, @@ -673,19 +675,29 @@ program .argument('', 'Annotation instructions (e.g., "annotate auth endpoints for OWASP Top 10")') .argument('[dir]', 'Project directory', '.') .option('-p, --project ', 'Project name', 'unknown') + .option('--mode ', 'Annotation placement mode: inline (default) or external (externalized .gal files)', 'inline') .option('--claude-code', 'Launch Claude Code in foreground') .option('--codex', 'Launch Codex CLI in foreground') .option('--gemini', 'Launch Gemini CLI in foreground') .option('--cursor', 'Open Cursor IDE with prompt on clipboard') .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard') .option('--clipboard', 'Copy annotation prompt to clipboard only') + .option('--stdout', 'Print annotation prompt to stdout and exit (for piping)') .action(async (prompt: string, dir: string, opts: { project: string; + mode?: string; claudeCode?: boolean; codex?: boolean; gemini?: boolean; - cursor?: boolean; windsurf?: boolean; clipboard?: boolean; + cursor?: boolean; windsurf?: boolean; clipboard?: boolean; stdout?: boolean; }) => { const root = resolve(dir); const project = detectProjectName(root, opts.project); + let annotationMode; + try { + annotationMode = resolveAnnotationMode(opts.mode); + } catch (err: any) { + console.error(err.message); + process.exit(1); + } // Resolve agent const agent = agentFromOpts(opts); @@ -707,16 +719,21 @@ program } catch { /* no model yet — that's fine */ } // Build prompt - const fullPrompt = buildAnnotatePrompt(prompt, root, model); + const fullPrompt = buildAnnotatePrompt(prompt, root, model, annotationMode); // Launch agent - console.log(`Launching ${agent.name} for annotation...`); - if (agent.cmd) { - console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`); + if (agent.id !== 'stdout') { + console.log(`Launching ${agent.name} for annotation...`); + if (agent.cmd) { + console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`); + } } const result = launchAgent(agent, fullPrompt, root); + // stdout mode: prompt already written to stdout — nothing else to do + if (agent.id === 'stdout') return; + if (result.clipboardCopied) { console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`); } @@ -931,7 +948,7 @@ program } 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`); + console.error(` ✓ Accepted — ${result.linesInserted} line(s) written to ${result.targetFile}\n`); } else if (choice === 'r') { let note = ''; while (!note) { @@ -940,9 +957,9 @@ program } 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`); + console.error(` ✓ Marked for remediation — ${result.linesInserted} line(s) written to ${result.targetFile}\n`); } else { - results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0 }); + results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0, targetFile: reviewable.exposure.location.file }); console.error(' — Skipped\n'); } } @@ -1404,10 +1421,12 @@ program console.log(H(' GAL — GuardLink Annotation Language')); console.log(H(' ══════════════════════════════════════════════════════════')); console.log(''); - console.log(D(' Annotations live in source code comments. GuardLink parses')); - console.log(D(' them to build a live threat model from your codebase.')); + console.log(D(' Annotations live in source comments or standalone .gal files.')); + console.log(D(' GuardLink parses them into a live threat model for your codebase.')); console.log(''); console.log(D(' Syntax: @verb subject [preposition object] [-- "description"]')); + console.log(D(' Inline examples below use comment prefixes; raw .gal files use the same lines without // or #.')); + console.log(D(' In .gal files, use @source file: line: [symbol:] to anchor following annotations.')); console.log(''); // ── DEFINITIONS ── diff --git a/src/diff/git.ts b/src/diff/git.ts index a8b58ab..358ffac 100644 --- a/src/diff/git.ts +++ b/src/diff/git.ts @@ -45,14 +45,14 @@ export async function parseAtRef(root: string, ref: string, project: string): Pr const filesRaw = execSync(`git ls-tree -r --name-only ${ref}`, { cwd: root, encoding: 'utf-8' }); const allFiles = filesRaw.trim().split('\n').filter(Boolean); - // Filter to likely annotated files (source code + definitions) + // Filter to likely annotated files (source code + standalone GAL annotations) const extensions = new Set([ '.ts', '.tsx', '.js', '.jsx', '.py', '.rs', '.go', '.java', '.rb', '.c', '.cpp', '.h', '.cs', '.php', '.swift', '.kt', '.scala', - '.yaml', '.yml', '.toml', '.json', + '.yaml', '.yml', '.toml', '.json', '.gal', ]); const relevantFiles = allFiles.filter(f => { - const ext = f.substring(f.lastIndexOf('.')); + const ext = f.substring(f.lastIndexOf('.')).toLowerCase(); return extensions.has(ext) || f.includes('.guardlink/'); }); diff --git a/src/init/index.ts b/src/init/index.ts index 388a7fc..87942d7 100644 --- a/src/init/index.ts +++ b/src/init/index.ts @@ -34,6 +34,7 @@ import { GITIGNORE_ENTRY, } from './templates.js'; import type { ThreatModel } from '../types/index.js'; +import type { AnnotationMode } from '../agents/index.js'; import { AGENT_CHOICES } from './picker.js'; export { detectProject, type ProjectInfo, type AgentFile } from './detect.js'; @@ -54,6 +55,12 @@ export interface InitOptions { dryRun?: boolean; /** Explicit agent IDs to create files for (when no existing agent files found) */ agentIds?: string[]; + /** + * Annotation placement mode. + * external: restrict all writes to .guardlink/ — no agent files, no .mcp.json at root, no docs/. + * inline: default behavior, writes all files including agent instruction files. + */ + mode?: AnnotationMode; } export interface InitResult { @@ -72,6 +79,7 @@ const GUARDLINK_MARKER_END = ''; export function initProject(options: InitOptions): InitResult { const { root, force = false, dryRun = false, skipAgentFiles = false } = options; + const isExternal = options.mode === 'external'; const project = detectProject(root); if (options.project) project.name = options.project; @@ -109,48 +117,77 @@ export function initProject(options: InitOptions): InitResult { skipped.push(`.guardlink/${defsFile} (exists)`); } - // ── 4. Create docs/GUARDLINK_REFERENCE.md ── + // ── 4. Create reference doc ── + // external mode: inside .guardlink/ (zero footprint outside it) + // inline mode: docs/GUARDLINK_REFERENCE.md (visible to humans browsing the project) - const docsDir = join(root, 'docs'); - const refDocPath = join(docsDir, 'GUARDLINK_REFERENCE.md'); - if (!existsSync(refDocPath) || force) { - if (!dryRun) { - ensureDir(docsDir); - writeFileSync(refDocPath, referenceDocContent(project)); + if (isExternal) { + const refDocPath = join(tsDir, 'GUARDLINK_REFERENCE.md'); + if (!existsSync(refDocPath) || force) { + if (!dryRun) writeFileSync(refDocPath, referenceDocContent(project)); + created.push('.guardlink/GUARDLINK_REFERENCE.md'); + } else { + skipped.push('.guardlink/GUARDLINK_REFERENCE.md (exists)'); } - created.push('docs/GUARDLINK_REFERENCE.md'); } else { - skipped.push('docs/GUARDLINK_REFERENCE.md (exists)'); + const docsDir = join(root, 'docs'); + const refDocPath = join(docsDir, 'GUARDLINK_REFERENCE.md'); + if (!existsSync(refDocPath) || force) { + if (!dryRun) { + ensureDir(docsDir); + writeFileSync(refDocPath, referenceDocContent(project)); + } + created.push('docs/GUARDLINK_REFERENCE.md'); + } else { + skipped.push('docs/GUARDLINK_REFERENCE.md (exists)'); + } } // ── 5. Update .gitignore ── - - const gitignorePath = join(root, '.gitignore'); - if (existsSync(gitignorePath)) { - const content = readFileSync(gitignorePath, 'utf-8'); - if (!content.includes('GuardLink') && !content.includes('.guardlink')) { - if (!dryRun) appendFileSync(gitignorePath, GITIGNORE_ENTRY); - updated.push('.gitignore'); + // Skipped in external mode: .guardlink/ is intentionally committed as a whole. + + if (!isExternal) { + const gitignorePath = join(root, '.gitignore'); + if (existsSync(gitignorePath)) { + const content = readFileSync(gitignorePath, 'utf-8'); + if (!content.includes('GuardLink') && !content.includes('.guardlink')) { + if (!dryRun) appendFileSync(gitignorePath, GITIGNORE_ENTRY); + updated.push('.gitignore'); + } } } // ── 6. Update/create agent instruction files ── + // Skipped in external mode: all writes are contained in .guardlink/. - if (!skipAgentFiles) { + if (!skipAgentFiles && !isExternal) { const agentResults = updateAgentFiles(root, project, force, dryRun, options.agentIds); created.push(...agentResults.created); updated.push(...agentResults.updated); skipped.push(...agentResults.skipped); } - // ── 7. Create .mcp.json for Claude Code MCP integration ── + // ── 7. MCP config ── + // external mode: placed inside .guardlink/ as a reference template (won't be auto-discovered + // by MCP clients, but documents the config for devs who want to enable it locally). + // inline mode: .mcp.json at project root for auto-discovery by Claude Code and other MCP clients. - const mcpPath = join(root, '.mcp.json'); - if (!existsSync(mcpPath) || force) { - if (!dryRun) writeFileSync(mcpPath, mcpConfig()); - created.push('.mcp.json'); + if (isExternal) { + const mcpPath = join(tsDir, '.mcp.json'); + if (!existsSync(mcpPath) || force) { + if (!dryRun) writeFileSync(mcpPath, mcpConfig()); + created.push('.guardlink/.mcp.json'); + } else { + skipped.push('.guardlink/.mcp.json (exists)'); + } } else { - skipped.push('.mcp.json (exists)'); + const mcpPath = join(root, '.mcp.json'); + if (!existsSync(mcpPath) || force) { + if (!dryRun) writeFileSync(mcpPath, mcpConfig()); + created.push('.mcp.json'); + } else { + skipped.push('.mcp.json (exists)'); + } } return { project, created, updated, skipped }; diff --git a/src/init/templates.ts b/src/init/templates.ts index 46b9560..1a321e0 100644 --- a/src/init/templates.ts +++ b/src/init/templates.ts @@ -57,7 +57,7 @@ Append after severity: \`cwe:CWE-89\`, \`owasp:A03:2021\`, \`capec:CAPEC-66\`, \ ## Rules 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.). +2. **Define once, reference everywhere.** \`@asset\`, \`@threat\`, \`@control\` with \`(#id)\` go in \`.guardlink/definitions${project.definitionsExt}\`. Relationship annotations can live inline in source comments or in standalone \`.gal\` files. 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. @@ -105,7 +105,7 @@ export function agentInstructions(project: ProjectInfo): string { return ` ## GuardLink — Security Annotations (Required) -This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments. +This project uses [GuardLink](https://guardlink.bugb.io) annotations in source code comments or standalone \`.gal\` files. **Full reference: \`docs/GUARDLINK_REFERENCE.md\`** ### Core Requirement @@ -118,7 +118,7 @@ This project uses [GuardLink](https://guardlink.bugb.io) annotations in source c 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\`. +5. Relationship annotations use verbs like: \`@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. @@ -272,7 +272,7 @@ export function cursorRulesContent(project: ProjectInfo): string { return ` # GuardLink Security Annotations -This project uses GuardLink annotations in source code comments. +This project uses GuardLink annotations in source code comments or standalone .gal files. ## 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. @@ -282,7 +282,7 @@ Every time you write or modify code that touches security-relevant behavior, you - 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. +- Relationship annotations use 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. diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 72b8921..6b6e9c1 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -85,7 +85,7 @@ function invalidateCache() { export function createServer(): McpServer { const server = new McpServer({ name: 'guardlink', - version: '1.4.0', + version: '1.4.1-gal', }); // ── Tool: guardlink_parse ── @@ -286,8 +286,9 @@ export function createServer(): McpServer { { root: z.string().describe('Project root directory').default('.'), prompt: z.string().describe('Annotation instructions (e.g., "annotate auth endpoints for OWASP Top 10")'), + mode: z.enum(['inline', 'external']).describe('Annotation placement mode — inline (default) or external (externalized .gal files)').default('inline'), }, - async ({ root, prompt }) => { + async ({ root, prompt, mode }) => { let model: ThreatModel | null = null; try { const result = await getModel(root); @@ -296,16 +297,20 @@ export function createServer(): McpServer { } } catch { /* no model yet — fine */ } - const annotatePrompt = buildAnnotatePrompt(prompt, root, model); + const annotatePrompt = buildAnnotatePrompt(prompt, root, model, mode); return { content: [{ type: 'text', text: JSON.stringify({ mode: 'agent', - message: 'Annotation prompt built with project context. Read the source files in the project directory, then add GuardLink annotations as code comments following the guidelines in the prompt. After annotating, call guardlink_parse to verify the annotations were parsed correctly.', + message: `Annotation prompt built with project context. Read the source files in the project directory, then add GuardLink annotations using ${mode === 'external' ? 'associated .gal files' : 'inline source comments'} following the guidelines in the prompt. After annotating, call guardlink_parse to verify the annotations were parsed correctly.`, prompt: annotatePrompt, guidelines: [ - 'Add annotations as comments directly above security-relevant code', - 'Use the project\'s comment style (// for TS/JS/Rust/Go, # for Python/Ruby/Shell)', + mode === 'external' + ? 'Write externalized annotations into associated .gal files using @source blocks' + : 'Add annotations as comments directly above security-relevant code', + mode === 'external' + ? 'Keep definitions in .guardlink/definitions.* and use raw GAL lines without comment prefixes' + : 'Use the project\'s comment style (// for TS/JS/Rust/Go, # for Python/Ruby/Shell)', 'After annotating, call guardlink_parse to verify results', ], }, null, 2) }], @@ -555,7 +560,7 @@ export function createServer(): McpServer { '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")'), + exposure_id: z.string().describe('Exposure ID from guardlink_review_list'), 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'), }, @@ -588,7 +593,7 @@ export function createServer(): McpServer { 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}` }], + content: [{ type: 'text', text: `${verb}: ${target.exposure.asset} → ${target.exposure.threat} [${target.exposure.severity}]\nJustification: ${justification}\n${result.linesInserted} annotation line(s) written to ${result.targetFile}` }], }; }, ); diff --git a/src/parser/clear.ts b/src/parser/clear.ts index 416c22b..dc4b9c1 100644 --- a/src/parser/clear.ts +++ b/src/parser/clear.ts @@ -1,6 +1,7 @@ /** * GuardLink — Annotation clearing utility. - * Scans project source files and removes all GuardLink annotation comment lines. + * Scans project source files and removes all GuardLink annotation lines. + * Standalone .gal files are treated as raw annotation text. * * Used by `guardlink clear` and `/clear` to let users start fresh with annotations. * @@ -16,8 +17,7 @@ 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'; +import { isStandaloneAnnotationFile, stripCommentPrefix } from './comment-strip.js'; // ─── Known GuardLink verbs ────────────────────────────────────────── @@ -25,7 +25,7 @@ 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', + 'comment', 'source', 'shield', 'shield:begin', 'shield:end', // v1 compat 'review', 'connects', ]); @@ -43,6 +43,7 @@ const DEFAULT_INCLUDE = [ '**/*.html', '**/*.xml', '**/*.svg', '**/*.css', '**/*.ex', '**/*.exs', + '**/*.[gG][aA][lL]', ]; const DEFAULT_EXCLUDE = [ @@ -74,11 +75,7 @@ export interface ClearAnnotationsResult { // ─── Core logic ───────────────────────────────────────────────────── -/** - * Check if a source line contains a GuardLink annotation. - */ -function isGuardLinkAnnotationLine(line: string): boolean { - const inner = stripCommentPrefix(line); +function isGuardLinkAnnotationText(inner: string | null): boolean { if (inner === null) return false; const trimmed = inner.trim(); @@ -92,16 +89,6 @@ function isGuardLinkAnnotationLine(line: string): boolean { 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. @@ -110,7 +97,7 @@ function isContinuationLine(line: string): boolean { * - Continuation lines (-- "...") that follow an annotation * - Empty comment lines that are left between annotations (cleanup) */ -function removeAnnotationsFromContent(content: string): { cleaned: string; removed: number } { +function removeAnnotationsFromContent(content: string, allowRawAnnotationLines: boolean): { cleaned: string; removed: number } { const lines = content.split('\n'); const result: string[] = []; let removed = 0; @@ -118,15 +105,16 @@ function removeAnnotationsFromContent(content: string): { cleaned: string; remov for (let i = 0; i < lines.length; i++) { const line = lines[i]; + const inner = allowRawAnnotationLines ? line : stripCommentPrefix(line); - if (isGuardLinkAnnotationLine(line)) { + if (isGuardLinkAnnotationText(inner)) { removed++; lastWasAnnotation = true; continue; } // Remove continuation lines that follow an annotation - if (lastWasAnnotation && isContinuationLine(line)) { + if (lastWasAnnotation && inner !== null && /^--\s*"/.test(inner.trim())) { removed++; continue; } @@ -160,17 +148,15 @@ export async function clearAnnotations(options: ClearAnnotationsOptions): Promis includeDefinitions = false, } = options; - // Build exclude list — skip .guardlink/ definitions unless explicitly included - const effectiveExclude = includeDefinitions - ? exclude - : [...exclude, '**/.guardlink/**']; - - const files = await fg(include, { + const candidateFiles = await fg(include, { cwd: root, - ignore: effectiveExclude, + ignore: exclude, absolute: true, dot: true, }); + const files = candidateFiles.filter(filePath => + includeDefinitions || !isDefinitionsFile(relative(root, filePath)), + ); const modifiedFiles: string[] = []; const perFile = new Map(); @@ -178,7 +164,7 @@ export async function clearAnnotations(options: ClearAnnotationsOptions): Promis for (const filePath of files) { const content = await readFile(filePath, 'utf-8'); - const { cleaned, removed } = removeAnnotationsFromContent(content); + const { cleaned, removed } = removeAnnotationsFromContent(content, isStandaloneAnnotationFile(filePath)); if (removed > 0) { const relPath = relative(root, filePath); @@ -194,3 +180,8 @@ export async function clearAnnotations(options: ClearAnnotationsOptions): Promis return { modifiedFiles, totalRemoved, perFile }; } + +function isDefinitionsFile(relPath: string): boolean { + const normalized = relPath.replaceAll('\\', '/'); + return normalized.startsWith('.guardlink/definitions.'); +} diff --git a/src/parser/comment-strip.ts b/src/parser/comment-strip.ts index 8c2cc2a..8afaae5 100644 --- a/src/parser/comment-strip.ts +++ b/src/parser/comment-strip.ts @@ -1,3 +1,5 @@ +import { extname } from 'node:path'; + /** * Comment prefix stripping per §2.9. * Strips the host language's comment prefix to expose the annotation text. @@ -56,6 +58,14 @@ export function stripCommentPrefix(line: string): string | null { return null; } +/** + * Standalone GAL files store raw annotation lines without host-language + * comment prefixes, unlike annotations embedded in source files. + */ +export function isStandaloneAnnotationFile(filePath: string): boolean { + return extname(filePath).toLowerCase() === '.gal'; +} + /** * Detect file's primary comment style from extension. * Used for multi-line continuation detection. diff --git a/src/parser/parse-file.ts b/src/parser/parse-file.ts index a87d343..e3f5605 100644 --- a/src/parser/parse-file.ts +++ b/src/parser/parse-file.ts @@ -1,6 +1,7 @@ /** * GuardLink — File-level parser. * Reads source files and extracts all GuardLink annotations. + * Standalone .gal files are treated as raw annotation text. * * @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" @@ -10,9 +11,8 @@ */ import { readFile } from 'node:fs/promises'; -import { basename, extname } from 'node:path'; -import type { Annotation, ParseDiagnostic, ParseResult } from '../types/index.js'; -import { stripCommentPrefix } from './comment-strip.js'; +import type { Annotation, ParseDiagnostic, ParseResult, SourceLocation } from '../types/index.js'; +import { isStandaloneAnnotationFile, stripCommentPrefix } from './comment-strip.js'; import { parseLine } from './parse-line.js'; import { unescapeDescription } from './normalize.js'; @@ -34,23 +34,27 @@ export function parseString(content: string, filePath: string = ''): Pars const diagnostics: ParseDiagnostic[] = []; let lastAnnotation: Annotation | null = null; let inShield = false; + const allowRawAnnotationLines = isStandaloneAnnotationFile(filePath); + let currentSource: SourceLocation | null = null; for (let i = 0; i < lines.length; i++) { const lineNum = i + 1; // 1-indexed const rawLine = lines[i]; - // Strip comment prefix - const inner = stripCommentPrefix(rawLine); + // Strip comment prefix unless this is a standalone .gal file, where + // annotations are stored as raw lines instead of host-language comments. + const inner = allowRawAnnotationLines ? rawLine : stripCommentPrefix(rawLine); if (inner === null) { lastAnnotation = null; continue; } + const text = inner.trimStart(); // Check for shield block boundaries — always parse these even inside shields - const trimmed = inner.trim(); + const trimmed = text.trim(); if (trimmed.startsWith('@shield:end')) { const location = { file: filePath, line: lineNum }; - const result = parseLine(inner, location); + const result = parseLine(text, location); if (result.annotation) annotations.push(result.annotation); inShield = false; lastAnnotation = null; @@ -58,7 +62,7 @@ export function parseString(content: string, filePath: string = ''): Pars } if (trimmed.startsWith('@shield:begin')) { const location = { file: filePath, line: lineNum }; - const result = parseLine(inner, location); + const result = parseLine(text, location); if (result.annotation) annotations.push(result.annotation); inShield = true; lastAnnotation = null; @@ -69,7 +73,7 @@ export function parseString(content: string, filePath: string = ''): Pars if (inShield) continue; // Check for continuation line: -- "..." - const contMatch = inner.match(/^--\s*"((?:[^"\\]|\\.)*)"/); + const contMatch = text.match(/^--\s*"((?:[^"\\]|\\.)*)"/); if (contMatch && lastAnnotation) { // Append to last annotation's description const contDesc = unescapeDescription(contMatch[1]); @@ -83,9 +87,28 @@ export function parseString(content: string, filePath: string = ''): Pars // Try to parse as annotation const location = { file: filePath, line: lineNum }; - const result = parseLine(inner, location); + const result = parseLine(text, location); + + if (result.sourceDirective) { + currentSource = { + file: result.sourceDirective.file, + line: result.sourceDirective.line, + parent_symbol: result.sourceDirective.symbol ?? null, + }; + lastAnnotation = null; + continue; + } if (result.annotation) { + if (allowRawAnnotationLines && currentSource) { + result.annotation.location = { + file: currentSource.file, + line: currentSource.line, + parent_symbol: currentSource.parent_symbol ?? null, + origin_file: filePath, + origin_line: lineNum, + }; + } annotations.push(result.annotation); lastAnnotation = result.annotation; } else { diff --git a/src/parser/parse-line.ts b/src/parser/parse-line.ts index c205c47..eb2c792 100644 --- a/src/parser/parse-line.ts +++ b/src/parser/parse-line.ts @@ -24,6 +24,9 @@ const THREAT_REF = String.raw`(?:#[a-zA-Z0-9_-]+|[A-Za-z]\w*(?:[_\- ][A-Za-z]\w* const SEVERITY = String.raw`\[(P[0-3]|critical|high|medium|low)\]`; const EXT_REF = String.raw`([a-zA-Z]+:[A-Za-z0-9_:.\-]+)`; const DESC = String.raw`--\s*"((?:[^"\\]|\\.)*)"`; +const SOURCE_FILE = String.raw`\S+`; +const SOURCE_LINE = String.raw`[1-9]\d*`; +const SOURCE_SYMBOL = String.raw`\S+`; // Capture external refs (0 or more, space-separated) const EXT_REFS_OPT = String.raw`((?:\s+[a-zA-Z]+:[A-Za-z0-9_:.\-]+)*)`; @@ -59,6 +62,9 @@ const PATTERNS: Record = { // Comment — developer note, description only comment: new RegExp(String.raw`^@comment(?:\s+${DESC})?$`), + // Standalone .gal directive — sets logical source location for following annotations + source: new RegExp(String.raw`^@source\s+file:(${SOURCE_FILE})\s+line:(${SOURCE_LINE})(?:\s+symbol:(${SOURCE_SYMBOL}))?$`), + // Special shield: new RegExp(String.raw`^@shield(?!:)(?:\s+${DESC})?$`), shield_begin: new RegExp(String.raw`^@shield:begin(?:\s+${DESC})?$`), @@ -80,10 +86,17 @@ function resolveRef(ref: string): string { // ─── Main parser ───────────────────────────────────────────────────── +export interface SourceDirective { + file: string; + line: number; + symbol?: string; +} + export interface ParseLineResult { annotation: Annotation | null; diagnostic: ParseDiagnostic | null; isContinuation: boolean; + sourceDirective?: SourceDirective | null; } /** @@ -102,9 +115,9 @@ export function parseLine( // Check for continuation line (-- "...") const contMatch = trimmed.match(new RegExp(String.raw`^${DESC}$`)); if (contMatch) { - return { annotation: null, diagnostic: null, isContinuation: true }; - } - return { annotation: null, diagnostic: null, isContinuation: false }; + return { annotation: null, diagnostic: null, isContinuation: true, sourceDirective: null }; + } + return { annotation: null, diagnostic: null, isContinuation: false, sourceDirective: null }; } const base = { location, raw: trimmed }; @@ -230,6 +243,20 @@ export function parseLine( return ok({ ...base, verb: 'comment', description: desc(m[1]) }); } + // ── @source ── + if ((m = trimmed.match(PATTERNS.source))) { + return { + annotation: null, + diagnostic: null, + isContinuation: false, + sourceDirective: { + file: m[1], + line: Number(m[2]), + symbol: m[3] || undefined, + }, + }; + } + // ── @shield ── if ((m = trimmed.match(PATTERNS.shield_begin))) { return ok({ ...base, verb: 'shield:begin', description: desc(m[1]) }); @@ -247,7 +274,7 @@ export function parseLine( const knownVerbs: Set = new Set([ 'asset', 'threat', 'control', 'mitigates', 'exposes', 'accepts', 'transfers', 'flows', 'boundary', 'validates', 'audit', 'owns', - 'handles', 'assumes', 'comment', 'shield', 'shield:begin', 'shield:end', + 'handles', 'assumes', 'comment', 'source', 'shield', 'shield:begin', 'shield:end', // v1 compat 'review', 'connects', ]); @@ -267,13 +294,13 @@ export function parseLine( } // Not a GuardLink annotation (could be @param, @returns, etc.) - return { annotation: null, diagnostic: null, isContinuation: false }; + return { annotation: null, diagnostic: null, isContinuation: false, sourceDirective: null }; } // ─── Helpers ───────────────────────────────────────────────────────── function ok(annotation: Annotation): ParseLineResult { - return { annotation, diagnostic: null, isContinuation: false }; + return { annotation, diagnostic: null, isContinuation: false, sourceDirective: null }; } function desc(raw: string | undefined): string | undefined { diff --git a/src/parser/parse-project.ts b/src/parser/parse-project.ts index b891378..0a463e7 100644 --- a/src/parser/parse-project.ts +++ b/src/parser/parse-project.ts @@ -8,11 +8,12 @@ * @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" + * @comment -- "Scans standalone .gal files in addition to comment-based source annotations" * @boundary #parser and FileSystem (#fs-boundary) -- "Trust boundary between parser and disk I/O" */ import fg from 'fast-glob'; -import { relative } from 'node:path'; +import { isAbsolute, relative } from 'node:path'; import type { Annotation, ThreatModel, ParseResult, ParseDiagnostic, AssetAnnotation, ThreatAnnotation, ControlAnnotation, @@ -51,6 +52,7 @@ const DEFAULT_INCLUDE = [ '**/*.html', '**/*.xml', '**/*.svg', '**/*.css', '**/*.ex', '**/*.exs', + '**/*.[gG][aA][lL]', ]; const DEFAULT_EXCLUDE = [ @@ -91,13 +93,19 @@ export async function parseProject(options: ParseProjectOptions): Promise<{ const relPath = relative(root, file); // Normalize file paths to relative for (const ann of result.annotations) { - ann.location.file = relPath; + ann.location.file = normalizeLocationPath(ann.location.file, file, root); + if (ann.location.origin_file) { + ann.location.origin_file = normalizeLocationPath(ann.location.origin_file, file, root); + } } for (const diag of result.diagnostics) { diag.file = relPath; } if (result.annotations.length > 0) { filesWithAnnotations.add(relPath); + for (const ann of result.annotations) { + filesWithAnnotations.add(ann.location.file); + } } allAnnotations.push(...result.annotations); allDiagnostics.push(...result.diagnostics); @@ -138,6 +146,12 @@ export async function parseProject(options: ParseProjectOptions): Promise<{ return { model, diagnostics: allDiagnostics }; } +function normalizeLocationPath(locationFile: string, physicalFile: string, root: string): string { + if (locationFile === physicalFile) return relative(root, physicalFile); + if (isAbsolute(locationFile)) return relative(root, locationFile); + return locationFile.replaceAll('\\', '/'); +} + function getAnnotationId(ann: Annotation): string | undefined { if ('id' in ann) return (ann as any).id; return undefined; diff --git a/src/review/index.ts b/src/review/index.ts index e84d3ef..03dbb78 100644 --- a/src/review/index.ts +++ b/src/review/index.ts @@ -16,8 +16,9 @@ */ import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { stripCommentPrefix } from '../parser/comment-strip.js'; +import { extname, resolve } from 'node:path'; +import { commentStyleForExt, stripCommentPrefix } from '../parser/comment-strip.js'; +import { parseLine } from '../parser/parse-line.js'; import { findUnmitigatedExposures } from '../parser/validate.js'; import type { ThreatModel, ThreatModelExposure, Severity } from '../types/index.js'; @@ -29,7 +30,7 @@ export interface ReviewableExposure { /** 1-based index in the review list */ index: number; exposure: ThreatModelExposure; - /** Stable ID for MCP: "file:line" */ + /** Stable ID for MCP */ id: string; } @@ -43,6 +44,8 @@ export interface ReviewResult { action: ReviewAction; /** Lines inserted into the file (empty for skip) */ linesInserted: number; + /** Physical file modified (or logical file for skip) */ + targetFile: string; } // ─── Severity ordering ────────────────────────────────────────────── @@ -78,7 +81,7 @@ export function getReviewableExposures(model: ThreatModel): ReviewableExposure[] return filtered.map((exposure, i) => ({ index: i + 1, exposure, - id: `${exposure.location.file}:${exposure.location.line}`, + id: reviewExposureId(exposure), })); } @@ -95,6 +98,8 @@ export function severityLabel(s?: Severity): string { interface CommentStyle { /** The prefix to use for new annotation lines */ prefix: string; + /** Optional suffix for single-line wrapper styles like */ + suffix: string; /** Indentation (leading whitespace) to match */ indent: string; } @@ -103,38 +108,60 @@ interface CommentStyle { * Detect the comment style and indentation from the @exposes source line. * Supports JSDoc ( * @...), single-line (// @...), and hash (# @...) styles. */ -function detectCommentStyle(rawLine: string): CommentStyle { +function detectCommentStyle(rawLine: string, filePath: string): CommentStyle { const indent = rawLine.match(/^(\s*)/)?.[1] || ''; const trimmed = rawLine.trimStart(); + if (trimmed.startsWith('@')) { + return { prefix: '', suffix: '', indent }; + } if (trimmed.startsWith('* @') || trimmed.startsWith('* @')) { - return { prefix: '* ', indent }; + return { prefix: '* ', suffix: '', indent }; } if (trimmed.startsWith('// @')) { - return { prefix: '// ', indent }; + return { prefix: '// ', suffix: '', indent }; } if (trimmed.startsWith('# @')) { - return { prefix: '# ', indent }; + return { prefix: '# ', suffix: '', indent }; } if (trimmed.startsWith('-- @')) { - return { prefix: '-- ', indent }; + return { prefix: '-- ', suffix: '', indent }; + } + if (trimmed.startsWith('', indent }; + } + if (trimmed.startsWith('/*')) { + return { prefix: '/* ', suffix: ' */', indent }; + } + + return fallbackCommentStyle(filePath, indent); +} + +function fallbackCommentStyle(filePath: string, indent: string): CommentStyle { + switch (commentStyleForExt(extname(filePath))) { + case '#': return { prefix: '# ', suffix: '', indent }; + case '--': return { prefix: '-- ', suffix: '', indent }; + case '', indent }; + case '/*': return { prefix: '/* ', suffix: ' */', indent }; + case '%': return { prefix: '% ', suffix: '', indent }; + case ';': return { prefix: '; ', suffix: '', indent }; + case 'REM': return { prefix: 'REM ', suffix: '', indent }; + case "'": return { prefix: "' ", suffix: '', indent }; + case '//': + default: + return { prefix: '// ', suffix: '', 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; + const rawTrimmed = line.trimStart(); + if (/^--\s*"/.test(rawTrimmed)) return true; + const inner = stripCommentPrefix(line) ?? rawTrimmed; + const parsed = parseLine(inner, { file: '', line: 1 }); + return Boolean(parsed.annotation || parsed.sourceDirective || parsed.isContinuation); } /** @@ -144,12 +171,15 @@ function isAnnotationLine(line: string): boolean { * 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 { +function findInsertionIndex(lines: string[], exposureLine: number, stopAtSourceBoundary: boolean = false): 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])) { + if (stopAtSourceBoundary && lines[idx + 1].trimStart().startsWith('@source')) { + break; + } idx++; } @@ -168,11 +198,11 @@ function todayISO(): string { * Returns lines WITHOUT trailing newline. */ function buildAcceptLines(style: CommentStyle, exposure: ThreatModelExposure, justification: string): string[] { - const { prefix, indent } = style; + const { prefix, suffix, 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}"`, + `${indent}${prefix}@accepts ${exposure.threat} on ${exposure.asset} -- "${escapeDesc(justification)}"${suffix}`, + `${indent}${prefix}@audit ${exposure.asset} -- "Accepted via guardlink review on ${date}"${suffix}`, ]; } @@ -180,10 +210,10 @@ function buildAcceptLines(style: CommentStyle, exposure: ThreatModelExposure, ju * Build the annotation line to insert for a "remediate" decision. */ function buildRemediateLines(style: CommentStyle, exposure: ThreatModelExposure, note: string): string[] { - const { prefix, indent } = style; + const { prefix, suffix, indent } = style; const date = todayISO(); return [ - `${indent}${prefix}@audit ${exposure.asset} -- "Planned remediation: ${escapeDesc(note)} — flagged via guardlink review on ${date}"`, + `${indent}${prefix}@audit ${exposure.asset} -- "Planned remediation: ${escapeDesc(note)} — flagged via guardlink review on ${date}"${suffix}`, ]; } @@ -205,17 +235,19 @@ async function insertAnnotations( exposure: ThreatModelExposure, newLines: string[], ): Promise { - const filePath = resolve(root, exposure.location.file); + const filePath = resolve(root, getWriteLocation(exposure).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 + const targetLocation = getWriteLocation(exposure); + const exposureIdx = targetLocation.line - 1; // 0-indexed if (exposureIdx < 0 || exposureIdx >= lines.length) { - throw new Error(`Line ${exposure.location.line} out of range in ${exposure.location.file}`); + throw new Error(`Line ${targetLocation.line} out of range in ${targetLocation.file}`); } - const insertIdx = findInsertionIndex(lines, exposure.location.line); + const style = detectCommentStyle(lines[exposureIdx], targetLocation.file); + const insertIdx = findInsertionIndex(lines, targetLocation.line, style.prefix === ''); // Splice in the new lines lines.splice(insertIdx, 0, ...newLines); @@ -240,17 +272,18 @@ export async function applyReviewAction( action: ReviewAction, ): Promise { if (action.decision === 'skip') { - return { exposure: reviewable, action, linesInserted: 0 }; + return { exposure: reviewable, action, linesInserted: 0, targetFile: getWriteLocation(reviewable.exposure).file }; } const { exposure } = reviewable; - const filePath = resolve(root, exposure.location.file); + const targetLocation = getWriteLocation(exposure); + const filePath = resolve(root, targetLocation.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]); + const exposureIdx = targetLocation.line - 1; + const style = detectCommentStyle(lines[exposureIdx], targetLocation.file); let newLines: string[]; if (action.decision === 'accept') { @@ -260,7 +293,26 @@ export async function applyReviewAction( } const linesInserted = await insertAnnotations(root, exposure, newLines); - return { exposure: reviewable, action, linesInserted }; + return { exposure: reviewable, action, linesInserted, targetFile: targetLocation.file }; +} + +function getWriteLocation(exposure: ThreatModelExposure): { file: string; line: number } { + return { + file: exposure.location.origin_file || exposure.location.file, + line: exposure.location.origin_line || exposure.location.line, + }; +} + +function reviewExposureId(exposure: ThreatModelExposure): string { + const writeLocation = getWriteLocation(exposure); + return [ + writeLocation.file, + String(writeLocation.line), + exposure.location.file, + String(exposure.location.line), + exposure.asset, + exposure.threat, + ].join(':'); } /** diff --git a/src/tui/commands.ts b/src/tui/commands.ts index 5fed1a6..8f5bacf 100644 --- a/src/tui/commands.ts +++ b/src/tui/commands.ts @@ -34,7 +34,7 @@ import { generateSarif } from '../analyzer/index.js'; import type { ThreatModel, ParseDiagnostic, ThreatModelExposure } from '../types/index.js'; import { C, severityBadge, severityText, severityTextPad, severityOrder, computeGrade, gradeColored, formatTable, readCodeContext, trunc, bar, fileLink, fileLinkTrunc, cleanCliArtifacts } from './format.js'; import { resolveLLMConfig, saveTuiConfig, loadTuiConfig } from './config.js'; -import { AGENTS, parseAgentFlag, launchAgent, launchAgentInline, copyToClipboard, buildAnnotatePrompt, type AgentEntry } from '../agents/index.js'; +import { AGENTS, parseAgentFlag, parseAnnotationModeFlag, 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'; import { loadWorkspaceConfig, linkProject, addToWorkspace, removeFromWorkspace, mergeReports, formatMergeSummary, diffMergedReports, formatDiffSummary, populateMetadata } from '../workspace/index.js'; @@ -159,10 +159,12 @@ export function cmdGal(): void { console.log(H(' GAL — GuardLink Annotation Language')); console.log(H(' ══════════════════════════════════════════════════════════')); console.log(''); - console.log(D(' Annotations live in source code comments. GuardLink parses')); - console.log(D(' them to build a live threat model from your codebase.')); + console.log(D(' Annotations live in source comments or standalone .gal files.')); + console.log(D(' GuardLink parses them into a live threat model for your codebase.')); console.log(''); console.log(D(' Syntax: @verb subject [preposition object] [-- "description"]')); + console.log(D(' Inline examples below use comment prefixes; raw .gal files use the same lines without // or #.')); + console.log(D(' In .gal files, use @source file: line: [symbol:] to anchor following annotations.')); console.log(''); // ── DEFINITIONS ────────────────────────────────────────────────── @@ -1472,11 +1474,16 @@ export function cmdThreatReports(ctx: TuiContext): void { // ─── /annotate ─────────────────────────────────────────────────────── export async function cmdAnnotate(args: string, ctx: TuiContext): Promise { - const { agent: flagAgent, cleanArgs } = parseAgentFlag(args); + const { mode: annotationMode, cleanArgs: argsWithoutMode, error: modeError } = parseAnnotationModeFlag(args); + if (modeError) { + console.log(C.warn(` ${modeError}`)); + return; + } + const { agent: flagAgent, cleanArgs } = parseAgentFlag(argsWithoutMode); if (!cleanArgs.trim()) { - console.log(C.warn(' Usage: /annotate [--claude-code|--codex|--gemini|--cursor|--windsurf|--clipboard]')); - console.log(C.dim(' Example: /annotate "annotate auth endpoints for OWASP Top 10" --claude-code')); + console.log(C.warn(' Usage: /annotate [--mode inline|external] [--claude-code|--codex|--gemini|--cursor|--windsurf|--clipboard]')); + console.log(C.dim(' Example: /annotate "annotate auth endpoints for OWASP Top 10" --mode external --claude-code')); return; } @@ -1487,7 +1494,7 @@ export async function cmdAnnotate(args: string, ctx: TuiContext): Promise if (!agent) return; // Build context prompt using shared builder - const prompt = buildAnnotatePrompt(cleanArgs.trim(), ctx.root, ctx.model); + const prompt = buildAnnotatePrompt(cleanArgs.trim(), ctx.root, ctx.model, annotationMode); // For terminal agents: foreground spawn (agent takes over terminal) if (agent.cmd) { @@ -1791,7 +1798,7 @@ export async function cmdReview(args: string, ctx: TuiContext): Promise { 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 }); + results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0, targetFile: reviewable.exposure.location.file }); console.log(` ${C.dim('— Skipped')}\n`); } } diff --git a/src/types/index.ts b/src/types/index.ts index b3b7e8e..859459e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -28,6 +28,8 @@ export interface SourceLocation { line: number; end_line?: number | null; parent_symbol?: string | null; + origin_file?: string | null; + origin_line?: number | null; } // ─── Parsed Annotations ────────────────────────────────────────────── diff --git a/tests/agents.test.ts b/tests/agents.test.ts new file mode 100644 index 0000000..3e24904 --- /dev/null +++ b/tests/agents.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from 'vitest'; +import { parseAnnotationModeFlag, resolveAnnotationMode } from '../src/agents/index.js'; + +describe('annotation mode parsing', () => { + it('parses --mode external', () => { + const parsed = parseAnnotationModeFlag('annotate auth --mode external --claude-code'); + expect(parsed.mode).toBe('external'); + expect(parsed.cleanArgs).toBe('annotate auth --claude-code'); + expect(parsed.error).toBeUndefined(); + }); + + it('parses --mode inline', () => { + const parsed = parseAnnotationModeFlag('annotate auth --mode inline --claude-code'); + expect(parsed.mode).toBe('inline'); + expect(parsed.cleanArgs).toBe('annotate auth --claude-code'); + expect(parsed.error).toBeUndefined(); + }); + + it('returns an error for invalid --mode values', () => { + const parsed = parseAnnotationModeFlag('annotate auth --mode wrong --claude-code'); + expect(parsed.error).toBe('Invalid --mode value. Use --mode inline or --mode external.'); + }); + + it('rejects invalid CLI mode values', () => { + expect(() => resolveAnnotationMode('wrong')).toThrow('Invalid annotation mode "wrong". Use "inline" or "external".'); + }); +}); diff --git a/tests/diff.test.ts b/tests/diff.test.ts new file mode 100644 index 0000000..bd7c919 --- /dev/null +++ b/tests/diff.test.ts @@ -0,0 +1,42 @@ +import { execSync } from 'node:child_process'; +import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseAtRef } from '../src/diff/index.js'; + +describe('parseAtRef', () => { + it('includes standalone .gal files from git refs', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-diff-')); + + try { + execSync('git init', { cwd: root, stdio: 'pipe' }); + execSync('git config user.name "GuardLink Tests"', { cwd: root, stdio: 'pipe' }); + execSync('git config user.email "guardlink-tests@example.com"', { cwd: root, stdio: 'pipe' }); + + await mkdir(join(root, '.guardlink', 'annotations'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Main API"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await writeFile( + join(root, '.guardlink', 'annotations', 'edge.ANNOTATIONS.GAL'), + '@exposes #api to #xss -- "Rendered HTML lacks output encoding"\n', + ); + + execSync('git add .', { cwd: root, stdio: 'pipe' }); + execSync('git commit -m "Add standalone GAL annotations"', { cwd: root, stdio: 'pipe' }); + + const model = await parseAtRef(root, 'HEAD', 'tmp'); + + expect(model.assets.map(a => a.id)).toContain('api'); + expect(model.exposures).toHaveLength(1); + expect(model.exposures[0].location.file).toBe('.guardlink/annotations/edge.ANNOTATIONS.GAL'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 8704fcc..37257c5 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from 'vitest'; +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { parseString } from '../src/parser/parse-file.js'; +import { parseProject } from '../src/parser/parse-project.js'; +import { clearAnnotations } from '../src/parser/clear.js'; import { normalizeName, resolveSeverity, unescapeDescription } from '../src/parser/normalize.js'; import { stripCommentPrefix } from '../src/parser/comment-strip.js'; import { findDanglingRefs, findUnmitigatedExposures } from '../src/parser/validate.js'; @@ -260,6 +265,53 @@ describe('parseString', () => { expect(annotations[0].verb).toBe('asset'); }); + it('parses raw standalone .gal files without comment prefixes', () => { + const { annotations } = parseString([ + '@asset App.Auth.Login (#login) -- "Externalized auth asset"', + '@threat Session_Hijacking (#session-hijack) [P1]', + '-- "Token theft through externalized annotations"', + '@exposes #login to #session-hijack -- "Session cookie lacks binding"', + ].join('\n'), 'annotations.gal'); + + expect(annotations).toHaveLength(3); + expect((annotations[0] as any).verb).toBe('asset'); + expect((annotations[1] as any).description).toBe('Token theft through externalized annotations'); + expect((annotations[2] as any).asset).toBe('#login'); + }); + + it('applies @source metadata to subsequent standalone .gal annotations', () => { + const { annotations } = parseString([ + '@source file:src/auth/login.ts line:42 symbol:authenticate', + '@exposes #login to #session-hijack -- "Session cookie lacks binding"', + '@audit #login -- "Review session issuance"', + '@source file:src/auth/session.ts line:88', + '@handles secrets on #login -- "Issues session token"', + ].join('\n'), 'annotations.gal'); + + expect(annotations).toHaveLength(3); + expect(annotations[0].location).toEqual({ + file: 'src/auth/login.ts', + line: 42, + parent_symbol: 'authenticate', + origin_file: 'annotations.gal', + origin_line: 2, + }); + expect(annotations[1].location).toEqual({ + file: 'src/auth/login.ts', + line: 42, + parent_symbol: 'authenticate', + origin_file: 'annotations.gal', + origin_line: 3, + }); + expect(annotations[2].location).toEqual({ + file: 'src/auth/session.ts', + line: 88, + parent_symbol: null, + origin_file: 'annotations.gal', + origin_line: 5, + }); + }); + // ── Error diagnostics ── it('reports malformed annotations', () => { @@ -326,6 +378,85 @@ describe('parseString', () => { expect(annotations).toHaveLength(1); expect(annotations[0].verb).toBe('shield'); }); + + it('scans standalone .gal files during project parsing', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-gal-')); + + try { + await mkdir(join(root, '.guardlink'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Standalone API asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await mkdir(join(root, '.guardlink', 'annotations'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'annotations', 'annotations.GAL'), + [ + '@source file:src/api.ts line:12 symbol:renderProfile', + '@exposes #api to #xss -- "Rendered HTML lacks output encoding"', + ].join('\n'), + ); + await mkdir(join(root, 'src'), { recursive: true }); + await writeFile(join(root, 'src', 'api.ts'), 'export const ok = true;\n'); + + const { model, diagnostics } = await parseProject({ root, project: 'tmp' }); + + expect(diagnostics).toHaveLength(0); + expect(model.annotations_parsed).toBe(3); + expect(model.assets.map(a => a.id)).toContain('api'); + expect(model.exposures).toHaveLength(1); + expect(model.exposures[0].location).toEqual({ + file: 'src/api.ts', + line: 12, + parent_symbol: 'renderProfile', + origin_file: '.guardlink/annotations/annotations.GAL', + origin_line: 2, + }); + expect(model.annotated_files).toContain('.guardlink/definitions.ts'); + expect(model.annotated_files).toContain('.guardlink/annotations/annotations.GAL'); + expect(model.annotated_files).toContain('src/api.ts'); + expect(model.unannotated_files).not.toContain('src/api.ts'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('clears raw standalone .gal annotations while preserving definitions by default', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-clear-gal-')); + + try { + await mkdir(join(root, '.guardlink'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Standalone API asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await mkdir(join(root, '.guardlink', 'annotations'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'annotations', 'annotations.gal'), + [ + '@source file:src/api.ts line:12 symbol:renderProfile', + '@exposes #api to #xss [high] -- "Rendered HTML lacks output encoding"', + '-- "Needs manual review"', + '@audit #api -- "Review before public release"', + ].join('\n'), + ); + + const result = await clearAnnotations({ root }); + + expect(result.modifiedFiles).toContain('.guardlink/annotations/annotations.gal'); + expect(result.modifiedFiles).not.toContain('.guardlink/definitions.ts'); + expect(await readFile(join(root, '.guardlink', 'annotations', 'annotations.gal'), 'utf-8')).toBe(''); + expect(await readFile(join(root, '.guardlink', 'definitions.ts'), 'utf-8')).toContain('@asset App.API'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); }); // ─── Validation: findDanglingRefs ───────────────────────────────────── diff --git a/tests/prompts.test.ts b/tests/prompts.test.ts new file mode 100644 index 0000000..b07cdee --- /dev/null +++ b/tests/prompts.test.ts @@ -0,0 +1,34 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { buildAnnotatePrompt } from '../src/agents/prompts.js'; + +describe('buildAnnotatePrompt', () => { + it('builds inline-specific guidance', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-prompt-inline-')); + + try { + const prompt = buildAnnotatePrompt('annotate auth code', root, null, 'inline'); + expect(prompt).toContain('This run MUST produce annotations as inline source comments.'); + expect(prompt).toContain('You MUST write annotations inline in the source code comments.'); + expect(prompt).toContain('Do NOT externalize annotations into `.gal` files when this mode is selected'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('builds external-specific guidance', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-prompt-external-')); + + try { + const prompt = buildAnnotatePrompt('annotate auth code', root, null, 'external'); + expect(prompt).toContain('This run MUST produce annotations as externalized .gal files.'); + expect(prompt).toContain('You MUST write annotations into associated standalone `.gal` files'); + expect(prompt).toContain('.guardlink/annotations/'); + expect(prompt).toContain('@source file: line: [symbol:]'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); diff --git a/tests/review.test.ts b/tests/review.test.ts new file mode 100644 index 0000000..293338b --- /dev/null +++ b/tests/review.test.ts @@ -0,0 +1,241 @@ +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { parseProject } from '../src/parser/parse-project.js'; +import { applyReviewAction, getReviewableExposures } from '../src/review/index.js'; + +describe('review with externalized annotations', () => { + it('writes review annotations back to the .gal source file', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-review-gal-')); + + try { + await mkdir(join(root, '.guardlink', 'annotations'), { recursive: true }); + await mkdir(join(root, 'src'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Standalone API asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await writeFile( + join(root, '.guardlink', 'annotations', 'security.gal'), + [ + '@source file:src/api.ts line:1 symbol:renderProfile', + '@exposes #api to #xss [high] -- "Rendered HTML lacks output encoding"', + ].join('\n'), + ); + await writeFile(join(root, 'src', 'api.ts'), 'export function renderProfile() { return "
"; }\n'); + + const { model } = await parseProject({ root, project: 'tmp' }); + const [reviewable] = getReviewableExposures(model); + expect(reviewable).toBeDefined(); + + await applyReviewAction(root, reviewable, { + decision: 'remediate', + justification: 'Add output encoding before release', + }); + + const galContent = await readFile(join(root, '.guardlink', 'annotations', 'security.gal'), 'utf-8'); + const sourceContent = await readFile(join(root, 'src', 'api.ts'), 'utf-8'); + + expect(galContent).toContain('@audit #api -- "Planned remediation: Add output encoding before release'); + expect(sourceContent).not.toContain('@audit'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('does not cross into the next @source block during review writeback', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-review-source-block-')); + + try { + await mkdir(join(root, '.guardlink', 'annotations'), { recursive: true }); + await mkdir(join(root, 'src'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Standalone API asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await writeFile( + join(root, '.guardlink', 'annotations', 'security.gal'), + [ + '@source file:src/api.ts line:1 symbol:firstHandler', + '@exposes #api to #xss [high] -- "First exposure"', + '@source file:src/api.ts line:10 symbol:secondHandler', + '@exposes #api to #xss [high] -- "Second exposure"', + ].join('\n'), + ); + await writeFile(join(root, 'src', 'api.ts'), 'export const api = true;\n'); + + const { model } = await parseProject({ root, project: 'tmp' }); + const [firstReviewable] = getReviewableExposures(model); + + await applyReviewAction(root, firstReviewable, { + decision: 'remediate', + justification: 'Fix the first block only', + }); + + const galLines = (await readFile(join(root, '.guardlink', 'annotations', 'security.gal'), 'utf-8')).split('\n'); + const firstAuditIndex = galLines.findIndex(line => line.includes('Fix the first block only')); + const secondSourceIndex = galLines.findIndex(line => line.startsWith('@source file:src/api.ts line:10')); + + expect(firstAuditIndex).toBeGreaterThan(0); + expect(firstAuditIndex).toBeLessThan(secondSourceIndex); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('assigns unique review IDs to multiple exposures at the same logical source location', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-review-ids-')); + + try { + await mkdir(join(root, '.guardlink', 'annotations'), { recursive: true }); + await mkdir(join(root, 'src'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Standalone API asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + '// @threat Info_Disclosure (#info-disclosure) [low] cwe:CWE-200 -- "Sensitive output"', + ].join('\n'), + ); + await writeFile( + join(root, '.guardlink', 'annotations', 'security.gal'), + [ + '@source file:src/api.ts line:1 symbol:renderProfile', + '@exposes #api to #xss [high] -- "First exposure"', + '@exposes #api to #info-disclosure [low] -- "Second exposure"', + ].join('\n'), + ); + await writeFile(join(root, 'src', 'api.ts'), 'export function renderProfile() { return "
"; }\n'); + + const { model } = await parseProject({ root, project: 'tmp' }); + const reviewables = getReviewableExposures(model); + + expect(reviewables).toHaveLength(2); + expect(new Set(reviewables.map(r => r.id)).size).toBe(2); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('does not treat decorators as GuardLink annotations during writeback', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-review-decorator-')); + + try { + await mkdir(join(root, '.guardlink'), { recursive: true }); + await mkdir(join(root, 'src'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.API (#api) -- "Decorator-backed API asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await writeFile( + join(root, 'src', 'api.ts'), + [ + '// @exposes #api to #xss [high] -- "Decorator-backed endpoint"', + '@Component()', + 'export class ApiController {}', + ].join('\n'), + ); + + const { model } = await parseProject({ root, project: 'tmp' }); + const [reviewable] = getReviewableExposures(model); + await applyReviewAction(root, reviewable, { + decision: 'remediate', + justification: 'Keep remediation above decorator', + }); + + const sourceLines = (await readFile(join(root, 'src', 'api.ts'), 'utf-8')).split('\n'); + expect(sourceLines[1]).toContain('@audit #api'); + expect(sourceLines[2]).toBe('@Component()'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it('uses valid HTML comment syntax when writing back review annotations', async () => { + const root = await mkdtemp(join(tmpdir(), 'guardlink-review-html-')); + + try { + await mkdir(join(root, '.guardlink'), { recursive: true }); + await mkdir(join(root, 'src'), { recursive: true }); + await writeFile( + join(root, '.guardlink', 'definitions.ts'), + [ + '// @asset App.Web (#web) -- "HTML rendering asset"', + '// @threat XSS (#xss) [high] cwe:CWE-79 -- "Unescaped output"', + ].join('\n'), + ); + await writeFile( + join(root, 'src', 'index.html'), + [ + '', + '
Hello
', + ].join('\n'), + ); + + const { model } = await parseProject({ root, project: 'tmp' }); + const [reviewable] = getReviewableExposures(model); + await applyReviewAction(root, reviewable, { + decision: 'remediate', + justification: 'Escape output before rendering', + }); + + const htmlContent = await readFile(join(root, 'src', 'index.html'), 'utf-8'); + expect(htmlContent).toContain('