guardlink 1.4.2 → 1.4.3

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 (119) hide show
  1. package/CHANGELOG.md +83 -9
  2. package/README.md +38 -1
  3. package/dist/agents/config.d.ts +7 -0
  4. package/dist/agents/config.d.ts.map +1 -1
  5. package/dist/agents/config.js.map +1 -1
  6. package/dist/agents/index.d.ts +1 -1
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/index.js +1 -1
  9. package/dist/agents/index.js.map +1 -1
  10. package/dist/agents/prompts.d.ts +14 -0
  11. package/dist/agents/prompts.d.ts.map +1 -1
  12. package/dist/agents/prompts.js +445 -2
  13. package/dist/agents/prompts.js.map +1 -1
  14. package/dist/analyze/format.d.ts +72 -0
  15. package/dist/analyze/format.d.ts.map +1 -0
  16. package/dist/analyze/format.js +176 -0
  17. package/dist/analyze/format.js.map +1 -0
  18. package/dist/analyze/index.d.ts +76 -0
  19. package/dist/analyze/index.d.ts.map +1 -1
  20. package/dist/analyze/index.js +165 -2
  21. package/dist/analyze/index.js.map +1 -1
  22. package/dist/analyze/prompts.d.ts +3 -2
  23. package/dist/analyze/prompts.d.ts.map +1 -1
  24. package/dist/analyze/prompts.js +16 -2
  25. package/dist/analyze/prompts.js.map +1 -1
  26. package/dist/analyzer/sarif.d.ts +3 -2
  27. package/dist/analyzer/sarif.d.ts.map +1 -1
  28. package/dist/analyzer/sarif.js +29 -3
  29. package/dist/analyzer/sarif.js.map +1 -1
  30. package/dist/cli/index.d.ts +2 -0
  31. package/dist/cli/index.d.ts.map +1 -1
  32. package/dist/cli/index.js +380 -28
  33. package/dist/cli/index.js.map +1 -1
  34. package/dist/dashboard/data.d.ts +11 -0
  35. package/dist/dashboard/data.d.ts.map +1 -1
  36. package/dist/dashboard/data.js +12 -0
  37. package/dist/dashboard/data.js.map +1 -1
  38. package/dist/dashboard/diagrams.d.ts +81 -12
  39. package/dist/dashboard/diagrams.d.ts.map +1 -1
  40. package/dist/dashboard/diagrams.js +750 -362
  41. package/dist/dashboard/diagrams.js.map +1 -1
  42. package/dist/dashboard/generate.d.ts +5 -2
  43. package/dist/dashboard/generate.d.ts.map +1 -1
  44. package/dist/dashboard/generate.js +2516 -244
  45. package/dist/dashboard/generate.js.map +1 -1
  46. package/dist/diff/engine.d.ts +2 -1
  47. package/dist/diff/engine.d.ts.map +1 -1
  48. package/dist/diff/engine.js +3 -2
  49. package/dist/diff/engine.js.map +1 -1
  50. package/dist/init/index.d.ts.map +1 -1
  51. package/dist/init/index.js +24 -5
  52. package/dist/init/index.js.map +1 -1
  53. package/dist/init/migrate.d.ts +39 -0
  54. package/dist/init/migrate.d.ts.map +1 -0
  55. package/dist/init/migrate.js +45 -0
  56. package/dist/init/migrate.js.map +1 -0
  57. package/dist/init/templates.d.ts +8 -0
  58. package/dist/init/templates.d.ts.map +1 -1
  59. package/dist/init/templates.js +71 -9
  60. package/dist/init/templates.js.map +1 -1
  61. package/dist/mcp/lookup.d.ts +1 -0
  62. package/dist/mcp/lookup.d.ts.map +1 -1
  63. package/dist/mcp/lookup.js +138 -10
  64. package/dist/mcp/lookup.js.map +1 -1
  65. package/dist/mcp/server.d.ts +2 -1
  66. package/dist/mcp/server.d.ts.map +1 -1
  67. package/dist/mcp/server.js +20 -8
  68. package/dist/mcp/server.js.map +1 -1
  69. package/dist/parser/clear.js +1 -1
  70. package/dist/parser/clear.js.map +1 -1
  71. package/dist/parser/feature-filter.d.ts +42 -0
  72. package/dist/parser/feature-filter.d.ts.map +1 -0
  73. package/dist/parser/feature-filter.js +109 -0
  74. package/dist/parser/feature-filter.js.map +1 -0
  75. package/dist/parser/format.d.ts +24 -0
  76. package/dist/parser/format.d.ts.map +1 -0
  77. package/dist/parser/format.js +29 -0
  78. package/dist/parser/format.js.map +1 -0
  79. package/dist/parser/index.d.ts +2 -0
  80. package/dist/parser/index.d.ts.map +1 -1
  81. package/dist/parser/index.js +1 -0
  82. package/dist/parser/index.js.map +1 -1
  83. package/dist/parser/parse-file.d.ts.map +1 -1
  84. package/dist/parser/parse-file.js +3 -1
  85. package/dist/parser/parse-file.js.map +1 -1
  86. package/dist/parser/parse-line.d.ts +3 -0
  87. package/dist/parser/parse-line.d.ts.map +1 -1
  88. package/dist/parser/parse-line.js +78 -22
  89. package/dist/parser/parse-line.js.map +1 -1
  90. package/dist/parser/parse-project.js +19 -0
  91. package/dist/parser/parse-project.js.map +1 -1
  92. package/dist/parser/validate.d.ts +3 -0
  93. package/dist/parser/validate.d.ts.map +1 -1
  94. package/dist/parser/validate.js +7 -0
  95. package/dist/parser/validate.js.map +1 -1
  96. package/dist/report/index.d.ts +1 -0
  97. package/dist/report/index.d.ts.map +1 -1
  98. package/dist/report/index.js +1 -0
  99. package/dist/report/index.js.map +1 -1
  100. package/dist/report/report.d.ts.map +1 -1
  101. package/dist/report/report.js +924 -24
  102. package/dist/report/report.js.map +1 -1
  103. package/dist/report/sequence.d.ts +11 -0
  104. package/dist/report/sequence.d.ts.map +1 -0
  105. package/dist/report/sequence.js +140 -0
  106. package/dist/report/sequence.js.map +1 -0
  107. package/dist/tui/commands.d.ts +1 -0
  108. package/dist/tui/commands.d.ts.map +1 -1
  109. package/dist/tui/commands.js +83 -4
  110. package/dist/tui/commands.js.map +1 -1
  111. package/dist/tui/index.d.ts.map +1 -1
  112. package/dist/tui/index.js +7 -2
  113. package/dist/tui/index.js.map +1 -1
  114. package/dist/types/index.d.ts +57 -3
  115. package/dist/types/index.d.ts.map +1 -1
  116. package/dist/workspace/merge.d.ts.map +1 -1
  117. package/dist/workspace/merge.js +6 -2
  118. package/dist/workspace/merge.js.map +1 -1
  119. package/package.json +1 -1
