guardlink 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +11 -2
  3. package/dist/agents/config.d.ts +17 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js +38 -4
  6. package/dist/agents/config.js.map +1 -1
  7. package/dist/agents/index.d.ts +5 -1
  8. package/dist/agents/index.d.ts.map +1 -1
  9. package/dist/agents/index.js +4 -1
  10. package/dist/agents/index.js.map +1 -1
  11. package/dist/agents/launcher.d.ts +25 -8
  12. package/dist/agents/launcher.d.ts.map +1 -1
  13. package/dist/agents/launcher.js +137 -9
  14. package/dist/agents/launcher.js.map +1 -1
  15. package/dist/agents/prompts.d.ts +9 -0
  16. package/dist/agents/prompts.d.ts.map +1 -1
  17. package/dist/agents/prompts.js +43 -6
  18. package/dist/agents/prompts.js.map +1 -1
  19. package/dist/analyze/index.d.ts +44 -8
  20. package/dist/analyze/index.d.ts.map +1 -1
  21. package/dist/analyze/index.js +291 -15
  22. package/dist/analyze/index.js.map +1 -1
  23. package/dist/analyze/llm.d.ts +65 -13
  24. package/dist/analyze/llm.d.ts.map +1 -1
  25. package/dist/analyze/llm.js +429 -107
  26. package/dist/analyze/llm.js.map +1 -1
  27. package/dist/analyze/prompts.d.ts +6 -2
  28. package/dist/analyze/prompts.d.ts.map +1 -1
  29. package/dist/analyze/prompts.js +230 -111
  30. package/dist/analyze/prompts.js.map +1 -1
  31. package/dist/analyze/tools.d.ts +28 -0
  32. package/dist/analyze/tools.d.ts.map +1 -0
  33. package/dist/analyze/tools.js +236 -0
  34. package/dist/analyze/tools.js.map +1 -0
  35. package/dist/analyzer/index.d.ts +3 -0
  36. package/dist/analyzer/index.d.ts.map +1 -1
  37. package/dist/analyzer/index.js +3 -0
  38. package/dist/analyzer/index.js.map +1 -1
  39. package/dist/analyzer/sarif.d.ts +5 -6
  40. package/dist/analyzer/sarif.d.ts.map +1 -1
  41. package/dist/analyzer/sarif.js +5 -6
  42. package/dist/analyzer/sarif.js.map +1 -1
  43. package/dist/cli/index.d.ts +27 -16
  44. package/dist/cli/index.d.ts.map +1 -1
  45. package/dist/cli/index.js +524 -105
  46. package/dist/cli/index.js.map +1 -1
  47. package/dist/dashboard/data.d.ts +5 -0
  48. package/dist/dashboard/data.d.ts.map +1 -1
  49. package/dist/dashboard/data.js +5 -0
  50. package/dist/dashboard/data.js.map +1 -1
  51. package/dist/dashboard/generate.d.ts +8 -5
  52. package/dist/dashboard/generate.d.ts.map +1 -1
  53. package/dist/dashboard/generate.js +206 -66
  54. package/dist/dashboard/generate.js.map +1 -1
  55. package/dist/dashboard/index.d.ts +5 -0
  56. package/dist/dashboard/index.d.ts.map +1 -1
  57. package/dist/dashboard/index.js +5 -0
  58. package/dist/dashboard/index.js.map +1 -1
  59. package/dist/diff/git.d.ts +10 -7
  60. package/dist/diff/git.d.ts.map +1 -1
  61. package/dist/diff/git.js +10 -7
  62. package/dist/diff/git.js.map +1 -1
  63. package/dist/diff/index.d.ts +4 -0
  64. package/dist/diff/index.d.ts.map +1 -1
  65. package/dist/diff/index.js +4 -0
  66. package/dist/diff/index.js.map +1 -1
  67. package/dist/init/detect.d.ts +5 -0
  68. package/dist/init/detect.d.ts.map +1 -1
  69. package/dist/init/detect.js +5 -0
  70. package/dist/init/detect.js.map +1 -1
  71. package/dist/init/index.d.ts +26 -6
  72. package/dist/init/index.d.ts.map +1 -1
  73. package/dist/init/index.js +91 -11
  74. package/dist/init/index.js.map +1 -1
  75. package/dist/init/picker.d.ts.map +1 -1
  76. package/dist/init/picker.js +17 -6
  77. package/dist/init/picker.js.map +1 -1
  78. package/dist/init/templates.d.ts +20 -0
  79. package/dist/init/templates.d.ts.map +1 -1
  80. package/dist/init/templates.js +167 -36
  81. package/dist/init/templates.js.map +1 -1
  82. package/dist/mcp/index.d.ts +5 -0
  83. package/dist/mcp/index.d.ts.map +1 -1
  84. package/dist/mcp/index.js +5 -0
  85. package/dist/mcp/index.js.map +1 -1
  86. package/dist/mcp/lookup.d.ts +5 -0
  87. package/dist/mcp/lookup.d.ts.map +1 -1
  88. package/dist/mcp/lookup.js +5 -0
  89. package/dist/mcp/lookup.js.map +1 -1
  90. package/dist/mcp/server.d.ts +16 -13
  91. package/dist/mcp/server.d.ts.map +1 -1
  92. package/dist/mcp/server.js +140 -17
  93. package/dist/mcp/server.js.map +1 -1
  94. package/dist/mcp/suggest.d.ts +8 -6
  95. package/dist/mcp/suggest.d.ts.map +1 -1
  96. package/dist/mcp/suggest.js +8 -6
  97. package/dist/mcp/suggest.js.map +1 -1
  98. package/dist/parser/clear.d.ts +36 -0
  99. package/dist/parser/clear.d.ts.map +1 -0
  100. package/dist/parser/clear.js +148 -0
  101. package/dist/parser/clear.js.map +1 -0
  102. package/dist/parser/index.d.ts +3 -1
  103. package/dist/parser/index.d.ts.map +1 -1
  104. package/dist/parser/index.js +2 -1
  105. package/dist/parser/index.js.map +1 -1
  106. package/dist/parser/parse-file.d.ts +5 -2
  107. package/dist/parser/parse-file.d.ts.map +1 -1
  108. package/dist/parser/parse-file.js +29 -2
  109. package/dist/parser/parse-file.js.map +1 -1
  110. package/dist/parser/parse-line.d.ts +3 -3
  111. package/dist/parser/parse-line.js +3 -3
  112. package/dist/parser/parse-project.d.ts +7 -7
  113. package/dist/parser/parse-project.d.ts.map +1 -1
  114. package/dist/parser/parse-project.js +24 -11
  115. package/dist/parser/parse-project.js.map +1 -1
  116. package/dist/parser/validate.d.ts +12 -0
  117. package/dist/parser/validate.d.ts.map +1 -1
  118. package/dist/parser/validate.js +44 -0
  119. package/dist/parser/validate.js.map +1 -1
  120. package/dist/report/index.d.ts +3 -0
  121. package/dist/report/index.d.ts.map +1 -1
  122. package/dist/report/index.js +3 -0
  123. package/dist/report/index.js.map +1 -1
  124. package/dist/report/report.d.ts +4 -7
  125. package/dist/report/report.d.ts.map +1 -1
  126. package/dist/report/report.js +68 -7
  127. package/dist/report/report.js.map +1 -1
  128. package/dist/review/index.d.ts +62 -0
  129. package/dist/review/index.d.ts.map +1 -0
  130. package/dist/review/index.js +226 -0
  131. package/dist/review/index.js.map +1 -0
  132. package/dist/tui/commands.d.ts +26 -1
  133. package/dist/tui/commands.d.ts.map +1 -1
  134. package/dist/tui/commands.js +608 -101
  135. package/dist/tui/commands.js.map +1 -1
  136. package/dist/tui/config.d.ts +6 -0
  137. package/dist/tui/config.d.ts.map +1 -1
  138. package/dist/tui/config.js +6 -0
  139. package/dist/tui/config.js.map +1 -1
  140. package/dist/tui/format.d.ts +7 -0
  141. package/dist/tui/format.d.ts.map +1 -1
  142. package/dist/tui/format.js +59 -0
  143. package/dist/tui/format.js.map +1 -1
  144. package/dist/tui/index.d.ts +8 -8
  145. package/dist/tui/index.d.ts.map +1 -1
  146. package/dist/tui/index.js +47 -10
  147. package/dist/tui/index.js.map +1 -1
  148. package/dist/tui/input.d.ts +6 -0
  149. package/dist/tui/input.d.ts.map +1 -1
  150. package/dist/tui/input.js +6 -0
  151. package/dist/tui/input.js.map +1 -1
  152. package/dist/types/index.d.ts +2 -0
  153. package/dist/types/index.d.ts.map +1 -1
  154. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -3,37 +3,49 @@
