guardlink 1.0.0 → 1.2.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 (91) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +14 -0
  3. package/dist/agents/config.d.ts +8 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js +28 -5
  6. package/dist/agents/config.js.map +1 -1
  7. package/dist/agents/index.d.ts +2 -1
  8. package/dist/agents/index.d.ts.map +1 -1
  9. package/dist/agents/index.js +1 -1
  10. package/dist/agents/index.js.map +1 -1
  11. package/dist/agents/launcher.d.ts +14 -0
  12. package/dist/agents/launcher.d.ts.map +1 -1
  13. package/dist/agents/launcher.js +126 -1
  14. package/dist/agents/launcher.js.map +1 -1
  15. package/dist/agents/prompts.d.ts +2 -2
  16. package/dist/agents/prompts.d.ts.map +1 -1
  17. package/dist/agents/prompts.js +251 -31
  18. package/dist/agents/prompts.js.map +1 -1
  19. package/dist/analyze/index.d.ts +34 -1
  20. package/dist/analyze/index.d.ts.map +1 -1
  21. package/dist/analyze/index.js +281 -8
  22. package/dist/analyze/index.js.map +1 -1
  23. package/dist/analyze/llm.d.ts +54 -3
  24. package/dist/analyze/llm.d.ts.map +1 -1
  25. package/dist/analyze/llm.js +418 -97
  26. package/dist/analyze/llm.js.map +1 -1
  27. package/dist/analyze/prompts.d.ts +3 -2
  28. package/dist/analyze/prompts.d.ts.map +1 -1
  29. package/dist/analyze/prompts.js +227 -111
  30. package/dist/analyze/prompts.js.map +1 -1
  31. package/dist/analyze/tools.d.ts +22 -0
  32. package/dist/analyze/tools.d.ts.map +1 -0
  33. package/dist/analyze/tools.js +230 -0
  34. package/dist/analyze/tools.js.map +1 -0
  35. package/dist/analyzer/sarif.js +1 -1
  36. package/dist/cli/index.d.ts +15 -7
  37. package/dist/cli/index.d.ts.map +1 -1
  38. package/dist/cli/index.js +290 -150
  39. package/dist/cli/index.js.map +1 -1
  40. package/dist/dashboard/data.d.ts +5 -0
  41. package/dist/dashboard/data.d.ts.map +1 -1
  42. package/dist/dashboard/data.js +24 -12
  43. package/dist/dashboard/data.js.map +1 -1
  44. package/dist/dashboard/diagrams.d.ts.map +1 -1
  45. package/dist/dashboard/diagrams.js +310 -37
  46. package/dist/dashboard/diagrams.js.map +1 -1
  47. package/dist/dashboard/generate.d.ts.map +1 -1
  48. package/dist/dashboard/generate.js +197 -64
  49. package/dist/dashboard/generate.js.map +1 -1
  50. package/dist/init/picker.d.ts.map +1 -1
  51. package/dist/init/picker.js +2 -2
  52. package/dist/init/picker.js.map +1 -1
  53. package/dist/init/templates.d.ts.map +1 -1
  54. package/dist/init/templates.js +52 -32
  55. package/dist/init/templates.js.map +1 -1
  56. package/dist/mcp/server.d.ts.map +1 -1
  57. package/dist/mcp/server.js +14 -28
  58. package/dist/mcp/server.js.map +1 -1
  59. package/dist/parser/index.d.ts +1 -0
  60. package/dist/parser/index.d.ts.map +1 -1
  61. package/dist/parser/index.js +1 -0
  62. package/dist/parser/index.js.map +1 -1
  63. package/dist/parser/parse-line.js +3 -3
  64. package/dist/parser/parse-line.js.map +1 -1
  65. package/dist/parser/parse-project.js +1 -1
  66. package/dist/parser/validate.d.ts +31 -0
  67. package/dist/parser/validate.d.ts.map +1 -0
  68. package/dist/parser/validate.js +149 -0
  69. package/dist/parser/validate.js.map +1 -0
  70. package/dist/report/report.d.ts.map +1 -1
  71. package/dist/report/report.js +64 -0
  72. package/dist/report/report.js.map +1 -1
  73. package/dist/tui/commands.d.ts +3 -3
  74. package/dist/tui/commands.d.ts.map +1 -1
  75. package/dist/tui/commands.js +390 -206
  76. package/dist/tui/commands.js.map +1 -1
  77. package/dist/tui/config.d.ts +2 -0
  78. package/dist/tui/config.d.ts.map +1 -1
  79. package/dist/tui/config.js.map +1 -1
  80. package/dist/tui/format.d.ts +7 -0
  81. package/dist/tui/format.d.ts.map +1 -1
  82. package/dist/tui/format.js +59 -0
  83. package/dist/tui/format.js.map +1 -1
  84. package/dist/tui/index.d.ts.map +1 -1
  85. package/dist/tui/index.js +32 -19
  86. package/dist/tui/index.js.map +1 -1
  87. package/dist/tui/input.d.ts +2 -2
  88. package/dist/tui/input.js +2 -2
  89. package/dist/types/index.d.ts +1 -1
  90. package/dist/types/index.d.ts.map +1 -1
  91. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -3,13 +3,21 @@
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
22
  * @exposes #cli to #path-traversal [high] cwe:CWE-22 -- "Accepts directory paths from command line arguments"