package/dist/cli/index.js CHANGED
@@ -12,6 +12,8 @@
12
12
  * guardlink sarif [dir] Export SARIF 2.1.0 for GitHub / VS Code
13
13
  * guardlink threat-report <prompt> AI-powered threat analysis (STRIDE, DREAD, PASTA, etc.)
14
14
  * guardlink threat-reports List saved AI threat reports
15
+ * guardlink translate [prompt] Generate CERT-X-GEN pentest templates from threats
16
+ * guardlink ask <query> Ask questions about threats and codebase context
15
17
  * guardlink annotate <prompt> Launch coding agent to add annotations
16
18
  * guardlink config <action> Manage LLM provider configuration
17
19
  * guardlink dashboard [dir] Generate interactive HTML dashboard
@@ -37,15 +39,17 @@
37
39
  import { Command } from 'commander';
38
40
  import { resolve, basename } from 'node:path';
39
41
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
40
- import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } from '../parser/index.js';
42
+ import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations, listFeatures, filterByFeature, getFeatureSummaries } from '../parser/index.js';
43
+ import { diagnosticIcon } from '../parser/format.js';
41
44
  import { initProject, detectProject, promptAgentSelection, syncAgentFiles } from '../init/index.js';
45
+ import { ensurePromptMd } from '../init/migrate.js';
42
46
  import { generateReport, generateMermaid } from '../report/index.js';
43
47
  import { diffModels, formatDiff, formatDiffMarkdown, parseAtRef } from '../diff/index.js';
44
48
  import { generateSarif } from '../analyzer/index.js';
45
49
  import { startStdioServer } from '../mcp/index.js';
46
- import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage } from '../analyze/index.js';
50
+ import { generateThreatReport, listThreatReports, loadThreatReportsForDashboard, loadPentestData, serializePentestFindings, buildConfig, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, serializeModel, buildUserMessage } from '../analyze/index.js';
47
51
  import { generateDashboardHTML } from '../dashboard/index.js';
48
- import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt, resolveAnnotationMode } from '../agents/index.js';
52
+ import { AGENTS, agentFromOpts, launchAgent, launchAgentInline, buildAnnotatePrompt, buildTranslatePrompt, buildAskPrompt, resolveAnnotationMode } from '../agents/index.js';
49
53
  import { resolveConfig, saveProjectConfig, saveGlobalConfig, loadProjectConfig, loadGlobalConfig, maskKey, describeConfigSource } from '../agents/config.js';
50
54
  import { getReviewableExposures, applyReviewAction, formatExposureForReview, summarizeReview } from '../review/index.js';
51
55
  import { populateMetadata, mergeReports, formatMergeSummary, diffMergedReports, formatDiffSummary, linkProject, addToWorkspace, removeFromWorkspace } from '../workspace/index.js';