3
3
  * GuardLink CLI — Reference Implementation
4
4
  *
5
5
  * Usage:
6
- * guardlink init [dir] Initialize GuardLink in a project
7
- * guardlink parse [dir] Parse annotations, output ThreatModel JSON
8
- * guardlink status [dir] Show annotation coverage summary
9
- * guardlink validate [dir] Check for syntax errors and dangling refs
10
- * guardlink analyze [framework] AI-powered threat analysis (STRIDE, DREAD, etc.)
11
- * guardlink annotate <prompt> Launch coding agent for annotation
12
- * guardlink config <action> Manage LLM provider configuration
6
+ * guardlink init [dir] Initialize GuardLink in a project
7
+ * guardlink parse [dir] Parse annotations, output ThreatModel JSON
8
+ * guardlink status [dir] Show annotation coverage summary
9
+ * guardlink validate [dir] Check for syntax errors and dangling refs
10
+ * guardlink report [dir] Generate markdown + JSON threat model report
11
+ * guardlink diff [ref] Compare threat model against a git ref
12
+ * guardlink sarif [dir] Export SARIF 2.1.0 for GitHub / VS Code
13
+ * guardlink threat-report <prompt> AI-powered threat analysis (STRIDE, DREAD, PASTA, etc.)
14
+ * guardlink threat-reports List saved AI threat reports
15
+ * guardlink annotate <prompt> Launch coding agent to add annotations
16
+ * guardlink config <action> Manage LLM provider configuration
17
+ * guardlink dashboard [dir] Generate interactive HTML dashboard
18
+ * guardlink mcp Start MCP server (stdio) for Claude Code, Cursor, etc.
19
+ * guardlink tui [dir] Interactive TUI with slash commands + AI chat
20
+ * guardlink gal Display GAL annotation language quick reference
13
21
  *
14
- * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "Accepts directory paths from command line arguments"
15
- * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes reports and SARIF to user-specified output paths"
16
- * @accepts #arbitrary-write on #cli -- "Intentional feature: users specify output paths for reports"
17
- * @mitigates #cli against #path-traversal using #path-validation -- "resolve() normalizes paths before passing to submodules"
18
- * @boundary between #cli and #parser (#cli-parser-boundary) -- "CLI is the primary user input trust boundary"
19
- * @flows User -> #cli via argv -- "User provides directory paths and options via command line"
20
- * @flows #cli -> #parser via parseProject -- "CLI dispatches parsed commands to parser"
21
- * @flows #cli -> #report via generateReport -- "CLI writes report output"
22
- * @flows #cli -> #init via initProject -- "CLI initializes project structure"
22
+ * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "User-supplied dir argument resolved via path.resolve"
23
+ * @mitigates #cli against #path-traversal using #path-validation -- "resolve() canonicalizes paths; cwd-relative by design"
24
+ * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "init/report/sarif/dashboard write files to user-specified paths"
25
+ * @mitigates #cli against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root"
26
+ * @exposes #cli to #api-key-exposure [high] cwe:CWE-798 -- "API keys handled in config set/show commands"
27
+ * @mitigates #cli against #api-key-exposure using #key-redaction -- "maskKey() redacts keys in show output"
28
+ * @exposes #cli to #cmd-injection [critical] cwe:CWE-78 -- "Agent launcher spawns child processes"
29
+ * @audit #cli -- "Child process spawning delegated to agents/launcher.ts with explicit args"
30
+ * @flows UserArgs -> #cli via process.argv -- "CLI argument input path"
31
+ * @flows #cli -> FileSystem via writeFile -- "Report/config output path"
32
+ * @boundary #cli and UserInput (#cli-input-boundary) -- "Trust boundary at CLI argument parsing"
33
+ * @handles secrets on #cli -- "Processes API keys via config commands"
23
34
  */
24
35
  import { Command } from 'commander';
25
36
  import { resolve, basename } from 'node:path';
26
- import { readFileSync, existsSync } from 'node:fs';
27
- import { parseProject, findDanglingRefs, findUnmitigatedExposures } from '../parser/index.js';
28
- import { initProject, detectProject, promptAgentSelection } from '../init/index.js';
37
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
38
+ import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } from '../parser/index.js';
39
+ import { initProject, detectProject, promptAgentSelection, syncAgentFiles } from '../init/index.js';
29
40
  import { generateReport, generateMermaid } from '../report/index.js';
30
41
  import { diffModels, formatDiff, formatDiffMarkdown, parseAtRef } from '../diff/index.js';
31
42
  import { generateSarif } from '../analyzer/index.js';
32
43
  import { startStdioServer } from '../mcp/index.js';
33
44
  import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage } from '../analyze/index.js';
34
45
  import { generateDashboardHTML } from '../dashboard/index.js';
35
- import { AGENTS, agentFromOpts, launchAgent, buildAnnotatePrompt } from '../agents/index.js';
46
+ import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt } from '../agents/index.js';
36
47
  import { resolveConfig, saveProjectConfig, saveGlobalConfig, loadProjectConfig, loadGlobalConfig, maskKey, describeConfigSource } from '../agents/config.js';
48
+ import { getReviewableExposures, applyReviewAction, formatExposureForReview, summarizeReview } from '../review/index.js';
37
49
  import gradient from 'gradient-string';
38
50
  const program = new Command();