15
23
  * @exposes #cli to #arbitrary-write [high] cwe:CWE-73 -- "Writes reports and SARIF to user-specified output paths"
@@ -23,8 +31,8 @@
23
31
  */
24
32
  import { Command } from 'commander';
25
33
  import { resolve, basename } from 'node:path';
26
- import { readFileSync, existsSync } from 'node:fs';
27
- import { parseProject } from '../parser/index.js';
34
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
35
+ import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from '../parser/index.js';
28
36
  import { initProject, detectProject, promptAgentSelection } from '../init/index.js';
29
37
  import { generateReport, generateMermaid } from '../report/index.js';
30
38
  import { diffModels, formatDiff, formatDiffMarkdown, parseAtRef } from '../diff/index.js';
@@ -32,7 +40,7 @@ import { generateSarif } from '../analyzer/index.js';
32
40
  import { startStdioServer } from '../mcp/index.js';
33
41
  import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage } from '../analyze/index.js';
34
42
  import { generateDashboardHTML } from '../dashboard/index.js';
35
- import { AGENTS, agentFromOpts, launchAgent, buildAnnotatePrompt } from '../agents/index.js';
43
+ import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt } from '../agents/index.js';
36
44
  import { resolveConfig, saveProjectConfig, saveGlobalConfig, loadProjectConfig, loadGlobalConfig, maskKey, describeConfigSource } from '../agents/config.js';
37
45
  import gradient from 'gradient-string';
38
46
  const program = new Command();
@@ -80,7 +88,7 @@ function detectProjectName(root, explicit) {
80
88
  program
81
89
  .name('guardlink')
82
90
  .description('GuardLink — Security annotations for code. Threat modeling that lives in your codebase.')
83
- .version('1.0.0')
91
+ .version('1.1.0')
84
92
  .addHelpText('before', gradient(['#00ff41', '#00d4ff'])(ASCII_LOGO));
85
93
  // ─── init ────────────────────────────────────────────────────────────
86
94
  program
@@ -196,9 +204,13 @@ program
196
204
  const { model, diagnostics } = await parseProject({ root, project: opts.project });
197
205
  // Check for dangling refs
198
206
  const danglingDiags = findDanglingRefs(model);
199
- const allDiags = [...diagnostics, ...danglingDiags];
207
+ // Check for @accepts without @audit (governance concern)
208
+ const acceptAuditDiags = findAcceptedWithoutAudit(model);
209
+ const allDiags = [...diagnostics, ...danglingDiags, ...acceptAuditDiags];
200
210
  // Check for unmitigated exposures
201
211
  const unmitigated = findUnmitigatedExposures(model);
212
+ // Check for accepted-but-unmitigated exposures (risk acceptance without real controls)
213
+ const acceptedOnly = findAcceptedExposures(model);
202
214
  printDiagnostics(allDiags);
203
215
  if (unmitigated.length > 0) {
204
216
  console.error(`\n⚠ ${unmitigated.length} unmitigated exposure(s):`);
@@ -206,11 +218,20 @@ program
206
218
  console.error(` ${u.asset} → ${u.threat} [${u.severity || 'unset'}] (${u.location.file}:${u.location.line})`);
207
219
  }
208
220
  }
221
+ if (acceptedOnly.length > 0) {
222
+ console.error(`\n⚡ ${acceptedOnly.length} accepted-but-unmitigated exposure(s) (risk accepted, no control in code):`);
223
+ for (const a of acceptedOnly) {
224
+ console.error(` ${a.asset} → ${a.threat} [${a.severity || 'unset'}] (${a.location.file}:${a.location.line})`);
225
+ }
226
+ }
209
227
  const errorCount = allDiags.filter(d => d.level === 'error').length;