@@ -95,7 +99,7 @@ function detectProjectName(root, explicit) {
95
99
  program
96
100
  .name('guardlink')
97
101
  .description('GuardLink — Security annotations for code. Threat modeling that lives in your codebase.')
98
- .version('1.4.2')
102
+ .version('1.4.3')
99
103
  .addHelpText('before', gradient(['#00ff41', '#00d4ff'])(ASCII_LOGO));
100
104
  // ─── init ────────────────────────────────────────────────────────────
101
105
  program
@@ -196,9 +200,16 @@ program
196
200
  .argument('[dir]', 'Project directory to scan', '.')
197
201
  .option('-p, --project <n>', 'Project name', 'unknown')
198
202
  .option('--not-annotated', 'List source files with no GuardLink annotations')
203
+ .option('--feature <names>', 'Filter status to specific feature(s) (comma-separated)')
199
204
  .action(async (dir, opts) => {
200
205
  const root = resolve(dir);
201
- const { model, diagnostics } = await parseProject({ root, project: opts.project });
206
+ let { model, diagnostics } = await parseProject({ root, project: opts.project });
207
+ // Apply feature filter if specified
208
+ if (opts.feature) {
209
+ const featureNames = opts.feature.split(',').map(s => s.trim());
210
+ model = filterByFeature(model, featureNames);
211
+ console.log(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}\n`);
212
+ }
202
213
  printDiagnostics(diagnostics);
203
214
  printStatus(model);
204
215
  if (opts.notAnnotated) {
@@ -244,6 +255,12 @@ program
244
255
  console.error(` ${a.asset} → ${a.threat} [${a.severity || 'unset'}] (${a.location.file}:${a.location.line})`);
245
256
  }
246
257
  }
258
+ if ((model.confirmed || []).length > 0) {
259
+ console.error(`\n🔴 ${model.confirmed.length} confirmed exploitable finding(s) — verified, not false positives:`);
260
+ for (const c of model.confirmed) {
261
+ console.error(` ${c.asset} ← ${c.threat} [${c.severity || 'unset'}] (${c.location.file}:${c.location.line})`);
262
+ }
263
+ }
247
264
  const errorCount = allDiags.filter(d => d.level === 'error').length;
248
265
  const hasUnmitigated = unmitigated.length > 0;
249
266
  if (errorCount === 0 && !hasUnmitigated && acceptedOnly.length === 0) {
@@ -275,17 +292,43 @@ program
275
292
  .option('-f, --format <fmt>', 'Output format: md, json, or both (default: md)', 'md')
276
293
  .option('--diagram-only', 'Output only the Mermaid diagram, no report wrapper')
277
294
  .option('--json', 'Also output threat-model.json alongside the report (legacy; prefer --format)')
295
+ .option('--feature <names>', 'Filter report to specific feature(s) (comma-separated)')
278
296
  .action(async (dir, opts) => {
279
297
  const root = resolve(dir);
280
- const { model, diagnostics } = await parseProject({ root, project: opts.project });
281
- // Show errors if any
298
+ let { model, diagnostics } = await parseProject({ root, project: opts.project });
299
+ // Apply feature filter if specified
300
+ if (opts.feature) {
301
+ const featureNames = opts.feature.split(',').map(s => s.trim());
302
+ model = filterByFeature(model, featureNames);
303
+ console.error(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}`);
304
+ }
305
+ // Show errors if any. Per-annotation errors don't block the report —
306
+ // affected annotations are skipped, the rest of the model still renders.
282
307
  const errors = diagnostics.filter(d => d.level === 'error');
283
- if (errors.length > 0) {
308
+ if (errors.length > 0)
284
309
  printDiagnostics(errors);
285
- console.error(`Fix errors above before generating report.\n`);
286
- }
287
310
  // Enrich with provenance metadata (git SHA, branch, workspace, schema version)
288
311
  const enrichedModel = populateMetadata(model, root);
312
+ // Auto-create .guardlink/prompt.md if a v1.4.x project doesn't have it
313
+ // (`init` short-circuits when .guardlink/ exists, so upgrades skip the
314
+ // template). One-line hint on first creation so the user knows it's a
315
+ // feature; silent thereafter.
316
+ const migrationResult = ensurePromptMd(root);
317
+ if (migrationResult === 'created') {
318
+ console.error('• Created .guardlink/prompt.md — fill it in to customize the report\'s Application Overview.');
319
+ }
320
+ // Load project description from .guardlink/prompt.md if it exists
321
+ try {
322
+ const { readFile } = await import('node:fs/promises');
323
+ const promptPath = resolve(root, '.guardlink', 'prompt.md');
324
+ const promptContent = await readFile(promptPath, 'utf-8');
325
+ if (promptContent.trim()) {
326
+ enrichedModel.prompt = promptContent.trim();
327
+ }
328
+ }
329
+ catch {
330
+ // No prompt file — that's fine, report will use annotation-derived overview
331
+ }
289
332
  if (opts.diagramOnly) {
290
333
  // Just output Mermaid
291
334
  const mermaid = generateMermaid(enrichedModel);
@@ -436,8 +479,11 @@ program
436
479
  const { buildProjectContext, extractCodeSnippets } = await import('../analyze/index.js');
437
480
  const projectContext = buildProjectContext(root);
438
481
  const codeSnippets = extractCodeSnippets(root, model);
482
+ const pentestData = loadPentestData(root);
483
+ const pentestContext = serializePentestFindings(pentestData);
439
484
  const systemPrompt = FRAMEWORK_PROMPTS[fw];
440
- const userMessage = buildUserMessage(serialized, fw, customPrompt, projectContext || undefined, codeSnippets || undefined);
485
+ const userMessage = buildUserMessage(serialized, fw, customPrompt, projectContext || undefined, codeSnippets || undefined, pentestContext || undefined);
486
+ const hasPentest = pentestData.totalFindings > 0;
441
487
  const analysisPrompt = `You are analyzing a codebase with GuardLink security annotations.
442
488
  You have access to the full source code in the current directory.
443
489
 
@@ -455,7 +501,8 @@ ${userMessage}
455
501
  3. Produce the full report as markdown
456
502
  4. Be specific — reference actual files, functions, and line numbers from the codebase
457
503
  5. Output ONLY the markdown report content — do NOT add any metadata comments, save confirmations, or file path messages
458
- 6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."`;
504
+ 6. Do NOT include lines like "Generated by...", "Agent:", "Project:", or "The report file write was blocked..."${hasPentest ? `
505
+ 7. The <pentest_findings> section contains CONFIRMED vulnerabilities from automated CXG security scans with real evidence. Cross-reference these against the threat model — mark confirmed findings as exploitable, identify which @exposes are now validated, and include a dedicated "Pentest Results" section summarizing all confirmed findings with their evidence and remediation guidance` : ''}`;
459
506
  // Resolve agent: explicit flag > project config CLI agent
460
507
  let agent = agentFromOpts(opts);
461
508
  if (!agent) {
@@ -663,6 +710,164 @@ program
663
710
  console.log('When done, run: guardlink parse');
664
711
  }
665
712
  });
713
+ // ─── translate ───────────────────────────────────────────────────────
714
+ program
715
+ .command('translate')
716
+ .description('Translate GuardLink threats into CERT-X-GEN pentest templates (generation only, no execution)')
717
+ .argument('[prompt...]', 'Optional translation instructions')
718
+ .option('-d, --dir <dir>', 'Project directory', '.')
719
+ .option('-p, --project <n>', 'Project name', 'unknown')
720
+ .option('--claude-code', 'Launch Claude Code in foreground')
721
+ .option('--codex', 'Launch Codex CLI in foreground')
722
+ .option('--gemini', 'Launch Gemini CLI in foreground')
723
+ .option('--cursor', 'Open Cursor IDE with prompt on clipboard')
724
+ .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard')
725
+ .option('--clipboard', 'Copy translation prompt to clipboard only')
726
+ .option('--feature <names>', 'Filter to specific feature(s) (comma-separated)')
727
+ .action(async (promptParts, opts) => {
728
+ const root = resolve(opts.dir);
729
+ const project = detectProjectName(root, opts.project);
730
+ const userPrompt = promptParts.join(' ').trim();
731
+ let { model, diagnostics } = await parseProject({ root, project });
732
+ // Apply feature filter if specified
733
+ if (opts.feature) {
734
+ const featureNames = opts.feature.split(',').map(s => s.trim());
735
+ model = filterByFeature(model, featureNames);
736
+ console.error(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}`);
737
+ }
738
+ const errors = diagnostics.filter(d => d.level === 'error');
739
+ if (errors.length > 0)
740
+ printDiagnostics(errors);
741
+ if (model.annotations_parsed === 0) {
742
+ console.error('No annotations found. Run: guardlink init . && add annotations first.');
743
+ process.exit(1);
744
+ }
745
+ // Build translate prompt
746
+ const fullPrompt = buildTranslatePrompt(userPrompt, root, model);
747
+ // Resolve agent: explicit flag > project config > default (Claude Code)
748
+ let agent = agentFromOpts(opts);
749
+ if (!agent) {
750
+ const projCfg = loadProjectConfig(root);
751
+ if (projCfg?.aiMode === 'cli-agent' && projCfg?.cliAgent) {
752
+ agent = AGENTS.find(a => a.id === projCfg.cliAgent) || null;
753
+ }
754
+ if (!agent) {
755
+ agent = AGENTS.find(a => a.id === 'claude-code') || null;
756
+ }
757
+ }
758
+ if (!agent) {
759
+ console.error('No agent available. Use one of:');
760
+ for (const a of AGENTS) {
761
+ console.error(` ${a.flag.padEnd(16)} ${a.name}`);
762
+ }
763
+ process.exit(1);
764
+ }
765
+ console.log(`Launching ${agent.name} for CXG template translation...`);
766
+ console.log(`Threat model: ${model.annotations_parsed} annotations, ${model.exposures.length} exposures`);
767
+ if (agent.cmd) {
768
+ console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`);
769
+ }
770
+ const result = launchAgent(agent, fullPrompt, root);
771
+ if (result.clipboardCopied) {
772
+ console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`);
773
+ }
774
+ if (result.error) {
775
+ console.error(`✗ ${result.error}`);
776
+ if (result.clipboardCopied) {
777
+ console.log('Prompt is on your clipboard — paste it manually.');
778
+ }
779
+ process.exit(1);
780
+ }
781
+ if (agent.cmd && result.launched) {
782
+ console.log(`\n✓ ${agent.name} session ended.`);
783
+ console.log(' Expected output location: .guardlink/cxg-templates/');
784
+ console.log(' Note: Templates are generated only, not executed.');
785
+ }
786
+ else if (agent.app && result.launched) {
787
+ console.log(`✓ ${agent.name} launched with project: ${project}`);
788
+ console.log('\nPaste (Cmd+V) the prompt in the AI chat panel.');
789
+ console.log('When done, review .guardlink/cxg-templates/');
790
+ }
791
+ else if (agent.id === 'clipboard') {
792
+ console.log('\nPaste the prompt into your preferred AI tool.');
793
+ console.log('When done, review .guardlink/cxg-templates/');
794
+ }
795
+ });
796
+ // ─── ask ─────────────────────────────────────────────────────────────
797
+ program
798
+ .command('ask')
799
+ .description('Ask questions about this project, its threat model, and security posture')
800
+ .argument('[query...]', 'Question to answer')
801
+ .option('-d, --dir <dir>', 'Project directory', '.')
802
+ .option('-p, --project <n>', 'Project name', 'unknown')
803
+ .option('--claude-code', 'Launch Claude Code in foreground')
804
+ .option('--codex', 'Launch Codex CLI in foreground')
805
+ .option('--gemini', 'Launch Gemini CLI in foreground')
806
+ .option('--cursor', 'Open Cursor IDE with prompt on clipboard')
807
+ .option('--windsurf', 'Open Windsurf IDE with prompt on clipboard')
808
+ .option('--clipboard', 'Copy ask prompt to clipboard only')
809
+ .action(async (queryParts, opts) => {
810
+ const root = resolve(opts.dir);
811
+ const project = detectProjectName(root, opts.project);
812
+ const query = queryParts.join(' ').trim();
813
+ if (!query) {
814
+ console.error('Usage: guardlink ask "<question>" [--claude-code|--codex|--gemini|--cursor|--windsurf|--clipboard]');
815
+ process.exit(1);
816
+ }
817
+ // Parse model if available; allow questions even for lightly-annotated projects
818
+ let model = null;
819
+ try {
820
+ const parsed = await parseProject({ root, project });
821
+ model = parsed.model;
822
+ }
823
+ catch {
824
+ model = null;
825
+ }
826
+ const fullPrompt = buildAskPrompt(query, root, model);
827
+ // Resolve agent: explicit flag > project config > default (Claude Code)
828
+ let agent = agentFromOpts(opts);
829
+ if (!agent) {
830
+ const projCfg = loadProjectConfig(root);
831
+ if (projCfg?.aiMode === 'cli-agent' && projCfg?.cliAgent) {
832
+ agent = AGENTS.find(a => a.id === projCfg.cliAgent) || null;
833
+ }
834
+ if (!agent) {
835
+ agent = AGENTS.find(a => a.id === 'claude-code') || null;
836
+ }
837
+ }
838
+ if (!agent) {
839
+ console.error('No agent available. Use one of:');
840
+ for (const a of AGENTS) {
841
+ console.error(` ${a.flag.padEnd(16)} ${a.name}`);
842
+ }
843
+ process.exit(1);
844
+ }
845
+ console.log(`Launching ${agent.name} for question answering...`);
846
+ if (agent.cmd) {
847
+ console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`);
848
+ }
849
+ const result = launchAgent(agent, fullPrompt, root);
850
+ if (result.clipboardCopied) {
851
+ console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`);
852
+ }
853
+ if (result.error) {
854
+ console.error(`✗ ${result.error}`);
855
+ if (result.clipboardCopied) {
856
+ console.log('Prompt is on your clipboard — paste it manually.');
857
+ }
858
+ process.exit(1);
859
+ }
860
+ if (agent.cmd && result.launched) {
861
+ console.log(`\n✓ ${agent.name} session ended.`);
862
+ }
863
+ else if (agent.app && result.launched) {
864
+ console.log(`✓ ${agent.name} launched with project: ${project}`);
865
+ console.log('\nPaste (Cmd+V) the prompt in the AI chat panel.');
866
+ }
867
+ else if (agent.id === 'clipboard') {
868
+ console.log('\nPaste the prompt into your preferred AI tool.');
869
+ }
870
+ });
666
871
  // ─── clear ───────────────────────────────────────────────────────────