39
51
  const ASCII_LOGO = `
@@ -178,11 +190,22 @@ program
178
190
  .description('Show annotation coverage summary')
179
191
  .argument('[dir]', 'Project directory to scan', '.')
180
192
  .option('-p, --project <n>', 'Project name', 'unknown')
193
+ .option('--not-annotated', 'List source files with no GuardLink annotations')
181
194
  .action(async (dir, opts) => {
182
195
  const root = resolve(dir);
183
196
  const { model, diagnostics } = await parseProject({ root, project: opts.project });
184
197
  printDiagnostics(diagnostics);
185
198
  printStatus(model);
199
+ if (opts.notAnnotated) {
200
+ printUnannotatedFiles(model);
201
+ }
202
+ // Auto-sync agent instruction files with updated model
203
+ if (model.annotations_parsed > 0) {
204
+ const syncResult = syncAgentFiles({ root, model });
205
+ if (syncResult.updated.length > 0) {
206
+ console.error(`↻ Synced ${syncResult.updated.length} agent instruction file(s)`);
207
+ }
208
+ }
186
209
  });
187
210
  // ─── validate ────────────────────────────────────────────────────────
188
211
  program
@@ -196,9 +219,13 @@ program
196
219
  const { model, diagnostics } = await parseProject({ root, project: opts.project });
197
220
  // Check for dangling refs
198
221
  const danglingDiags = findDanglingRefs(model);
199
- const allDiags = [...diagnostics, ...danglingDiags];
222
+ // Check for @accepts without @audit (governance concern)
223
+ const acceptAuditDiags = findAcceptedWithoutAudit(model);
224
+ const allDiags = [...diagnostics, ...danglingDiags, ...acceptAuditDiags];
200
225
  // Check for unmitigated exposures
201
226
  const unmitigated = findUnmitigatedExposures(model);
227
+ // Check for accepted-but-unmitigated exposures (risk acceptance without real controls)
228
+ const acceptedOnly = findAcceptedExposures(model);
202
229
  printDiagnostics(allDiags);
203
230
  if (unmitigated.length > 0) {
204
231
  console.error(`\n⚠ ${unmitigated.length} unmitigated exposure(s):`);
@@ -206,14 +233,30 @@ program
206
233
  console.error(` ${u.asset} → ${u.threat} [${u.severity || 'unset'}] (${u.location.file}:${u.location.line})`);
207
234
  }
208
235
  }
236
+ if (acceptedOnly.length > 0) {
237
+ console.error(`\n⚡ ${acceptedOnly.length} accepted-but-unmitigated exposure(s) (risk accepted, no control in code):`);
238
+ for (const a of acceptedOnly) {
239
+ console.error(` ${a.asset} → ${a.threat} [${a.severity || 'unset'}] (${a.location.file}:${a.location.line})`);
240
+ }
241
+ }
209
242
  const errorCount = allDiags.filter(d => d.level === 'error').length;
210
243
  const hasUnmitigated = unmitigated.length > 0;
211
- if (errorCount === 0 && !hasUnmitigated) {
244
+ if (errorCount === 0 && !hasUnmitigated && acceptedOnly.length === 0) {
212
245
  console.error('\n✓ All annotations valid, no unmitigated exposures.');
213
246
  }
247
+ else if (errorCount === 0 && !hasUnmitigated && acceptedOnly.length > 0) {
248
+ console.error(`\nValidation passed. ${acceptedOnly.length} exposure(s) accepted without mitigation — ensure these are intentional human decisions.`);
249
+ }
214
250
  else if (errorCount === 0 && hasUnmitigated) {
215
251
  console.error(`\nValidation passed with ${unmitigated.length} unmitigated exposure(s).`);
216
252
  }
253
+ // Auto-sync agent instruction files with updated model
254
+ if (model.annotations_parsed > 0) {
255
+ const syncResult = syncAgentFiles({ root, model });
256
+ if (syncResult.updated.length > 0) {
257
+ console.error(`↻ Synced ${syncResult.updated.length} agent instruction file(s)`);
258
+ }
259
+ }
217
260
  // Exit 1 on errors always; also on unmitigated if --strict
218
261
  process.exit(errorCount > 0 || (opts.strict && hasUnmitigated) ? 1 : 0);
219
262
  });
@@ -340,32 +383,33 @@ program
340
383
  // ─── threat-report ───────────────────────────────────────────────────
341
384
  program
342
385
  .command('threat-report')
343
- .description('Generate an AI threat report using a security framework (STRIDE, DREAD, PASTA, etc.)')
344
- .argument('[framework]', 'Framework: stride, dread, pasta, attacker, rapid, general', 'general')
345
- .argument('[dir]', 'Project directory', '.')
386
+ .description('Generate an AI threat report using a framework or custom prompt')
387
+ .argument('[prompt...]', 'Framework (stride, dread, pasta, attacker, rapid, general) or custom prompt text')
388
+ .option('-d, --dir <dir>', 'Project directory', '.')
346
389
  .option('-p, --project <n>', 'Project name', 'unknown')
347
- .option('--provider <provider>', 'LLM provider: anthropic, openai, openrouter, deepseek (auto-detected from env)')
390
+ .option('--provider <provider>', 'LLM provider: anthropic, openai, google, openrouter, deepseek (auto-detected from env)')
348
391
  .option('--model <model>', 'Model name (default: provider-specific)')
349
392
  .option('--api-key <key>', 'API key (default: from env variable)')
350
393
  .option('--no-stream', 'Disable streaming output')
351
- .option('--custom <prompt>', 'Custom analysis prompt (replaces framework prompt header)')
352
- .option('--claude-code', 'Launch Claude Code in foreground')
353
- .option('--codex', 'Launch Codex CLI in foreground')
354
- .option('--gemini', 'Launch Gemini CLI in foreground')
394
+ .option('--web-search', 'Enable web search grounding (OpenAI only)')
395
+ .option('--thinking', 'Enable extended thinking / reasoning (Anthropic, DeepSeek only)')
396
+ .option('--claude-code', 'Run via Claude Code (inline)')
397
+ .option('--codex', 'Run via Codex CLI (inline)')
398
+ .option('--gemini', 'Run via Gemini CLI (inline)')
355
399
  .option('--cursor', 'Open Cursor IDE with prompt on clipboard')
356
400
  .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard')
357
401
  .option('--clipboard', 'Copy threat report prompt to clipboard only')
358
- .action(async (framework, dir, opts) => {
359
- const root = resolve(dir);
402
+ .action(async (promptParts, opts) => {
403
+ const root = resolve(opts.dir);
360
404
  const project = detectProjectName(root, opts.project);
361
- // Validate framework
405
+ const input = promptParts.join(' ').trim();
406
+ // Determine framework vs custom prompt
362
407
  const validFrameworks = ['stride', 'dread', 'pasta', 'attacker', 'rapid', 'general'];
363
- if (!validFrameworks.includes(framework)) {
364
- console.error(`Unknown framework: ${framework}`);
365
- console.error(`Available: ${validFrameworks.join(', ')}`);
366
- process.exit(1);
367
- }
368
- const fw = framework;
408
+ const inputLower = input.toLowerCase();
409
+ const isStandard = validFrameworks.includes(inputLower);
410
+ const fw = (isStandard ? inputLower : 'general');
411
+ const customPrompt = isStandard ? undefined : (input || undefined);
412
+ const reportLabel = customPrompt ? 'Custom Threat Analysis' : FRAMEWORK_LABELS[fw];
369
413
  // Parse project
370
414
  const { model, diagnostics } = await parseProject({ root, project });
371
415
  const errors = diagnostics.filter(d => d.level === 'error');
@@ -375,34 +419,74 @@ program
375
419
  console.error('No annotations found. Run: guardlink init . && add annotations first.');
376
420
  process.exit(1);
377
421
  }
378
- // Resolve agent (same pattern as annotate)
379
- const agent = agentFromOpts(opts);
380
- // ── Agent path: build prompt, launch agent ──
381
- if (agent) {
382
- const serialized = serializeModel(model);
383
- const systemPrompt = FRAMEWORK_PROMPTS[fw] || FRAMEWORK_PROMPTS.general;
384
- const userMessage = buildUserMessage(serialized, fw, opts.custom);
385
- const fullPrompt = `${systemPrompt}\n\n${userMessage}\n\nAlso read the source files to understand code context. Save the report to .guardlink/threat-reports/ as a markdown file.`;
386
- console.log(`Generating ${FRAMEWORK_LABELS[fw]} via ${agent.name}...`);
387
- if (agent.cmd) {
388
- console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`);
389
- }
390
- const result = launchAgent(agent, fullPrompt, root);
391
- if (result.clipboardCopied) {
392
- console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`);
422
+ // Build analysis prompt (shared by agent and API paths)
423
+ const serialized = serializeModel(model);
424
+ const { buildProjectContext, extractCodeSnippets } = await import('../analyze/index.js');
425
+ const projectContext = buildProjectContext(root);
426
+ const codeSnippets = extractCodeSnippets(root, model);
427
+ const systemPrompt = FRAMEWORK_PROMPTS[fw];
428
+ const userMessage = buildUserMessage(serialized, fw, customPrompt, projectContext || undefined, codeSnippets || undefined);
429
+ const analysisPrompt = `You are analyzing a codebase with GuardLink security annotations.
430
+ You have access to the full source code in the current directory.
431
+
432
+ ${systemPrompt}
433
+
434
+ ## Task
435
+ Read the source code and GuardLink annotations, then produce a thorough ${reportLabel}.
436
+
437
+ ## Threat Model (serialized from annotations)
438
+ ${userMessage}
439
+
440
+ ## Instructions
441
+ 1. Read the actual source files to understand the code — don't just rely on the serialized model above
442
+ 2. Cross-reference the annotations with the real code to validate findings
443
+ 3. Produce the full report as markdown
444
+ 4. Be specific — reference actual files, functions, and line numbers from the codebase
445
+ 5. Output ONLY the markdown report content — do NOT add any metadata comments, save confirmations, or file path messages
446
+ 6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."`;
447
+ // Resolve agent: explicit flag > project config CLI agent
448
+ let agent = agentFromOpts(opts);
449
+ if (!agent) {
450
+ const projCfg = loadProjectConfig(root);
451
+ if (projCfg?.aiMode === 'cli-agent' && projCfg?.cliAgent) {
452
+ agent = AGENTS.find(a => a.id === projCfg.cliAgent) || null;
393
453
  }
454
+ }
455
+ // ── Path 1: CLI Agent (inline, non-interactive) ──
456
+ if (agent && agent.cmd) {
457
+ console.error(`\n🔍 ${reportLabel}`);
458
+ console.error(` Agent: ${agent.name} (inline)`);
459
+ console.error(` Annotations: ${model.annotations_parsed} | Exposures: ${model.exposures.length}\n`);
460
+ const result = await launchAgentInline(agent, analysisPrompt, root, (text) => process.stdout.write(text), { autoYes: true });
394
461
  if (result.error) {
395
- console.error(`✗ ${result.error}`);
396
- if (result.clipboardCopied) {
397
- console.log('Prompt is on your clipboard — paste it manually.');
398
- }
462
+ console.error(`\n✗ ${result.error}`);
399
463
  process.exit(1);
400
464
  }
401
- if (agent.cmd && result.launched) {
402
- console.log(`\n✓ ${agent.name} session ended.`);
403
- console.log(' Run: guardlink threat-reports to see saved reports.');
465
+ process.stdout.write('\n');
466
+ // Save the agent's output as a report
467
+ if (result.content.trim()) {
468
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
469
+ const reportsDir = resolve(root, '.guardlink', 'threat-reports');
470
+ if (!existsSync(reportsDir))
471
+ mkdirSync(reportsDir, { recursive: true });
472
+ const filename = `${timestamp}-${fw}.md`;
473
+ const filepath = resolve(reportsDir, filename);
474
+ // Clean ANSI codes and CLI artifacts from the output before saving
475
+ const { cleanCliArtifacts } = await import('../tui/format.js');
476
+ const cleanedContent = cleanCliArtifacts(result.content);
477
+ const header = `---\nframework: ${fw}\nlabel: ${FRAMEWORK_LABELS[fw]}\nmodel: ${agent.name}\ntimestamp: ${new Date().toISOString()}\nproject: ${project}\nannotations: ${model.annotations_parsed}\n---\n\n# ${FRAMEWORK_LABELS[fw]}\n\n> Generated by \`guardlink threat-report ${fw}\` on ${new Date().toISOString().slice(0, 10)}\n> Agent: ${agent.name} | Project: ${project} | Annotations: ${model.annotations_parsed}\n\n`;
478
+ writeFileSync(filepath, header + cleanedContent + '\n');
479
+ console.error(`\n✓ Report saved to .guardlink/threat-reports/${filename}`);
404
480
  }
405
- else if (agent.app && result.launched) {
481
+ return;
482
+ }
483
+ // ── Path 2: Clipboard / IDE agent ──
484
+ if (agent && !agent.cmd) {
485
+ const result = launchAgent(agent, analysisPrompt, root);
486
+ if (result.clipboardCopied) {
487
+ console.log(`✓ Prompt copied to clipboard (${analysisPrompt.length.toLocaleString()} chars)`);
488
+ }
489
+ if (result.launched && agent.app) {
406
490
  console.log(`✓ ${agent.name} launched with project: ${project}`);
407
491
  console.log('\nPaste (Cmd+V) the prompt in the AI chat panel.');
408
492
  console.log('When done, run: guardlink threat-reports');
@@ -411,26 +495,26 @@ program
411
495
  console.log('\nPaste the prompt into your preferred AI tool.');
412
496
  console.log('When done, run: guardlink threat-reports');
413
497
  }
498
+ else if (result.error) {
499
+ console.error(`✗ ${result.error}`);
500
+ process.exit(1);
501
+ }
414
502
  return;
415
503
  }
416
- // ── API path: direct LLM call (no agent flag) ──
504
+ // ── Path 3: Direct API call ──
417
505
  const llmConfig = buildConfig({
418
506
  provider: opts.provider,
419
507
  model: opts.model,
420
508
  apiKey: opts.apiKey,
421
- });
509
+ }) || resolveConfig(root);
422
510
  if (!llmConfig) {
423
- // No agent, no API key show usage like annotate does
424
- console.error('No agent or API key specified. Use one of:');
425
- for (const a of AGENTS) {
426
- console.error(` ${a.flag.padEnd(16)} ${a.name}`);
427
- }
428
- console.error('');
429
- console.error('Or set an API key: ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.');
430
- console.error('Or use: --provider anthropic --api-key sk-...');
511
+ console.error('No AI provider configured. Use one of:');
512
+ console.error(' guardlink config Configure API provider');
513
+ console.error(' --claude-code / --codex Use a CLI agent');
514
+ console.error(' ANTHROPIC_API_KEY=... Set env var');
431
515
  process.exit(1);
432
516
  }
433
- console.error(`\n🔍 ${FRAMEWORK_LABELS[fw]}`);
517
+ console.error(`\n🔍 ${reportLabel}`);
434
518
  console.error(` Provider: ${llmConfig.provider} | Model: ${llmConfig.model}`);
435
519
  console.error(` Annotations: ${model.annotations_parsed} | Exposures: ${model.exposures.length}\n`);
436
520
  try {
@@ -439,9 +523,11 @@ program
439
523
  model,
440
524
  framework: fw,
441
525
  llmConfig,
442
- customPrompt: opts.custom,
526
+ customPrompt,
443
527
  stream: opts.stream !== false,
444
528
  onChunk: opts.stream !== false ? (text) => process.stdout.write(text) : undefined,
529
+ webSearch: opts.webSearch,
530
+ extendedThinking: opts.thinking,
445
531
  });