210
228
  const hasUnmitigated = unmitigated.length > 0;
211
- if (errorCount === 0 && !hasUnmitigated) {
229
+ if (errorCount === 0 && !hasUnmitigated && acceptedOnly.length === 0) {
212
230
  console.error('\n✓ All annotations valid, no unmitigated exposures.');
213
231
  }
232
+ else if (errorCount === 0 && !hasUnmitigated && acceptedOnly.length > 0) {
233
+ console.error(`\nValidation passed. ${acceptedOnly.length} exposure(s) accepted without mitigation — ensure these are intentional human decisions.`);
234
+ }
214
235
  else if (errorCount === 0 && hasUnmitigated) {
215
236
  console.error(`\nValidation passed with ${unmitigated.length} unmitigated exposure(s).`);
216
237
  }
@@ -340,32 +361,33 @@ program
340
361
  // ─── threat-report ───────────────────────────────────────────────────
341
362
  program
342
363
  .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', '.')
364
+ .description('Generate an AI threat report using a framework or custom prompt')
365
+ .argument('[prompt...]', 'Framework (stride, dread, pasta, attacker, rapid, general) or custom prompt text')
366
+ .option('-d, --dir <dir>', 'Project directory', '.')
346
367
  .option('-p, --project <n>', 'Project name', 'unknown')
347
- .option('--provider <provider>', 'LLM provider: anthropic, openai, openrouter, deepseek (auto-detected from env)')
368
+ .option('--provider <provider>', 'LLM provider: anthropic, openai, google, openrouter, deepseek (auto-detected from env)')
348
369
  .option('--model <model>', 'Model name (default: provider-specific)')
349
370
  .option('--api-key <key>', 'API key (default: from env variable)')
350
371
  .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')
372
+ .option('--web-search', 'Enable web search grounding (OpenAI only)')
373
+ .option('--thinking', 'Enable extended thinking / reasoning (Anthropic, DeepSeek only)')
374
+ .option('--claude-code', 'Run via Claude Code (inline)')
375
+ .option('--codex', 'Run via Codex CLI (inline)')
376
+ .option('--gemini', 'Run via Gemini CLI (inline)')
355
377
  .option('--cursor', 'Open Cursor IDE with prompt on clipboard')
356
378
  .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard')
357
379
  .option('--clipboard', 'Copy threat report prompt to clipboard only')
358
- .action(async (framework, dir, opts) => {
359
- const root = resolve(dir);
380
+ .action(async (promptParts, opts) => {
381
+ const root = resolve(opts.dir);
360
382
  const project = detectProjectName(root, opts.project);
361
- // Validate framework
383
+ const input = promptParts.join(' ').trim();
384
+ // Determine framework vs custom prompt
362
385
  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;
386
+ const inputLower = input.toLowerCase();
387
+ const isStandard = validFrameworks.includes(inputLower);
388
+ const fw = (isStandard ? inputLower : 'general');
389
+ const customPrompt = isStandard ? undefined : (input || undefined);
390
+ const reportLabel = customPrompt ? 'Custom Threat Analysis' : FRAMEWORK_LABELS[fw];
369
391
  // Parse project
370
392
  const { model, diagnostics } = await parseProject({ root, project });
371
393
  const errors = diagnostics.filter(d => d.level === 'error');
@@ -375,34 +397,74 @@ program
375
397
  console.error('No annotations found. Run: guardlink init . && add annotations first.');
376
398
  process.exit(1);
377
399
  }
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)`);
400
+ // Build analysis prompt (shared by agent and API paths)
401
+ const serialized = serializeModel(model);
402
+ const { buildProjectContext, extractCodeSnippets } = await import('../analyze/index.js');
403
+ const projectContext = buildProjectContext(root);
404
+ const codeSnippets = extractCodeSnippets(root, model);
405
+ const systemPrompt = FRAMEWORK_PROMPTS[fw];
406
+ const userMessage = buildUserMessage(serialized, fw, customPrompt, projectContext || undefined, codeSnippets || undefined);
407
+ const analysisPrompt = `You are analyzing a codebase with GuardLink security annotations.
408
+ You have access to the full source code in the current directory.
409
+
410
+ ${systemPrompt}
411
+
412
+ ## Task
413
+ Read the source code and GuardLink annotations, then produce a thorough ${reportLabel}.
414
+
415
+ ## Threat Model (serialized from annotations)
416
+ ${userMessage}
417
+
418
+ ## Instructions
419
+ 1. Read the actual source files to understand the code — don't just rely on the serialized model above
420
+ 2. Cross-reference the annotations with the real code to validate findings
421
+ 3. Produce the full report as markdown
422
+ 4. Be specific — reference actual files, functions, and line numbers from the codebase
423
+ 5. Output ONLY the markdown report content — do NOT add any metadata comments, save confirmations, or file path messages
424
+ 6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."`;
425
+ // Resolve agent: explicit flag > project config CLI agent
426
+ let agent = agentFromOpts(opts);
427
+ if (!agent) {
428
+ const projCfg = loadProjectConfig(root);
429
+ if (projCfg?.aiMode === 'cli-agent' && projCfg?.cliAgent) {
430
+ agent = AGENTS.find(a => a.id === projCfg.cliAgent) || null;
393
431
  }
432
+ }
433
+ // ── Path 1: CLI Agent (inline, non-interactive) ──
434
+ if (agent && agent.cmd) {
435
+ console.error(`\n🔍 ${reportLabel}`);
436
+ console.error(` Agent: ${agent.name} (inline)`);
437
+ console.error(` Annotations: ${model.annotations_parsed} | Exposures: ${model.exposures.length}\n`);
438
+ const result = await launchAgentInline(agent, analysisPrompt, root, (text) => process.stdout.write(text), { autoYes: true });
394
439
  if (result.error) {
395
- console.error(`✗ ${result.error}`);
396
- if (result.clipboardCopied) {
397
- console.log('Prompt is on your clipboard — paste it manually.');
398
- }
440
+ console.error(`\n✗ ${result.error}`);
399
441
  process.exit(1);
400
442
  }
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.');
443
+ process.stdout.write('\n');
444
+ // Save the agent's output as a report
445
+ if (result.content.trim()) {
446
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
447
+ const reportsDir = resolve(root, '.guardlink', 'threat-reports');
448
+ if (!existsSync(reportsDir))
449
+ mkdirSync(reportsDir, { recursive: true });
450
+ const filename = `${timestamp}-${fw}.md`;
451
+ const filepath = resolve(reportsDir, filename);
452
+ // Clean ANSI codes and CLI artifacts from the output before saving
453
+ const { cleanCliArtifacts } = await import('../tui/format.js');
454
+ const cleanedContent = cleanCliArtifacts(result.content);
455
+ 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`;
456
+ writeFileSync(filepath, header + cleanedContent + '\n');
457
+ console.error(`\n✓ Report saved to .guardlink/threat-reports/${filename}`);
458
+ }
459
+ return;
460
+ }
461
+ // ── Path 2: Clipboard / IDE agent ──
462
+ if (agent && !agent.cmd) {
463
+ const result = launchAgent(agent, analysisPrompt, root);
464
+ if (result.clipboardCopied) {
465
+ console.log(`✓ Prompt copied to clipboard (${analysisPrompt.length.toLocaleString()} chars)`);
404
466
  }
405
- else if (agent.app && result.launched) {
467
+ if (result.launched && agent.app) {
406
468
  console.log(`✓ ${agent.name} launched with project: ${project}`);
407
469
  console.log('\nPaste (Cmd+V) the prompt in the AI chat panel.');
408
470
  console.log('When done, run: guardlink threat-reports');
@@ -411,26 +473,26 @@ program
411
473
  console.log('\nPaste the prompt into your preferred AI tool.');
412
474
  console.log('When done, run: guardlink threat-reports');
413
475
  }
476
+ else if (result.error) {
477
+ console.error(`✗ ${result.error}`);
478
+ process.exit(1);
479
+ }
414
480
  return;
415
481
  }
416
- // ── API path: direct LLM call (no agent flag) ──
482
+ // ── Path 3: Direct API call ──
417
483
  const llmConfig = buildConfig({
418
484
  provider: opts.provider,
419
485
  model: opts.model,
420
486
  apiKey: opts.apiKey,
421
- });
487
+ }) || resolveConfig(root);
422
488
  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-...');
489
+ console.error('No AI provider configured. Use one of:');
490
+ console.error(' guardlink config Configure API provider');
491
+ console.error(' --claude-code / --codex Use a CLI agent');
492
+ console.error(' ANTHROPIC_API_KEY=... Set env var');
431
493
  process.exit(1);
432
494
  }
433
- console.error(`\n🔍 ${FRAMEWORK_LABELS[fw]}`);
495
+ console.error(`\n🔍 ${reportLabel}`);
434
496
  console.error(` Provider: ${llmConfig.provider} | Model: ${llmConfig.model}`);
435
497
  console.error(` Annotations: ${model.annotations_parsed} | Exposures: ${model.exposures.length}\n`);
436
498
  try {
@@ -439,9 +501,11 @@ program
439
501
  model,
440
502
  framework: fw,
441
503
  llmConfig,
442
- customPrompt: opts.custom,
504
+ customPrompt,
443
505
  stream: opts.stream !== false,
444
506
  onChunk: opts.stream !== false ? (text) => process.stdout.write(text) : undefined,
507
+ webSearch: opts.webSearch,
508
+ extendedThinking: opts.thinking,
445
509
  });
446
510
  if (opts.stream !== false) {
447
511
  process.stdout.write('\n');
@@ -453,6 +517,9 @@ program
453
517
  if (result.inputTokens || result.outputTokens) {
454
518
  console.error(` Tokens: ${result.inputTokens || '?'} in / ${result.outputTokens || '?'} out`);
455
519
  }
520
+ if (result.thinkingTokens) {
521
+ console.error(` Thinking: ${result.thinkingTokens} tokens`);
522
+ }
456
523
  }
457
524
  catch (err) {
458
525
  console.error(`\n✗ Threat report generation failed: ${err.message}`);
@@ -552,7 +619,7 @@ program
552
619
  .command('config')
553
620
  .description('Manage LLM provider configuration')
554
621
  .argument('<action>', 'Action: set, show, clear')
555
- .argument('[key]', 'Config key: provider, api-key, model')
622
+ .argument('[key]', 'Config key: provider, api-key, model, ai-mode, cli-agent')
556
623
  .argument('[value]', 'Value to set')
557
624
  .option('--global', 'Use global config (~/.config/guardlink/) instead of project')
558
625
  .action(async (action, key, value, opts) => {
@@ -562,17 +629,24 @@ program
562
629
  case 'show': {
563
630
  const config = resolveConfig(root);
564
631
  const source = describeConfigSource(root);
632
+ const projCfg = isGlobal ? loadGlobalConfig() : loadProjectConfig(root);
633
+ const aiMode = projCfg?.aiMode || 'api';
634
+ const cliAgent = projCfg?.cliAgent;
635
+ console.log(`AI Mode: ${aiMode}${cliAgent ? ` (${cliAgent})` : ''}`);
565
636
  if (config) {
566
637
  console.log(`Provider: ${config.provider}`);
567
638
  console.log(`Model: ${config.model}`);
568
639
  console.log(`API Key: ${maskKey(config.apiKey)}`);
569
640
  console.log(`Source: ${source}`);
570
641
  }
571
- else {
642
+ else if (aiMode !== 'cli-agent') {
572
643
  console.log('No LLM configuration found.');
573
644
  console.log('\nSet one with:');
574
645
  console.log(' guardlink config set provider anthropic');
575
646
  console.log(' guardlink config set api-key sk-ant-...');
647
+ console.log('\nOr use a CLI agent:');
648
+ console.log(' guardlink config set ai-mode cli-agent');
649
+ console.log(' guardlink config set cli-agent claude-code');
576
650
  console.log('\nOr set environment variables:');
577
651
  console.log(' export GUARDLINK_LLM_KEY=sk-ant-...');
578
652
  console.log(' export GUARDLINK_LLM_PROVIDER=anthropic');
@@ -582,17 +656,19 @@ program
582
656
  case 'set': {
583
657
  if (!key || !value) {
584
658
  console.error('Usage: guardlink config set <key> <value>');
585
- console.error('Keys: provider, api-key, model');
659
+ console.error('Keys: provider, api-key, model, ai-mode, cli-agent');
586
660
  process.exit(1);
587
661
  }
588
662
  const existing = isGlobal
589
663
  ? loadGlobalConfig() || {}
590
664
  : loadProjectConfig(root) || {};
665
+ const validProviders = ['anthropic', 'openai', 'google', 'openrouter', 'deepseek', 'ollama'];
666
+ const validAgentIds = AGENTS.map(a => a.id);
591
667
  switch (key) {
592
668
  case 'provider':
593
- if (!['anthropic', 'openai', 'openrouter', 'deepseek'].includes(value)) {
669
+ if (!validProviders.includes(value)) {
594
670
  console.error(`Unknown provider: ${value}`);
595
- console.error('Available: anthropic, openai, openrouter, deepseek');
671
+ console.error(`Available: ${validProviders.join(', ')}`);
596
672
  process.exit(1);
597
673
  }
598
674
  existing.provider = value;
@@ -603,8 +679,25 @@ program
603
679
  case 'model':
604
680
  existing.model = value;
605
681
  break;
682
+ case 'ai-mode':
683
+ if (!['api', 'cli-agent'].includes(value)) {
684
+ console.error(`Unknown ai-mode: ${value}`);
685
+ console.error('Available: api, cli-agent');
686
+ process.exit(1);
687
+ }
688
+ existing.aiMode = value;
689
+ break;
690
+ case 'cli-agent':
691
+ if (!validAgentIds.includes(value)) {
692
+ console.error(`Unknown cli-agent: ${value}`);
693
+ console.error(`Available: ${validAgentIds.join(', ')}`);
694
+ process.exit(1);
695
+ }
696
+ existing.cliAgent = value;
697
+ existing.aiMode = 'cli-agent';
698
+ break;
606
699
  default:
607
- console.error(`Unknown config key: ${key}. Use: provider, api-key, model`);
700
+ console.error(`Unknown config key: ${key}. Use: provider, api-key, model, ai-mode, cli-agent`);
608
701
  process.exit(1);
609
702
  }
610
703
  if (isGlobal) {
@@ -676,7 +769,7 @@ program
676
769
  .command('tui')
677
770
  .description('Interactive TUI — slash commands, AI chat, exposure triage')
678
771
  .argument('[dir]', 'project directory', '.')
679
- .option('--provider <provider>', 'LLM provider for this session (anthropic, openai, openrouter, deepseek)')
772
+ .option('--provider <provider>', 'LLM provider for this session (anthropic, openai, google, openrouter, deepseek)')
680
773
  .option('--api-key <key>', 'LLM API key for this session (not persisted)')
681
774
  .option('--model <model>', 'LLM model override')
682
775
  .action(async (dir, opts) => {
@@ -693,31 +786,132 @@ program
693
786
  .description('Display GuardLink Annotation Language (GAL) quick reference')
694
787
  .action(() => {
695
788
  import('chalk').then(({ default: c }) => {
789
+ const H = (s) => c.bold.cyan(s);
790
+ const V = (s) => c.bold.cyanBright(s);
791
+ const K = (s) => c.yellow(s);
792
+ const D = (s) => c.dim(s);
793
+ const EX = (s) => c.green(s);
696
794
  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`);
795
+ console.log('');
796
+ console.log(H(' ══════════════════════════════════════════════════════════'));
797
+ console.log(H(' GAL GuardLink Annotation Language'));
798
+ console.log(H(' ══════════════════════════════════════════════════════════'));
799
+ console.log('');
800
+ console.log(D(' Annotations live in source code comments. GuardLink parses'));
801
+ console.log(D(' them to build a live threat model from your codebase.'));
802
+ console.log('');
803
+ console.log(D(' Syntax: @verb subject [preposition object] [: description]'));
804
+ console.log('');
805
+ // ── DEFINITIONS ──
806
+ console.log(H(' ── Definitions ─────────────────────────────────────────────'));
807
+ console.log('');
808
+ console.log(` ${V('@asset')} ${K('<path>')} ${D('[: description]')}`);
809
+ console.log(D(' Declare a named asset (component, service, data store).'));
810
+ console.log(D(' Path uses dot notation for hierarchy.'));
811
+ console.log(EX(' // @asset api.auth.token_store : Stores JWT refresh tokens'));
812
+ console.log(EX(' // @asset db.users'));
813
+ console.log('');
814
+ console.log(` ${V('@threat')} ${K('<name>')} ${D('[severity: critical|high|medium|low] [: description]')}`);
815
+ console.log(D(' Declare a named threat. Severity aliases: P0=critical P1=high P2=medium P3=low.'));
816
+ console.log(EX(' // @threat SQL Injection severity:high : Unsanitized input reaches DB'));
817
+ console.log(EX(' // @threat Token Theft severity:P0'));
818
+ console.log('');
819
+ console.log(` ${V('@control')} ${K('<name>')} ${D('[: description]')}`);
820
+ console.log(D(' Declare a security control (mitigation mechanism).'));
821
+ console.log(EX(' // @control Input Validation : Sanitize all user-supplied strings'));
822
+ console.log(EX(' // @control Rate Limiting'));
823
+ console.log('');
824
+ // ── RELATIONSHIPS ──
825
+ console.log(H(' ── Relationships ───────────────────────────────────────────'));
826
+ console.log('');
827
+ console.log(` ${V('@exposes')} ${K('<asset>')} ${D('to')} ${K('<threat>')} ${D('[severity: ...] [: description]')}`);
828
+ console.log(D(' Mark an asset as exposed to a threat at this code location.'));
829
+ console.log(D(' This is the primary annotation — every exposure creates a finding.'));
830
+ console.log(EX(' // @exposes api.auth to SQL Injection severity:high'));
831
+ console.log(EX(' // @exposes db.users to Token Theft severity:critical : No token rotation'));
832
+ console.log('');
833
+ console.log(` ${V('@mitigates')} ${K('<asset>')} ${D('against')} ${K('<threat>')} ${D('[with')} ${K('<control>')}${D('] [: description]')}`);
834
+ console.log(D(' Mark that a control mitigates a threat on an asset.'));
835
+ console.log(D(' Closes the exposure — removes it from open findings.'));
836
+ console.log(EX(' // @mitigates api.auth against SQL Injection with Input Validation'));
837
+ console.log(EX(' // @mitigates db.users against Token Theft : Rotation implemented in v2'));
838
+ console.log('');
839
+ console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[: reason]')}`);
840
+ console.log(D(' Explicitly accept a risk. Removes it from open findings.'));
841
+ console.log(D(' Use when the risk is known and intentionally not mitigated.'));
842
+ console.log(EX(' // @accepts Timing Attack on api.auth : Acceptable for current threat model'));
843
+ console.log('');
844
+ console.log(` ${V('@transfers')} ${K('<threat>')} ${D('from')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[: description]')}`);
845
+ console.log(D(' Transfer responsibility for a threat to another asset/team.'));
846
+ console.log(EX(' // @transfers DDoS from api.gateway to cdn.cloudflare : Handled by CDN layer'));
847
+ console.log('');
848
+ // ── DATA FLOWS ──
849
+ console.log(H(' ── Data Flows & Boundaries ─────────────────────────────────'));
850
+ console.log('');
851
+ console.log(` ${V('@flows')} ${K('<source>')} ${D('to')} ${K('<target>')} ${D('[via')} ${K('<mechanism>')}${D('] [: description]')}`);
852
+ console.log(D(' Document data movement between components.'));
853
+ console.log(D(' Appears in the Data Flow Diagram.'));
854
+ console.log(EX(' // @flows api.auth to db.users via TLS 1.3'));
855
+ console.log(EX(' // @flows mobile.app to api.gateway via HTTPS : User credentials'));
856
+ console.log('');
857
+ console.log(` ${V('@boundary')} ${K('<asset_a>')} ${D('and')} ${K('<asset_b>')} ${D('[: description]')}`);
858
+ console.log(D(' Declare a trust boundary between two assets.'));
859
+ console.log(D(' Groups assets in the Data Flow Diagram.'));
860
+ console.log(EX(' // @boundary internet and api.gateway : Public-facing edge'));
861
+ console.log(EX(' // @boundary api.gateway and db.users : Internal network boundary'));
862
+ console.log('');
863
+ // ── LIFECYCLE ──
864
+ console.log(H(' ── Lifecycle & Governance ──────────────────────────────────'));
865
+ console.log('');
866
+ console.log(` ${V('@handles')} ${K('<classification>')} ${D('on')} ${K('<asset>')} ${D('[: description]')}`);
867
+ console.log(D(' Declare data classification handled by an asset.'));
868
+ console.log(D(' Classifications: pii phi financial secrets internal public'));
869
+ console.log(EX(' // @handles pii on db.users : Stores name, email, phone'));
870
+ console.log(EX(' // @handles secrets on api.auth.token_store'));
871
+ console.log('');
872
+ console.log(` ${V('@owns')} ${K('<owner>')} ${K('<asset>')} ${D('[: description]')}`);
873
+ console.log(D(' Assign ownership of an asset to a team or person.'));
874
+ console.log(EX(' // @owns platform-team api.auth'));
875
+ console.log('');
876
+ console.log(` ${V('@validates')} ${K('<control>')} ${D('on')} ${K('<asset>')} ${D('[: description]')}`);
877
+ console.log(D(' Assert that a control has been validated/tested on an asset.'));
878
+ console.log(EX(' // @validates Input Validation on api.auth : Pen-tested 2024-Q3'));
879
+ console.log('');
880
+ console.log(` ${V('@audit')} ${K('<asset>')} ${D('[: description]')}`);
881
+ console.log(D(' Mark that this code path is an audit trail point.'));
882
+ console.log(EX(' // @audit db.users : All writes logged to audit_log table'));
883
+ console.log('');
884
+ console.log(` ${V('@assumes')} ${K('<asset>')} ${D('[: description]')}`);
885
+ console.log(D(' Document a security assumption about an asset.'));
886
+ console.log(EX(' // @assumes api.gateway : Upstream WAF filters malformed requests'));
887
+ console.log('');
888
+ console.log(` ${V('@comment')} ${D('[: description]')}`);
889
+ console.log(D(' Free-form developer security note (no structural effect).'));
890
+ console.log(EX(' // @comment : TODO — add rate limiting before v2 launch'));
891
+ console.log('');
892
+ // ── SHIELD BLOCKS ──
893
+ console.log(H(' ── Shield Blocks ───────────────────────────────────────────'));
894
+ console.log('');
895
+ console.log(` ${V('@shield:begin')} ${D('/')} ${V('@shield:end')}`);
896
+ console.log(D(' Wrap a code block to mark it as security-sensitive.'));
897
+ console.log(D(' GuardLink will flag unannotated symbols inside the block.'));
898
+ console.log(EX(' // @shield:begin'));
899
+ console.log(EX(' function verifyToken(token: string) { ... }'));
900
+ console.log(EX(' // @shield:end'));
901
+ console.log('');
902
+ // ── TIPS ──
903
+ console.log(H(' ── Tips ────────────────────────────────────────────────────'));
904
+ console.log('');
905
+ console.log(D(' • Annotations work in any comment style: // /* # -- <!-- -->'));
906
+ console.log(D(' • Place annotations on the line ABOVE the code they describe'));
907
+ console.log(D(' • Asset names are case-insensitive and normalized (spaces→underscores)'));
908
+ console.log(D(' • Threat/control names can reference IDs with #id syntax'));
909
+ console.log(D(' • Run guardlink parse after adding annotations to update the threat model'));
910
+ console.log(D(' • Run guardlink validate to check for syntax errors and dangling references'));
911
+ console.log(D(' • Run guardlink annotate to have an AI agent add annotations automatically'));
912
+ console.log('');
913
+ console.log(H(' ══════════════════════════════════════════════════════════'));
914
+ console.log('');
721
915
  });
722
916
  });