667
872
  program
668
873
  .command('clear')
@@ -859,7 +1064,7 @@ program
859
1064
  .command('config')
860
1065
  .description('Manage LLM provider configuration')
861
1066
  .argument('<action>', 'Action: set, show, clear')
862
- .argument('[key]', 'Config key: provider, api-key, model, ai-mode, cli-agent')
1067
+ .argument('[key]', 'Config key: provider, api-key, model, ai-mode, cli-agent, redact-evidence')
863
1068
  .argument('[value]', 'Value to set')
864
1069
  .option('--global', 'Use global config (~/.config/guardlink/) instead of project')
865
1070
  .action(async (action, key, value, opts) => {
@@ -872,12 +1077,13 @@ program
872
1077
  const projCfg = isGlobal ? loadGlobalConfig() : loadProjectConfig(root);
873
1078
  const aiMode = projCfg?.aiMode || 'api';
874
1079
  const cliAgent = projCfg?.cliAgent;
875
- console.log(`AI Mode: ${aiMode}${cliAgent ? ` (${cliAgent})` : ''}`);
1080
+ const redactEvidence = projCfg?.redactEvidence === true;
1081
+ console.log(`AI Mode: ${aiMode}${cliAgent ? ` (${cliAgent})` : ''}`);
876
1082
  if (config) {
877
- console.log(`Provider: ${config.provider}`);
878
- console.log(`Model: ${config.model}`);
879
- console.log(`API Key: ${maskKey(config.apiKey)}`);
880
- console.log(`Source: ${source}`);
1083
+ console.log(`Provider: ${config.provider}`);
1084
+ console.log(`Model: ${config.model}`);
1085
+ console.log(`API Key: ${maskKey(config.apiKey)}`);
1086
+ console.log(`Source: ${source}`);
881
1087
  }
882
1088
  else if (aiMode !== 'cli-agent') {
883
1089
  console.log('No LLM configuration found.');
@@ -891,6 +1097,7 @@ program
891
1097
  console.log(' export GUARDLINK_LLM_KEY=sk-ant-...');
892
1098
  console.log(' export GUARDLINK_LLM_PROVIDER=anthropic');
893
1099
  }
1100
+ console.log(`Redact evidence: ${redactEvidence ? 'on (pentest evidence is surgically redacted at load)' : 'off (full evidence; see docs/handling-evidence.md)'}`);
894
1101
  break;
895
1102
  }
896
1103
  case 'set': {
@@ -936,8 +1143,26 @@ program
936
1143
  existing.cliAgent = value;
937
1144
  existing.aiMode = 'cli-agent';
938
1145
  break;
1146
+ case 'redact-evidence': {
1147
+ // Accept truthy/falsy spellings — `true`, `false`, `on`, `off`,
1148
+ // `1`, `0`. Reject anything else so a typo doesn't silently
1149
+ // enable redaction the user didn't want.
1150
+ const v = value.toLowerCase();
1151
+ if (['true', 'on', '1', 'yes'].includes(v)) {
1152
+ existing.redactEvidence = true;
1153
+ }
1154
+ else if (['false', 'off', '0', 'no'].includes(v)) {
1155
+ existing.redactEvidence = false;
1156
+ }
1157
+ else {
1158
+ console.error(`Invalid value for redact-evidence: ${value}`);
1159
+ console.error('Use: true | false (also accepted: on/off, 1/0, yes/no)');
1160
+ process.exit(1);
1161
+ }
1162
+ break;
1163
+ }
939
1164
  default:
940
- console.error(`Unknown config key: ${key}. Use: provider, api-key, model, ai-mode, cli-agent`);
1165
+ console.error(`Unknown config key: ${key}. Use: provider, api-key, model, ai-mode, cli-agent, redact-evidence`);
941
1166
  process.exit(1);
942
1167
  }
943
1168
  if (isGlobal) {
@@ -975,19 +1200,27 @@ program
975
1200
  .option('-p, --project <n>', 'Project name', 'unknown')
976
1201
  .option('-o, --output <file>', 'Output file (default: threat-dashboard.html)')
977
1202
  .option('--light', 'Default to light theme instead of dark')
1203
+ .option('--feature <names>', 'Filter dashboard to specific feature(s) (comma-separated)')
978
1204
  .action(async (dir, opts) => {
979
1205
  const root = resolve(dir);
980
1206
  const project = detectProjectName(root, opts.project);
981
- const { model, diagnostics } = await parseProject({ root, project });
1207
+ let { model, diagnostics } = await parseProject({ root, project });
1208
+ // Apply feature filter if specified
1209
+ if (opts.feature) {
1210
+ const featureNames = opts.feature.split(',').map(s => s.trim());
1211
+ model = filterByFeature(model, featureNames);
1212
+ console.error(`Filtered to feature(s): ${featureNames.map(f => `"${f}"`).join(', ')}`);
1213
+ }
982
1214
  const errors = diagnostics.filter(d => d.level === 'error');
983
1215
  if (errors.length > 0)
984
1216
  printDiagnostics(errors);
985
- if (model.annotations_parsed === 0) {
1217
+ if (model.annotations_parsed === 0 && !opts.feature) {
986
1218
  console.error('No annotations found. Add GuardLink annotations first.');
987
1219
  process.exit(1);
988
1220
  }
989
1221
  const analyses = loadThreatReportsForDashboard(root);
990
- let html = generateDashboardHTML(model, root, analyses);
1222
+ const pentestData = loadPentestData(root);
1223
+ let html = generateDashboardHTML(model, root, analyses, pentestData);
991
1224
  // Switch default theme if requested
992
1225
  if (opts.light) {
993
1226
  html = html.replace('data-theme="dark"', 'data-theme="light"');
@@ -1216,6 +1449,94 @@ program
1216
1449
  // Print full markdown summary to stdout (pipeable)
1217
1450
  console.log(formatMergeSummary(merged));
1218
1451
  });
1452
+ // ─── feature ──────────────────────────────────────────────────────────
1453
+ const featureCmd = program
1454
+ .command('feature')
1455
+ .description('Manage and inspect feature tags across the threat model');
1456
+ featureCmd
1457
+ .command('list')
1458
+ .description('List all features found in annotations')
1459
+ .argument('[dir]', 'Project directory to scan', '.')
1460
+ .option('-p, --project <n>', 'Project name', 'unknown')
1461
+ .option('--json', 'Output as JSON')
1462
+ .action(async (dir, opts) => {
1463
+ const root = resolve(dir);
1464
+ const project = detectProjectName(root, opts.project);
1465
+ const { model } = await parseProject({ root, project });
1466
+ const features = listFeatures(model);
1467
+ if (features.length === 0) {
1468
+ console.log('No @feature annotations found.');
1469
+ console.log('Tag code with: // @feature "Feature Name" -- "description"');
1470
+ return;
1471
+ }
1472
+ if (opts.json) {
1473
+ const summaries = getFeatureSummaries(model);
1474
+ console.log(JSON.stringify(summaries, null, 2));
1475
+ return;
1476
+ }
1477
+ const summaries = getFeatureSummaries(model);
1478
+ console.log(`Features in ${project}:\n`);
1479
+ for (const s of summaries) {
1480
+ const files = s.files.length === 1 ? '1 file' : `${s.files.length} files`;
1481
+ const exposureInfo = s.exposures > 0 ? ` | ${s.exposures} exposure(s)` : '';
1482
+ const confirmedInfo = s.confirmed > 0 ? ` | ${s.confirmed} confirmed` : '';
1483
+ console.log(` "${s.name}" (${files}, ${s.annotations} annotations${exposureInfo}${confirmedInfo})`);
1484
+ for (const f of s.files) {
1485
+ console.log(` → ${f}`);
1486
+ }
1487
+ }
1488
+ console.log(`\n${features.length} feature(s) total`);
1489
+ });
1490
+ featureCmd
1491
+ .command('show')
1492
+ .description('Show detailed threat model for a specific feature')
1493
+ .argument('<name>', 'Feature name (case-insensitive)')
1494
+ .option('-d, --dir <dir>', 'Project directory', '.')
1495
+ .option('-p, --project <n>', 'Project name', 'unknown')
1496
+ .option('--json', 'Output as JSON')
1497
+ .action(async (name, opts) => {
1498
+ const root = resolve(opts.dir);
1499
+ const project = detectProjectName(root, opts.project);
1500
+ const { model } = await parseProject({ root, project });
1501
+ const filtered = filterByFeature(model, [name]);
1502
+ const totalAnnotations = filtered.assets.length + filtered.threats.length +
1503
+ filtered.controls.length + filtered.mitigations.length + filtered.exposures.length +
1504
+ filtered.confirmed.length + filtered.flows.length + filtered.boundaries.length;
1505
+ if (totalAnnotations === 0) {
1506
+ console.error(`No annotations found for feature "${name}".`);
1507
+ const available = listFeatures(model);
1508
+ if (available.length > 0) {
1509
+ console.error(`Available features: ${available.map(f => `"${f}"`).join(', ')}`);
1510
+ }
1511
+ process.exit(1);
1512
+ }
1513
+ if (opts.json) {
1514
+ console.log(JSON.stringify(filtered, null, 2));
1515
+ return;
1516
+ }
1517
+ console.log(`Feature: "${name}"\n`);
1518
+ console.log(` Assets: ${filtered.assets.length}`);
1519
+ console.log(` Threats: ${filtered.threats.length}`);
1520
+ console.log(` Controls: ${filtered.controls.length}`);
1521
+ console.log(` Mitigations: ${filtered.mitigations.length}`);
1522
+ console.log(` Exposures: ${filtered.exposures.length}`);
1523
+ if (filtered.confirmed.length > 0)
1524
+ console.log(` Confirmed: ${filtered.confirmed.length} 🔴`);
1525
+ console.log(` Flows: ${filtered.flows.length}`);
1526
+ console.log(` Boundaries: ${filtered.boundaries.length}`);
1527
+ if (filtered.exposures.length > 0) {
1528
+ console.log(`\n Exposures:`);
1529
+ for (const e of filtered.exposures) {
1530
+ console.log(` ${e.asset} → ${e.threat} [${e.severity || 'unset'}] (${e.location.file}:${e.location.line})`);
1531
+ }
1532
+ }
1533
+ if (filtered.mitigations.length > 0) {
1534
+ console.log(`\n Mitigations:`);
1535
+ for (const m of filtered.mitigations) {
1536
+ console.log(` ${m.asset} against ${m.threat}${m.control ? ` using ${m.control}` : ''} (${m.location.file}:${m.location.line})`);
1537
+ }
1538
+ }
1539
+ });
1219
1540
  // ─── mcp ─────────────────────────────────────────────────────────────
1220
1541
  program
1221
1542
  .command('mcp')
@@ -1297,6 +1618,12 @@ program
1297
1618
  console.log(EX(' // @mitigates api.auth against SQL Injection using Input Validation'));
1298
1619
  console.log(EX(' // @mitigates db.users against Token Theft -- "Rotation implemented in v2"'));
1299
1620
  console.log('');
1621
+ console.log(` ${V('@confirmed')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[severity]')} ${D('[ext-refs]')} ${D('[-- "evidence"]')}`);
1622
+ console.log(D(' Mark a threat as verified exploitable (pentest, scan, or manual repro).'));
1623
+ console.log(D(' Not a false positive — use observed severity. Distinct from @exposes (hypothesis).'));
1624
+ console.log(EX(' // @confirmed SQL Injection on api.auth [critical] cwe:CWE-89 -- "Pen test: blind SQLi on /login"'));
1625
+ console.log(EX(' // @confirmed #secret-exposure on App.Config [critical] -- "Live key in repo; verified with provider"'));
1626
+ console.log('');
1300
1627
  console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[-- "reason"]')}`);
1301
1628
  console.log(D(' Explicitly accept a risk. Removes it from open findings.'));
1302
1629
  console.log(D(' Use when the risk is known and intentionally not mitigated.'));
@@ -1347,6 +1674,21 @@ program
1347
1674
  console.log(D(' Document a security assumption about an asset.'));
1348
1675
  console.log(EX(' // @assumes api.gateway -- "Upstream WAF filters malformed requests"'));
1349
1676
  console.log('');
1677
+ // ── FEATURE TAGGING ──
1678
+ console.log(H(' ── Feature Tagging ─────────────────────────────────────────'));
1679
+ console.log('');
1680
+ console.log(` ${V('@feature')} ${K('"Feature Name"')} ${D('[-- "description"]')}`);
1681
+ console.log(D(' Tag code with a feature name for filtering reports and dashboards.'));
1682
+ console.log(D(' A file can have multiple @feature tags. All annotations in that file'));
1683
+ console.log(D(' are associated with the tagged features.'));
1684
+ console.log(EX(' // @feature "SSO Login" -- "Single sign-on authentication flow"'));
1685
+ console.log(EX(' // @feature "Payment Processing"'));
1686
+ console.log(D(''));
1687
+ console.log(D(' Filter commands by feature:'));
1688
+ console.log(EX(' guardlink feature list'));
1689
+ console.log(EX(' guardlink report . --feature "SSO Login"'));
1690
+ console.log(EX(' guardlink dashboard . --feature "SSO Login,Payment Processing"'));
1691
+ console.log('');
1350
1692
  console.log(` ${V('@comment')} ${D('[-- "description"]')}`);
1351
1693
  console.log(D(' Free-form developer security note (no structural effect).'));
1352
1694
  console.log(EX(' // @comment -- "TODO — add rate limiting before v2 launch"'));
@@ -1368,7 +1710,7 @@ program
1368
1710
  // ── EXTERNAL REFERENCES ──
1369
1711
  console.log(H(' ── External References ─────────────────────────────────────'));
1370
1712
  console.log('');
1371
- console.log(D(' Append space-separated refs after severity on @threat and @exposes:'));
1713
+ console.log(D(' Append space-separated refs after severity on @threat, @exposes, and @confirmed:'));
1372
1714
  console.log(EX(' cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66 attack:T1190'));
1373
1715
  console.log('');
1374
1716
  console.log(D(' Example:'));
@@ -1402,23 +1744,27 @@ else {
1402
1744
  // ─── Helpers ─────────────────────────────────────────────────────────
1403
1745
  function printDiagnostics(diagnostics) {
1404
1746
  for (const d of diagnostics) {
1405
- const prefix = d.level === 'error' ? '✗' : '⚠';
1406
- console.error(`${prefix} ${d.file}:${d.line}: ${d.message}`);
1747
+ console.error(`${diagnosticIcon(d.level)} ${d.file}:${d.line}: ${d.message}`);
1407
1748
  if (d.raw)
1408
1749
  console.error(` → ${d.raw}`);
1409
1750
  }
1410
1751
  if (diagnostics.length > 0) {
1752
+ const fatals = diagnostics.filter(d => d.level === 'fatal').length;
1411
1753
  const errors = diagnostics.filter(d => d.level === 'error').length;
1412
1754
  const warnings = diagnostics.filter(d => d.level === 'warning').length;
1413
- console.error(`\n${errors} error(s), ${warnings} warning(s)\n`);
1755
+ const parts = [];
1756
+ if (fatals > 0)
1757
+ parts.push(`${fatals} fatal(s)`);
1758
+ parts.push(`${errors} error(s)`, `${warnings} warning(s)`);
1759
+ console.error(`\n${parts.join(', ')}\n`);
1414
1760
  }
1415
1761
  }
1416
1762
  function printStatus(model) {
1417
1763
  console.log(`GuardLink Status: ${model.project}`);
1418
1764
  console.log(`${'─'.repeat(40)}`);
1419
1765
  console.log(`Files scanned: ${model.source_files}`);
1420
- console.log(` Annotated: ${model.annotated_files.length}`);
1421
- console.log(` Not annotated: ${model.unannotated_files.length}`);
1766
+ console.log(` Files annotated: ${model.annotated_files.length}`);
1767
+ console.log(` Files unannotated: ${model.unannotated_files.length}`);
1422
1768
  console.log(`Annotations: ${model.annotations_parsed}`);
1423
1769
  console.log(`${'─'.repeat(40)}`);
1424
1770
  console.log(`Assets: ${model.assets.length}`);
@@ -1426,6 +1772,8 @@ function printStatus(model) {
1426
1772
  console.log(`Controls: ${model.controls.length}`);
1427
1773
  console.log(`Mitigations: ${model.mitigations.length}`);
1428
1774
  console.log(`Exposures: ${model.exposures.length}`);
1775
+ if ((model.confirmed || []).length > 0)
1776
+ console.log(`Confirmed: ${model.confirmed.length} 🔴`);
1429
1777
  console.log(`Acceptances: ${model.acceptances.length}`);
1430
1778
  console.log(`Transfers: ${model.transfers.length}`);
1431
1779
  console.log(`Flows: ${model.flows.length}`);
@@ -1435,6 +1783,10 @@ function printStatus(model) {
1435
1783
  console.log(`Ownership: ${model.ownership.length}`);
1436
1784
  console.log(`Data handling: ${model.data_handling.length}`);
1437
1785
  console.log(`Assumptions: ${model.assumptions.length}`);
1786
+ if (model.features.length > 0) {
1787
+ const uniqueFeatures = new Set(model.features.map(f => f.feature));
1788
+ console.log(`Features: ${uniqueFeatures.size} (${model.features.length} tag${model.features.length > 1 ? 's' : ''})`);
1789
+ }
1438
1790
  console.log(`Comments: ${model.comments.length}`);
1439
1791
  console.log(`Shields: ${model.shields.length}`);
1440
1792
  }