446
532
  if (opts.stream !== false) {
447
533
  process.stdout.write('\n');
@@ -453,6 +539,9 @@ program
453
539
  if (result.inputTokens || result.outputTokens) {
454
540
  console.error(` Tokens: ${result.inputTokens || '?'} in / ${result.outputTokens || '?'} out`);
455
541
  }
542
+ if (result.thinkingTokens) {
543
+ console.error(` Thinking: ${result.thinkingTokens} tokens`);
544
+ }
456
545
  }
457
546
  catch (err) {
458
547
  console.error(`\n✗ Threat report generation failed: ${err.message}`);
@@ -547,12 +636,203 @@ program
547
636
  console.log('When done, run: guardlink parse');
548
637
  }
549
638
  });
639
+ // ─── clear ───────────────────────────────────────────────────────────
640
+ program
641
+ .command('clear')
642
+ .description('Remove all GuardLink annotations from source files — start fresh')
643
+ .argument('[dir]', 'Project directory', '.')
644
+ .option('--dry-run', 'Show what would be removed without modifying files')
645
+ .option('--include-definitions', 'Also clear .guardlink/definitions files')
646
+ .option('-y, --yes', 'Skip confirmation prompt')
647
+ .action(async (dir, opts) => {
648
+ const root = resolve(dir);
649
+ // First, show what will be cleared
650
+ const preview = await clearAnnotations({
651
+ root,
652
+ dryRun: true,
653
+ includeDefinitions: opts.includeDefinitions,
654
+ });
655
+ if (preview.totalRemoved === 0) {
656
+ console.log('No GuardLink annotations found in source files.');
657
+ return;
658
+ }
659
+ console.log(`\nFound ${preview.totalRemoved} annotation line(s) across ${preview.modifiedFiles.length} file(s):\n`);
660
+ for (const [file, count] of preview.perFile) {
661
+ console.log(` ${file} (${count} line${count > 1 ? 's' : ''})`);
662
+ }
663
+ console.log('');
664
+ if (opts.dryRun) {
665
+ console.log('(dry run) No files were modified.');
666
+ return;
667
+ }
668
+ // Confirmation prompt
669
+ if (!opts.yes) {
670
+ if (!process.stdin.isTTY) {
671
+ console.error('Use --yes to confirm in non-interactive mode.');
672
+ process.exit(1);
673
+ }
674
+ const readline = await import('node:readline');
675
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
676
+ const answer = await new Promise(resolve => {
677
+ rl.question('⚠ This will remove all annotations from source files. Continue? (y/N): ', resolve);
678
+ });
679
+ rl.close();
680
+ if (answer.trim().toLowerCase() !== 'y') {
681
+ console.log('Cancelled.');
682
+ return;
683
+ }
684
+ }
685
+ // Actually clear
686
+ const result = await clearAnnotations({
687
+ root,
688
+ dryRun: false,
689
+ includeDefinitions: opts.includeDefinitions,
690
+ });
691
+ console.log(`\n✓ Removed ${result.totalRemoved} annotation line(s) from ${result.modifiedFiles.length} file(s).`);
692
+ console.log(' Run: guardlink annotate to re-annotate from scratch.');
693
+ });
694
+ // ─── sync ────────────────────────────────────────────────────────────
695
+ program
696
+ .command('sync')
697
+ .description('Sync agent instruction files with current threat model — keeps ALL coding agents up to date')
698
+ .argument('[dir]', 'Project directory', '.')
699
+ .option('--dry-run', 'Show what would be updated without modifying files')
700
+ .action(async (dir, opts) => {
701
+ const root = resolve(dir);
702
+ // Parse the current model
703
+ const { model } = await parseProject({ root, project: basename(root) });
704
+ if (model.annotations_parsed === 0) {
705
+ console.log('No annotations found. Run: guardlink annotate to add annotations first.');
706
+ console.log('Syncing agent files with base instructions (no model context)...\n');
707
+ }
708
+ const result = syncAgentFiles({ root, model, dryRun: opts.dryRun });
709
+ if (result.updated.length > 0) {
710
+ console.log(`${opts.dryRun ? '(dry run) Would update' : '✓ Updated'} ${result.updated.length} agent instruction file(s):\n`);
711
+ for (const f of result.updated) {
712
+ console.log(` ${f}`);
713
+ }
714
+ }
715
+ if (result.skipped.length > 0) {
716
+ console.log(`\nSkipped: ${result.skipped.join(', ')}`);
717
+ }
718
+ if (!opts.dryRun && model.annotations_parsed > 0) {
719
+ console.log(`\n✓ All agent instruction files now include live threat model context.`);
720
+ console.log(` ${model.assets.length} assets, ${model.threats.length} threats, ${model.controls.length} controls, ${model.exposures.length} exposures.`);
721
+ console.log(' Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) will see these IDs.');
722
+ }
723
+ });
724
+ // ─── unannotated ─────────────────────────────────────────────────────
725
+ program
726
+ .command('unannotated')
727
+ .description('List source files with no GuardLink annotations')
728
+ .argument('[dir]', 'Project directory to scan', '.')
729
+ .option('-p, --project <n>', 'Project name', 'unknown')
730
+ .action(async (dir, opts) => {
731
+ const root = resolve(dir);
732
+ const { model } = await parseProject({ root, project: opts.project });
733
+ printUnannotatedFiles(model);
734
+ });
735
+ // ─── review ──────────────────────────────────────────────────────────
736
+ program
737
+ .command('review')
738
+ .description('Interactive governance review of unmitigated exposures — accept, remediate, or skip')
739
+ .argument('[dir]', 'Project directory to scan', '.')
740
+ .option('-p, --project <n>', 'Project name', 'unknown')
741
+ .option('--severity <levels>', 'Filter by severity: critical,high,medium,low', undefined)
742
+ .option('--list', 'Just list reviewable exposures without prompting')
743
+ .action(async (dir, opts) => {
744
+ const root = resolve(dir);
745
+ const { model } = await parseProject({ root, project: opts.project });
746
+ let exposures = getReviewableExposures(model);
747
+ // Filter by severity if requested
748
+ if (opts.severity) {
749
+ const allowed = new Set(opts.severity.split(',').map(s => s.trim().toLowerCase()));
750
+ exposures = exposures.filter(e => allowed.has(e.exposure.severity || 'low'));
751
+ // Re-index after filtering
752
+ exposures = exposures.map((e, i) => ({ ...e, index: i + 1 }));
753
+ }
754
+ if (exposures.length === 0) {
755
+ console.error('✓ No unmitigated exposures to review.');
756
+ return;
757
+ }
758
+ // List-only mode
759
+ if (opts.list) {
760
+ console.error(`\n${exposures.length} unmitigated exposure(s):\n`);
761
+ for (const r of exposures) {
762
+ const e = r.exposure;
763
+ console.error(` ${r.index}. ${e.asset} → ${e.threat} [${e.severity || '?'}] (${e.location.file}:${e.location.line})`);
764
+ }
765
+ console.error('');
766
+ return;
767
+ }
768
+ // Interactive review
769
+ const { createInterface } = await import('node:readline');
770
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
771
+ const ask = (q) => new Promise(resolve => rl.question(q, resolve));
772
+ console.error(`\n guardlink review — ${exposures.length} unmitigated exposure(s)\n`);
773
+ const results = [];
774
+ for (const reviewable of exposures) {
775
+ console.error(formatExposureForReview(reviewable, exposures.length));
776
+ console.error('');
777
+ console.error(' (a) Accept — risk acknowledged and intentional');
778
+ console.error(' (r) Remediate — mark as planned fix');
779
+ console.error(' (s) Skip — leave open for now');
780
+ console.error(' (q) Quit review');
781
+ console.error('');
782
+ const choice = (await ask(' Choice [a/r/s/q]: ')).trim().toLowerCase();
783
+ if (choice === 'q') {
784
+ console.error('\n Review ended.\n');
785
+ break;
786
+ }
787
+ if (choice === 'a') {
788
+ let justification = '';
789
+ while (!justification) {
790
+ justification = (await ask(' Justification (required): ')).trim();
791
+ if (!justification)
792
+ console.error(' ⚠ Justification is mandatory for acceptance.');
793
+ }
794
+ const result = await applyReviewAction(root, reviewable, { decision: 'accept', justification });
795
+ results.push(result);
796
+ console.error(` ✓ Accepted — ${result.linesInserted} line(s) written to ${reviewable.exposure.location.file}\n`);
797
+ }
798
+ else if (choice === 'r') {
799
+ let note = '';
800
+ while (!note) {
801
+ note = (await ask(' Remediation note (required): ')).trim();
802
+ if (!note)
803
+ console.error(' ⚠ Remediation note is mandatory.');
804
+ }
805
+ const result = await applyReviewAction(root, reviewable, { decision: 'remediate', justification: note });
806
+ results.push(result);
807
+ console.error(` ✓ Marked for remediation — ${result.linesInserted} line(s) written to ${reviewable.exposure.location.file}\n`);
808
+ }
809
+ else {
810
+ results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0 });
811
+ console.error(' — Skipped\n');
812
+ }
813
+ }
814
+ rl.close();
815
+ if (results.length > 0) {
816
+ console.error(summarizeReview(results));
817
+ // Auto-sync agent files if any annotations were written
818
+ if (results.some(r => r.linesInserted > 0)) {
819
+ try {
820
+ // Re-parse to get updated model
821
+ const { model: newModel } = await parseProject({ root, project: opts.project });
822
+ const syncResult = syncAgentFiles({ root, model: newModel });
823
+ if (syncResult.updated.length > 0)
824
+ console.error(`↻ Synced ${syncResult.updated.length} agent instruction file(s)`);
825
+ }
826
+ catch { }
827
+ }
828
+ }
829
+ });
550
830
  // ─── config ──────────────────────────────────────────────────────────