723
917
  // If no subcommand given, launch TUI
@@ -764,58 +958,4 @@ function printStatus(model) {
764
958
  console.log(`Comments: ${model.comments.length}`);
765
959
  console.log(`Shields: ${model.shields.length}`);
766
960
  }
767
- function findDanglingRefs(model) {
768
- const diagnostics = [];
769
- // Collect all defined IDs
770
- const definedIds = new Set();
771
- for (const a of model.assets)
772
- if (a.id)
773
- definedIds.add(a.id);
774
- for (const t of model.threats)
775
- if (t.id)
776
- definedIds.add(t.id);
777
- for (const c of model.controls)
778
- if (c.id)
779
- definedIds.add(c.id);
780
- for (const b of model.boundaries)
781
- if (b.id)
782
- definedIds.add(b.id);
783
- // Check all references
784
- const checkRef = (ref, loc) => {
785
- if (ref.startsWith('#')) {
786
- const id = ref.slice(1);
787
- if (!definedIds.has(id)) {
788
- diagnostics.push({
789
- level: 'warning',
790
- message: `Dangling reference: #${id} is never defined`,
791
- file: loc.file,
792
- line: loc.line,
793
- });
794
- }
795
- }
796
- };
797
- for (const m of model.mitigations) {
798
- checkRef(m.threat, m.location);
799
- if (m.control)
800
- checkRef(m.control, m.location);
801
- }
802
- for (const e of model.exposures)
803
- checkRef(e.threat, e.location);
804
- for (const a of model.acceptances)
805
- checkRef(a.threat, a.location);
806
- for (const t of model.transfers)
807
- checkRef(t.threat, t.location);
808
- for (const v of model.validations)
809
- checkRef(v.control, v.location);
810
- return diagnostics;
811
- }
812
- function findUnmitigatedExposures(model) {
813
- // Build set of (asset, threat) pairs that are mitigated or accepted
814
- const mitigated = new Set();
815
- for (const m of model.mitigations)
816
- mitigated.add(`${m.asset}::${m.threat}`);
817
- for (const a of model.acceptances)
818
- mitigated.add(`${a.asset}::${a.threat}`);
819
- return model.exposures.filter(e => !mitigated.has(`${e.asset}::${e.threat}`));
820
- }
821
961
  //# sourceMappingURL=index.js.map