551
831
  program
552
832
  .command('config')
553
833
  .description('Manage LLM provider configuration')
554
834
  .argument('<action>', 'Action: set, show, clear')
555
- .argument('[key]', 'Config key: provider, api-key, model')
835
+ .argument('[key]', 'Config key: provider, api-key, model, ai-mode, cli-agent')
556
836
  .argument('[value]', 'Value to set')
557
837
  .option('--global', 'Use global config (~/.config/guardlink/) instead of project')
558
838
  .action(async (action, key, value, opts) => {
@@ -562,17 +842,24 @@ program
562
842
  case 'show': {
563
843
  const config = resolveConfig(root);
564
844
  const source = describeConfigSource(root);
845
+ const projCfg = isGlobal ? loadGlobalConfig() : loadProjectConfig(root);
846
+ const aiMode = projCfg?.aiMode || 'api';
847
+ const cliAgent = projCfg?.cliAgent;
848
+ console.log(`AI Mode: ${aiMode}${cliAgent ? ` (${cliAgent})` : ''}`);
565
849
  if (config) {
566
850
  console.log(`Provider: ${config.provider}`);
567
851
  console.log(`Model: ${config.model}`);
568
852
  console.log(`API Key: ${maskKey(config.apiKey)}`);
569
853
  console.log(`Source: ${source}`);
570
854
  }
571
- else {
855
+ else if (aiMode !== 'cli-agent') {
572
856
  console.log('No LLM configuration found.');
573
857
  console.log('\nSet one with:');
574
858
  console.log(' guardlink config set provider anthropic');
575
859
  console.log(' guardlink config set api-key sk-ant-...');
860
+ console.log('\nOr use a CLI agent:');
861
+ console.log(' guardlink config set ai-mode cli-agent');
862
+ console.log(' guardlink config set cli-agent claude-code');
576
863
  console.log('\nOr set environment variables:');
577
864
  console.log(' export GUARDLINK_LLM_KEY=sk-ant-...');
578
865
  console.log(' export GUARDLINK_LLM_PROVIDER=anthropic');
@@ -582,17 +869,19 @@ program
582
869
  case 'set': {
583
870
  if (!key || !value) {
584
871
  console.error('Usage: guardlink config set <key> <value>');
585
- console.error('Keys: provider, api-key, model');
872
+ console.error('Keys: provider, api-key, model, ai-mode, cli-agent');
586
873
  process.exit(1);
587
874
  }
588
875
  const existing = isGlobal
589
876
  ? loadGlobalConfig() || {}
590
877
  : loadProjectConfig(root) || {};
878
+ const validProviders = ['anthropic', 'openai', 'google', 'openrouter', 'deepseek', 'ollama'];
879
+ const validAgentIds = AGENTS.map(a => a.id);
591
880
  switch (key) {
592
881
  case 'provider':
593
- if (!['anthropic', 'openai', 'openrouter', 'deepseek'].includes(value)) {
882
+ if (!validProviders.includes(value)) {
594
883
  console.error(`Unknown provider: ${value}`);
595
- console.error('Available: anthropic, openai, openrouter, deepseek');
884
+ console.error(`Available: ${validProviders.join(', ')}`);
596
885
  process.exit(1);
597
886
  }
598
887
  existing.provider = value;
@@ -603,8 +892,25 @@ program
603
892
  case 'model':
604
893
  existing.model = value;
605
894
  break;
895
+ case 'ai-mode':
896
+ if (!['api', 'cli-agent'].includes(value)) {
897
+ console.error(`Unknown ai-mode: ${value}`);
898
+ console.error('Available: api, cli-agent');
899
+ process.exit(1);
900
+ }
901
+ existing.aiMode = value;
902
+ break;
903
+ case 'cli-agent':
904
+ if (!validAgentIds.includes(value)) {
905
+ console.error(`Unknown cli-agent: ${value}`);
906
+ console.error(`Available: ${validAgentIds.join(', ')}`);
907
+ process.exit(1);
908
+ }
909
+ existing.cliAgent = value;
910
+ existing.aiMode = 'cli-agent';
911
+ break;
606
912
  default:
607
- console.error(`Unknown config key: ${key}. Use: provider, api-key, model`);
913
+ console.error(`Unknown config key: ${key}. Use: provider, api-key, model, ai-mode, cli-agent`);
608
914
  process.exit(1);
609
915
  }
610
916
  if (isGlobal) {
@@ -676,7 +982,7 @@ program
676
982
  .command('tui')
677
983
  .description('Interactive TUI — slash commands, AI chat, exposure triage')
678
984
  .argument('[dir]', 'project directory', '.')
679
- .option('--provider <provider>', 'LLM provider for this session (anthropic, openai, openrouter, deepseek)')
985
+ .option('--provider <provider>', 'LLM provider for this session (anthropic, openai, google, openrouter, deepseek)')
680
986
  .option('--api-key <key>', 'LLM API key for this session (not persisted)')
681
987
  .option('--model <model>', 'LLM model override')
682
988
  .action(async (dir, opts) => {
@@ -693,31 +999,132 @@ program
693
999
  .description('Display GuardLink Annotation Language (GAL) quick reference')
694
1000
  .action(() => {
695
1001
  import('chalk').then(({ default: c }) => {
1002
+ const H = (s) => c.bold.cyan(s);
1003
+ const V = (s) => c.bold.cyanBright(s);
1004
+ const K = (s) => c.yellow(s);
1005
+ const D = (s) => c.dim(s);
1006
+ const EX = (s) => c.green(s);
696
1007
  console.log(gradient(['#00ff41', '#00d4ff'])(ASCII_LOGO));
697
- console.log(`${c.bold.bgCyan.black(' GUARDLINK ANNOTATION LANGUAGE (GAL) ')}\n`);
698
- console.log(`${c.bold('Syntax:')}`);
699
- console.log(` // @verb <args> [qualifiers] [refs] -- "description"\n`);
700
- console.log(`${c.bold('Definition Verbs:')}`);
701
- console.log(` ${c.green('@asset')} <path> (#id) ${c.gray('Declare a component')}`);
702
- console.log(` ${c.green('@threat')} <name> (#id) [sev] ${c.gray('Declare a threat')}`);
703
- console.log(` ${c.green('@control')} <name> (#id) ${c.gray('Declare a security control')}\n`);
704
- console.log(`${c.bold('Relationship Verbs:')}`);
705
- console.log(` ${c.green('@mitigates')} <asset> against <threat> using <control>`);
706
- console.log(` ${c.green('@exposes')} <asset> to <threat> [severity]`);
707
- console.log(` ${c.green('@flows')} <source> -> <target> via <mechanism>`);
708
- console.log(` ${c.green('@boundary')} between <asset-a> and <asset-b> (#id)\n`);
709
- console.log(`${c.bold('Lifecycle & Metadata:')}`);
710
- console.log(` ${c.green('@handles')} <data> on <asset> ${c.gray('Data classification')}`);
711
- console.log(` ${c.green('@owns')} <owner> for <asset> ${c.gray('Security ownership')}`);
712
- console.log(` ${c.green('@assumes')} <asset> ${c.gray('Security assumption')}`);
713
- console.log(` ${c.green('@shield')} [-- "reason"] ${c.gray('AI exclusion marker')}\n`);
714
- console.log(`${c.bold('Severity Levels:')}`);
715
- console.log(` [critical] | [high] | [medium] | [low]`);
716
- console.log(` [P0] | [P1] | [P2] | [P3]\n`);
717
- console.log(`${c.bold('Data Classifications:')}`);
718
- console.log(` pii | secrets | financial | phi | internal | public\n`);
719
- console.log(`${c.bold('Example:')}`);
720
- console.log(` ${c.gray('// @mitigates #api against #sqli using #prepared-stmts -- "Parameterized query"')}\n`);
1008
+ console.log('');
1009
+ console.log(H(' ══════════════════════════════════════════════════════════'));
1010
+ console.log(H(' GAL GuardLink Annotation Language'));
1011
+ console.log(H(' ══════════════════════════════════════════════════════════'));
1012
+ console.log('');
1013
+ console.log(D(' Annotations live in source code comments. GuardLink parses'));
1014
+ console.log(D(' them to build a live threat model from your codebase.'));
1015
+ console.log('');
1016
+ console.log(D(' Syntax: @verb subject [preposition object] [: description]'));
1017
+ console.log('');
1018
+ // ── DEFINITIONS ──
1019
+ console.log(H(' ── Definitions ─────────────────────────────────────────────'));
1020
+ console.log('');
1021
+ console.log(` ${V('@asset')} ${K('<path>')} ${D('[: description]')}`);
1022
+ console.log(D(' Declare a named asset (component, service, data store).'));
1023
+ console.log(D(' Path uses dot notation for hierarchy.'));
1024
+ console.log(EX(' // @asset api.auth.token_store : Stores JWT refresh tokens'));
1025
+ console.log(EX(' // @asset db.users'));
1026
+ console.log('');
1027
+ console.log(` ${V('@threat')} ${K('<name>')} ${D('[severity: critical|high|medium|low] [: description]')}`);
1028
+ console.log(D(' Declare a named threat. Severity aliases: P0=critical P1=high P2=medium P3=low.'));
1029
+ console.log(EX(' // @threat SQL Injection severity:high : Unsanitized input reaches DB'));
1030
+ console.log(EX(' // @threat Token Theft severity:P0'));
1031
+ console.log('');
1032
+ console.log(` ${V('@control')} ${K('<name>')} ${D('[: description]')}`);
1033
+ console.log(D(' Declare a security control (mitigation mechanism).'));
1034
+ console.log(EX(' // @control Input Validation : Sanitize all user-supplied strings'));
1035
+ console.log(EX(' // @control Rate Limiting'));
1036
+ console.log('');
1037
+ // ── RELATIONSHIPS ──
1038
+ console.log(H(' ── Relationships ───────────────────────────────────────────'));
1039
+ console.log('');
1040
+ console.log(` ${V('@exposes')} ${K('<asset>')} ${D('to')} ${K('<threat>')} ${D('[severity: ...] [: description]')}`);
1041
+ console.log(D(' Mark an asset as exposed to a threat at this code location.'));
1042
+ console.log(D(' This is the primary annotation — every exposure creates a finding.'));
1043
+ console.log(EX(' // @exposes api.auth to SQL Injection severity:high'));
1044
+ console.log(EX(' // @exposes db.users to Token Theft severity:critical : No token rotation'));
1045
+ console.log('');
1046
+ console.log(` ${V('@mitigates')} ${K('<asset>')} ${D('against')} ${K('<threat>')} ${D('[with')} ${K('<control>')}${D('] [: description]')}`);
1047
+ console.log(D(' Mark that a control mitigates a threat on an asset.'));
1048
+ console.log(D(' Closes the exposure — removes it from open findings.'));
1049
+ console.log(EX(' // @mitigates api.auth against SQL Injection with Input Validation'));
1050
+ console.log(EX(' // @mitigates db.users against Token Theft : Rotation implemented in v2'));
1051
+ console.log('');
1052
+ console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[: reason]')}`);
1053
+ console.log(D(' Explicitly accept a risk. Removes it from open findings.'));
1054
+ console.log(D(' Use when the risk is known and intentionally not mitigated.'));
1055
+ console.log(EX(' // @accepts Timing Attack on api.auth : Acceptable for current threat model'));
1056
+ console.log('');
1057
+ console.log(` ${V('@transfers')} ${K('<threat>')} ${D('from')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[: description]')}`);
1058
+ console.log(D(' Transfer responsibility for a threat to another asset/team.'));
1059
+ console.log(EX(' // @transfers DDoS from api.gateway to cdn.cloudflare : Handled by CDN layer'));
1060
+ console.log('');
1061
+ // ── DATA FLOWS ──
1062
+ console.log(H(' ── Data Flows & Boundaries ─────────────────────────────────'));
1063
+ console.log('');
1064
+ console.log(` ${V('@flows')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[via')} ${K('<mechanism>')}${D('] [: description]')}`);
1065
+ console.log(D(' Document data movement between components.'));
1066
+ console.log(D(' Appears in the Data Flow Diagram.'));
1067
+ console.log(EX(' // @flows api.auth to db.users via TLS 1.3'));
1068
+ console.log(EX(' // @flows mobile.app to api.gateway via HTTPS : User credentials'));
1069
+ console.log('');
1070
+ console.log(` ${V('@boundary')} ${K('<asset_a>')} ${D('and')} ${K('<asset_b>')} ${D('[: description]')}`);
1071
+ console.log(D(' Declare a trust boundary between two assets.'));
1072
+ console.log(D(' Groups assets in the Data Flow Diagram.'));
1073
+ console.log(EX(' // @boundary internet and api.gateway : Public-facing edge'));
1074
+ console.log(EX(' // @boundary api.gateway and db.users : Internal network boundary'));
1075
+ console.log('');
1076
+ // ── LIFECYCLE ──
1077
+ console.log(H(' ── Lifecycle & Governance ──────────────────────────────────'));
1078
+ console.log('');
1079
+ console.log(` ${V('@handles')} ${K('<classification>')} ${D('on')} ${K('<asset>')} ${D('[: description]')}`);
1080
+ console.log(D(' Declare data classification handled by an asset.'));
1081
+ console.log(D(' Classifications: pii phi financial secrets internal public'));
1082
+ console.log(EX(' // @handles pii on db.users : Stores name, email, phone'));
1083
+ console.log(EX(' // @handles secrets on api.auth.token_store'));
1084
+ console.log('');
1085
+ console.log(` ${V('@owns')} ${K('<owner>')} ${K('<asset>')} ${D('[: description]')}`);
1086
+ console.log(D(' Assign ownership of an asset to a team or person.'));
1087
+ console.log(EX(' // @owns platform-team api.auth'));
1088
+ console.log('');
1089
+ console.log(` ${V('@validates')} ${K('<control>')} ${D('on')} ${K('<asset>')} ${D('[: description]')}`);
1090
+ console.log(D(' Assert that a control has been validated/tested on an asset.'));
1091
+ console.log(EX(' // @validates Input Validation on api.auth : Pen-tested 2024-Q3'));
1092
+ console.log('');
1093
+ console.log(` ${V('@audit')} ${K('<asset>')} ${D('[: description]')}`);
1094
+ console.log(D(' Mark that this code path is an audit trail point.'));
1095
+ console.log(EX(' // @audit db.users : All writes logged to audit_log table'));
1096
+ console.log('');
1097
+ console.log(` ${V('@assumes')} ${K('<asset>')} ${D('[: description]')}`);
1098
+ console.log(D(' Document a security assumption about an asset.'));
1099
+ console.log(EX(' // @assumes api.gateway : Upstream WAF filters malformed requests'));
1100
+ console.log('');
1101
+ console.log(` ${V('@comment')} ${D('[: description]')}`);
1102
+ console.log(D(' Free-form developer security note (no structural effect).'));
1103
+ console.log(EX(' // @comment : TODO — add rate limiting before v2 launch'));
1104
+ console.log('');
1105
+ // ── SHIELD BLOCKS ──
1106
+ console.log(H(' ── Shield Blocks ───────────────────────────────────────────'));
1107
+ console.log('');
1108
+ console.log(` ${V('@shield:begin')} ${D('/')} ${V('@shield:end')}`);
1109
+ console.log(D(' Wrap a code block to mark it as security-sensitive.'));
1110
+ console.log(D(' GuardLink will flag unannotated symbols inside the block.'));
1111
+ console.log(EX(' // @shield:begin'));
1112
+ console.log(EX(' function verifyToken(token: string) { ... }'));
1113
+ console.log(EX(' // @shield:end'));
1114
+ console.log('');
1115
+ // ── TIPS ──
1116
+ console.log(H(' ── Tips ────────────────────────────────────────────────────'));
1117
+ console.log('');
1118
+ console.log(D(' • Annotations work in any comment style: // /* # -- <!-- -->'));
1119
+ console.log(D(' • Place annotations on the line ABOVE the code they describe'));
1120
+ console.log(D(' • Asset names are case-insensitive and normalized (spaces→underscores)'));
1121
+ console.log(D(' • Threat/control names can reference IDs with #id syntax'));
1122
+ console.log(D(' • Run guardlink parse after adding annotations to update the threat model'));
1123
+ console.log(D(' • Run guardlink validate to check for syntax errors and dangling references'));
1124
+ console.log(D(' • Run guardlink annotate to have an AI agent add annotations automatically'));
1125
+ console.log('');
1126
+ console.log(H(' ══════════════════════════════════════════════════════════'));
1127
+ console.log('');
721
1128
  });
722
1129
  });
723
1130
  // If no subcommand given, launch TUI
@@ -745,6 +1152,8 @@ function printStatus(model) {
745
1152
  console.log(`GuardLink Status: ${model.project}`);
746
1153
  console.log(`${'─'.repeat(40)}`);
747
1154
  console.log(`Files scanned: ${model.source_files}`);
1155
+ console.log(` Annotated: ${model.annotated_files.length}`);
1156
+ console.log(` Not annotated: ${model.unannotated_files.length}`);
748
1157
  console.log(`Annotations: ${model.annotations_parsed}`);
749
1158
  console.log(`${'─'.repeat(40)}`);
750
1159
  console.log(`Assets: ${model.assets.length}`);
@@ -764,4 +1173,14 @@ function printStatus(model) {
764
1173
  console.log(`Comments: ${model.comments.length}`);
765
1174
  console.log(`Shields: ${model.shields.length}`);
766
1175
  }
1176
+ function printUnannotatedFiles(model) {
1177
+ if (model.unannotated_files.length === 0) {
1178
+ console.log(`\n✓ All source files have GuardLink annotations.`);
1179
+ return;
1180
+ }
1181
+ console.log(`\n⚠ ${model.unannotated_files.length} source file(s) with no annotations:`);
1182
+ for (const f of model.unannotated_files) {
1183
+ console.log(` ${f}`);
1184
+ }
1185
+ }
767
1186
  //# sourceMappingURL=index.js.map