guardlink 1.4.1 → 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.
- package/CHANGELOG.md +111 -7
- package/README.md +53 -5
- package/dist/agents/config.d.ts +7 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +9 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +36 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/launcher.d.ts.map +1 -1
- package/dist/agents/launcher.js +5 -0
- package/dist/agents/launcher.js.map +1 -1
- package/dist/agents/prompts.d.ts +16 -1
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +511 -16
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/format.d.ts +72 -0
- package/dist/analyze/format.d.ts.map +1 -0
- package/dist/analyze/format.js +176 -0
- package/dist/analyze/format.js.map +1 -0
- package/dist/analyze/index.d.ts +76 -0
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +165 -2
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/prompts.d.ts +3 -2
- package/dist/analyze/prompts.d.ts.map +1 -1
- package/dist/analyze/prompts.js +17 -3
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyzer/sarif.d.ts +3 -2
- package/dist/analyzer/sarif.d.ts.map +1 -1
- package/dist/analyzer/sarif.js +29 -3
- package/dist/analyzer/sarif.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +408 -37
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/data.d.ts +11 -0
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +12 -0
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/diagrams.d.ts +81 -12
- package/dist/dashboard/diagrams.d.ts.map +1 -1
- package/dist/dashboard/diagrams.js +750 -362
- package/dist/dashboard/diagrams.js.map +1 -1
- package/dist/dashboard/generate.d.ts +5 -2
- package/dist/dashboard/generate.d.ts.map +1 -1
- package/dist/dashboard/generate.js +2516 -244
- package/dist/dashboard/generate.js.map +1 -1
- package/dist/diff/engine.d.ts +2 -1
- package/dist/diff/engine.d.ts.map +1 -1
- package/dist/diff/engine.js +3 -2
- package/dist/diff/engine.js.map +1 -1
- package/dist/diff/git.js +3 -3
- package/dist/diff/git.js.map +1 -1
- package/dist/init/index.d.ts +7 -0
- package/dist/init/index.d.ts.map +1 -1
- package/dist/init/index.js +82 -27
- package/dist/init/index.js.map +1 -1
- package/dist/init/migrate.d.ts +39 -0
- package/dist/init/migrate.d.ts.map +1 -0
- package/dist/init/migrate.js +45 -0
- package/dist/init/migrate.js.map +1 -0
- package/dist/init/templates.d.ts +8 -0
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +68 -6
- package/dist/init/templates.js.map +1 -1
- package/dist/mcp/lookup.d.ts +1 -0
- package/dist/mcp/lookup.d.ts.map +1 -1
- package/dist/mcp/lookup.js +138 -10
- package/dist/mcp/lookup.js.map +1 -1
- package/dist/mcp/server.d.ts +2 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +32 -15
- package/dist/mcp/server.js.map +1 -1
- package/dist/parser/clear.d.ts +2 -1
- package/dist/parser/clear.d.ts.map +1 -1
- package/dist/parser/clear.js +19 -29
- package/dist/parser/clear.js.map +1 -1
- package/dist/parser/comment-strip.d.ts +5 -0
- package/dist/parser/comment-strip.d.ts.map +1 -1
- package/dist/parser/comment-strip.js +8 -0
- package/dist/parser/comment-strip.js.map +1 -1
- package/dist/parser/feature-filter.d.ts +42 -0
- package/dist/parser/feature-filter.d.ts.map +1 -0
- package/dist/parser/feature-filter.js +109 -0
- package/dist/parser/feature-filter.js.map +1 -0
- package/dist/parser/format.d.ts +24 -0
- package/dist/parser/format.d.ts.map +1 -0
- package/dist/parser/format.js +29 -0
- package/dist/parser/format.js.map +1 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parse-file.d.ts +1 -0
- package/dist/parser/parse-file.d.ts.map +1 -1
- package/dist/parser/parse-file.js +34 -9
- package/dist/parser/parse-file.js.map +1 -1
- package/dist/parser/parse-line.d.ts +9 -0
- package/dist/parser/parse-line.d.ts.map +1 -1
- package/dist/parser/parse-line.js +100 -26
- package/dist/parser/parse-line.js.map +1 -1
- package/dist/parser/parse-project.d.ts +1 -0
- package/dist/parser/parse-project.d.ts.map +1 -1
- package/dist/parser/parse-project.js +36 -2
- package/dist/parser/parse-project.js.map +1 -1
- package/dist/parser/validate.d.ts +3 -0
- package/dist/parser/validate.d.ts.map +1 -1
- package/dist/parser/validate.js +7 -0
- package/dist/parser/validate.js.map +1 -1
- package/dist/report/index.d.ts +1 -0
- package/dist/report/index.d.ts.map +1 -1
- package/dist/report/index.js +1 -0
- package/dist/report/index.js.map +1 -1
- package/dist/report/report.d.ts.map +1 -1
- package/dist/report/report.js +924 -24
- package/dist/report/report.js.map +1 -1
- package/dist/report/sequence.d.ts +11 -0
- package/dist/report/sequence.d.ts.map +1 -0
- package/dist/report/sequence.js +140 -0
- package/dist/report/sequence.js.map +1 -0
- package/dist/review/index.d.ts +3 -1
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +77 -35
- package/dist/review/index.js.map +1 -1
- package/dist/tui/commands.d.ts +1 -0
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +98 -12
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +7 -2
- package/dist/tui/index.js.map +1 -1
- package/dist/types/index.d.ts +59 -3
- package/dist/types/index.d.ts.map +1 -1
- package/dist/workspace/merge.d.ts.map +1 -1
- package/dist/workspace/merge.js +6 -2
- package/dist/workspace/merge.js.map +1 -1
- 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 } 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.
|
|
102
|
+
.version('1.4.3')
|
|
99
103
|
.addHelpText('before', gradient(['#00ff41', '#00d4ff'])(ASCII_LOGO));
|
|
100
104
|
// ─── init ────────────────────────────────────────────────────────────
|
|
101
105
|
program
|
|
@@ -104,6 +108,7 @@ program
|
|
|
104
108
|
.argument('[dir]', 'Project directory', '.')
|
|
105
109
|
.option('-p, --project <n>', 'Override project name')
|
|
106
110
|
.option('-a, --agent <agents>', 'Agent(s) to create files for: claude,cursor,codex,copilot,windsurf,cline,none (comma-separated)')
|
|
111
|
+
.option('--mode <mode>', 'Annotation mode: inline (default) or external. external restricts all writes to .guardlink/ — no agent files, no .mcp.json at root', 'inline')
|
|
107
112
|
.option('--skip-agent-files', 'Only create .guardlink/, skip agent file updates')
|
|
108
113
|
.option('--force', 'Overwrite existing GuardLink config and instructions')
|
|
109
114
|
.option('--dry-run', 'Show what would be created without writing files')
|
|
@@ -140,6 +145,7 @@ program
|
|
|
140
145
|
const result = initProject({
|
|
141
146
|
root,
|
|
142
147
|
project: opts.project,
|
|
148
|
+
mode: resolveAnnotationMode(opts.mode),
|
|
143
149
|
skipAgentFiles: opts.skipAgentFiles,
|
|
144
150
|
force: opts.force,
|
|
145
151
|
dryRun: opts.dryRun,
|
|
@@ -194,9 +200,16 @@ program
|
|
|
194
200
|
.argument('[dir]', 'Project directory to scan', '.')
|
|
195
201
|
.option('-p, --project <n>', 'Project name', 'unknown')
|
|
196
202
|
.option('--not-annotated', 'List source files with no GuardLink annotations')
|
|
203
|
+
.option('--feature <names>', 'Filter status to specific feature(s) (comma-separated)')
|
|
197
204
|
.action(async (dir, opts) => {
|
|
198
205
|
const root = resolve(dir);
|
|
199
|
-
|
|
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
|
+
}
|
|
200
213
|
printDiagnostics(diagnostics);
|
|
201
214
|
printStatus(model);
|
|
202
215
|
if (opts.notAnnotated) {
|
|
@@ -242,6 +255,12 @@ program
|
|
|
242
255
|
console.error(` ${a.asset} → ${a.threat} [${a.severity || 'unset'}] (${a.location.file}:${a.location.line})`);
|
|
243
256
|
}
|
|
244
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
|
+
}
|
|
245
264
|
const errorCount = allDiags.filter(d => d.level === 'error').length;
|
|
246
265
|
const hasUnmitigated = unmitigated.length > 0;
|
|
247
266
|
if (errorCount === 0 && !hasUnmitigated && acceptedOnly.length === 0) {
|
|
@@ -273,17 +292,43 @@ program
|
|
|
273
292
|
.option('-f, --format <fmt>', 'Output format: md, json, or both (default: md)', 'md')
|
|
274
293
|
.option('--diagram-only', 'Output only the Mermaid diagram, no report wrapper')
|
|
275
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)')
|
|
276
296
|
.action(async (dir, opts) => {
|
|
277
297
|
const root = resolve(dir);
|
|
278
|
-
|
|
279
|
-
//
|
|
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.
|
|
280
307
|
const errors = diagnostics.filter(d => d.level === 'error');
|
|
281
|
-
if (errors.length > 0)
|
|
308
|
+
if (errors.length > 0)
|
|
282
309
|
printDiagnostics(errors);
|
|
283
|
-
console.error(`Fix errors above before generating report.\n`);
|
|
284
|
-
}
|
|
285
310
|
// Enrich with provenance metadata (git SHA, branch, workspace, schema version)
|
|
286
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
|
+
}
|
|
287
332
|
if (opts.diagramOnly) {
|
|
288
333
|
// Just output Mermaid
|
|
289
334
|
const mermaid = generateMermaid(enrichedModel);
|
|
@@ -434,8 +479,11 @@ program
|
|
|
434
479
|
const { buildProjectContext, extractCodeSnippets } = await import('../analyze/index.js');
|
|
435
480
|
const projectContext = buildProjectContext(root);
|
|
436
481
|
const codeSnippets = extractCodeSnippets(root, model);
|
|
482
|
+
const pentestData = loadPentestData(root);
|
|
483
|
+
const pentestContext = serializePentestFindings(pentestData);
|
|
437
484
|
const systemPrompt = FRAMEWORK_PROMPTS[fw];
|
|
438
|
-
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;
|
|
439
487
|
const analysisPrompt = `You are analyzing a codebase with GuardLink security annotations.
|
|
440
488
|
You have access to the full source code in the current directory.
|
|
441
489
|
|
|
@@ -453,7 +501,8 @@ ${userMessage}
|
|
|
453
501
|
3. Produce the full report as markdown
|
|
454
502
|
4. Be specific — reference actual files, functions, and line numbers from the codebase
|
|
455
503
|
5. Output ONLY the markdown report content — do NOT add any metadata comments, save confirmations, or file path messages
|
|
456
|
-
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` : ''}`;
|
|
457
506
|
// Resolve agent: explicit flag > project config CLI agent
|
|
458
507
|
let agent = agentFromOpts(opts);
|
|
459
508
|
if (!agent) {
|
|
@@ -586,15 +635,25 @@ program
|
|
|
586
635
|
.argument('<prompt>', 'Annotation instructions (e.g., "annotate auth endpoints for OWASP Top 10")')
|
|
587
636
|
.argument('[dir]', 'Project directory', '.')
|
|
588
637
|
.option('-p, --project <n>', 'Project name', 'unknown')
|
|
638
|
+
.option('--mode <mode>', 'Annotation placement mode: inline (default) or external (externalized .gal files)', 'inline')
|
|
589
639
|
.option('--claude-code', 'Launch Claude Code in foreground')
|
|
590
640
|
.option('--codex', 'Launch Codex CLI in foreground')
|
|
591
641
|
.option('--gemini', 'Launch Gemini CLI in foreground')
|
|
592
642
|
.option('--cursor', 'Open Cursor IDE with prompt on clipboard')
|
|
593
643
|
.option('--windsurf', 'Open Windsurf IDE with prompt on clipboard')
|
|
594
644
|
.option('--clipboard', 'Copy annotation prompt to clipboard only')
|
|
645
|
+
.option('--stdout', 'Print annotation prompt to stdout and exit (for piping)')
|
|
595
646
|
.action(async (prompt, dir, opts) => {
|
|
596
647
|
const root = resolve(dir);
|
|
597
648
|
const project = detectProjectName(root, opts.project);
|
|
649
|
+
let annotationMode;
|
|
650
|
+
try {
|
|
651
|
+
annotationMode = resolveAnnotationMode(opts.mode);
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
console.error(err.message);
|
|
655
|
+
process.exit(1);
|
|
656
|
+
}
|
|
598
657
|
// Resolve agent
|
|
599
658
|
const agent = agentFromOpts(opts);
|
|
600
659
|
if (!agent) {
|
|
@@ -614,13 +673,18 @@ program
|
|
|
614
673
|
}
|
|
615
674
|
catch { /* no model yet — that's fine */ }
|
|
616
675
|
// Build prompt
|
|
617
|
-
const fullPrompt = buildAnnotatePrompt(prompt, root, model);
|
|
676
|
+
const fullPrompt = buildAnnotatePrompt(prompt, root, model, annotationMode);
|
|
618
677
|
// Launch agent
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
678
|
+
if (agent.id !== 'stdout') {
|
|
679
|
+
console.log(`Launching ${agent.name} for annotation...`);
|
|
680
|
+
if (agent.cmd) {
|
|
681
|
+
console.log(`${agent.name} will take over this terminal. Exit the agent to return.\n`);
|
|
682
|
+
}
|
|
622
683
|
}
|
|
623
684
|
const result = launchAgent(agent, fullPrompt, root);
|
|
685
|
+
// stdout mode: prompt already written to stdout — nothing else to do
|
|
686
|
+
if (agent.id === 'stdout')
|
|
687
|
+
return;
|
|
624
688
|
if (result.clipboardCopied) {
|
|
625
689
|
console.log(`✓ Prompt copied to clipboard (${fullPrompt.length.toLocaleString()} chars)`);
|
|
626
690
|
}
|
|
@@ -646,6 +710,164 @@ program
|
|
|
646
710
|
console.log('When done, run: guardlink parse');
|
|
647
711
|
}
|
|
648
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
|
+
});
|
|
649
871
|
// ─── clear ───────────────────────────────────────────────────────────
|
|
650
872
|
program
|
|
651
873
|
.command('clear')
|
|
@@ -803,7 +1025,7 @@ program
|
|
|
803
1025
|
}
|
|
804
1026
|
const result = await applyReviewAction(root, reviewable, { decision: 'accept', justification });
|
|
805
1027
|
results.push(result);
|
|
806
|
-
console.error(` ✓ Accepted — ${result.linesInserted} line(s) written to ${
|
|
1028
|
+
console.error(` ✓ Accepted — ${result.linesInserted} line(s) written to ${result.targetFile}\n`);
|
|
807
1029
|
}
|
|
808
1030
|
else if (choice === 'r') {
|
|
809
1031
|
let note = '';
|
|
@@ -814,10 +1036,10 @@ program
|
|
|
814
1036
|
}
|
|
815
1037
|
const result = await applyReviewAction(root, reviewable, { decision: 'remediate', justification: note });
|
|
816
1038
|
results.push(result);
|
|
817
|
-
console.error(` ✓ Marked for remediation — ${result.linesInserted} line(s) written to ${
|
|
1039
|
+
console.error(` ✓ Marked for remediation — ${result.linesInserted} line(s) written to ${result.targetFile}\n`);
|
|
818
1040
|
}
|
|
819
1041
|
else {
|
|
820
|
-
results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0 });
|
|
1042
|
+
results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0, targetFile: reviewable.exposure.location.file });
|
|
821
1043
|
console.error(' — Skipped\n');
|
|
822
1044
|
}
|
|
823
1045
|
}
|
|
@@ -842,7 +1064,7 @@ program
|
|
|
842
1064
|
.command('config')
|
|
843
1065
|
.description('Manage LLM provider configuration')
|
|
844
1066
|
.argument('<action>', 'Action: set, show, clear')
|
|
845
|
-
.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')
|
|
846
1068
|
.argument('[value]', 'Value to set')
|
|
847
1069
|
.option('--global', 'Use global config (~/.config/guardlink/) instead of project')
|
|
848
1070
|
.action(async (action, key, value, opts) => {
|
|
@@ -855,12 +1077,13 @@ program
|
|
|
855
1077
|
const projCfg = isGlobal ? loadGlobalConfig() : loadProjectConfig(root);
|
|
856
1078
|
const aiMode = projCfg?.aiMode || 'api';
|
|
857
1079
|
const cliAgent = projCfg?.cliAgent;
|
|
858
|
-
|
|
1080
|
+
const redactEvidence = projCfg?.redactEvidence === true;
|
|
1081
|
+
console.log(`AI Mode: ${aiMode}${cliAgent ? ` (${cliAgent})` : ''}`);
|
|
859
1082
|
if (config) {
|
|
860
|
-
console.log(`Provider:
|
|
861
|
-
console.log(`Model:
|
|
862
|
-
console.log(`API Key:
|
|
863
|
-
console.log(`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}`);
|
|
864
1087
|
}
|
|
865
1088
|
else if (aiMode !== 'cli-agent') {
|
|
866
1089
|
console.log('No LLM configuration found.');
|
|
@@ -874,6 +1097,7 @@ program
|
|
|
874
1097
|
console.log(' export GUARDLINK_LLM_KEY=sk-ant-...');
|
|
875
1098
|
console.log(' export GUARDLINK_LLM_PROVIDER=anthropic');
|
|
876
1099
|
}
|
|
1100
|
+
console.log(`Redact evidence: ${redactEvidence ? 'on (pentest evidence is surgically redacted at load)' : 'off (full evidence; see docs/handling-evidence.md)'}`);
|
|
877
1101
|
break;
|
|
878
1102
|
}
|
|
879
1103
|
case 'set': {
|
|
@@ -919,8 +1143,26 @@ program
|
|
|
919
1143
|
existing.cliAgent = value;
|
|
920
1144
|
existing.aiMode = 'cli-agent';
|
|
921
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
|
+
}
|
|
922
1164
|
default:
|
|
923
|
-
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`);
|
|
924
1166
|
process.exit(1);
|
|
925
1167
|
}
|
|
926
1168
|
if (isGlobal) {
|
|
@@ -958,19 +1200,27 @@ program
|
|
|
958
1200
|
.option('-p, --project <n>', 'Project name', 'unknown')
|
|
959
1201
|
.option('-o, --output <file>', 'Output file (default: threat-dashboard.html)')
|
|
960
1202
|
.option('--light', 'Default to light theme instead of dark')
|
|
1203
|
+
.option('--feature <names>', 'Filter dashboard to specific feature(s) (comma-separated)')
|
|
961
1204
|
.action(async (dir, opts) => {
|
|
962
1205
|
const root = resolve(dir);
|
|
963
1206
|
const project = detectProjectName(root, opts.project);
|
|
964
|
-
|
|
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
|
+
}
|
|
965
1214
|
const errors = diagnostics.filter(d => d.level === 'error');
|
|
966
1215
|
if (errors.length > 0)
|
|
967
1216
|
printDiagnostics(errors);
|
|
968
|
-
if (model.annotations_parsed === 0) {
|
|
1217
|
+
if (model.annotations_parsed === 0 && !opts.feature) {
|
|
969
1218
|
console.error('No annotations found. Add GuardLink annotations first.');
|
|
970
1219
|
process.exit(1);
|
|
971
1220
|
}
|
|
972
1221
|
const analyses = loadThreatReportsForDashboard(root);
|
|
973
|
-
|
|
1222
|
+
const pentestData = loadPentestData(root);
|
|
1223
|
+
let html = generateDashboardHTML(model, root, analyses, pentestData);
|
|
974
1224
|
// Switch default theme if requested
|
|
975
1225
|
if (opts.light) {
|
|
976
1226
|
html = html.replace('data-theme="dark"', 'data-theme="light"');
|
|
@@ -1199,6 +1449,94 @@ program
|
|
|
1199
1449
|
// Print full markdown summary to stdout (pipeable)
|
|
1200
1450
|
console.log(formatMergeSummary(merged));
|
|
1201
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
|
+
});
|
|
1202
1540
|
// ─── mcp ─────────────────────────────────────────────────────────────
|
|
1203
1541
|
program
|
|
1204
1542
|
.command('mcp')
|
|
@@ -1238,10 +1576,12 @@ program
|
|
|
1238
1576
|
console.log(H(' GAL — GuardLink Annotation Language'));
|
|
1239
1577
|
console.log(H(' ══════════════════════════════════════════════════════════'));
|
|
1240
1578
|
console.log('');
|
|
1241
|
-
console.log(D(' Annotations live in source
|
|
1242
|
-
console.log(D(' them
|
|
1579
|
+
console.log(D(' Annotations live in source comments or standalone .gal files.'));
|
|
1580
|
+
console.log(D(' GuardLink parses them into a live threat model for your codebase.'));
|
|
1243
1581
|
console.log('');
|
|
1244
1582
|
console.log(D(' Syntax: @verb subject [preposition object] [-- "description"]'));
|
|
1583
|
+
console.log(D(' Inline examples below use comment prefixes; raw .gal files use the same lines without // or #.'));
|
|
1584
|
+
console.log(D(' In .gal files, use @source file:<path> line:<n> [symbol:<name>] to anchor following annotations.'));
|
|
1245
1585
|
console.log('');
|
|
1246
1586
|
// ── DEFINITIONS ──
|
|
1247
1587
|
console.log(H(' ── Definitions ─────────────────────────────────────────────'));
|
|
@@ -1278,6 +1618,12 @@ program
|
|
|
1278
1618
|
console.log(EX(' // @mitigates api.auth against SQL Injection using Input Validation'));
|
|
1279
1619
|
console.log(EX(' // @mitigates db.users against Token Theft -- "Rotation implemented in v2"'));
|
|
1280
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('');
|
|
1281
1627
|
console.log(` ${V('@accepts')} ${K('<threat>')} ${D('on')} ${K('<asset>')} ${D('[-- "reason"]')}`);
|
|
1282
1628
|
console.log(D(' Explicitly accept a risk. Removes it from open findings.'));
|
|
1283
1629
|
console.log(D(' Use when the risk is known and intentionally not mitigated.'));
|
|
@@ -1328,6 +1674,21 @@ program
|
|
|
1328
1674
|
console.log(D(' Document a security assumption about an asset.'));
|
|
1329
1675
|
console.log(EX(' // @assumes api.gateway -- "Upstream WAF filters malformed requests"'));
|
|
1330
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('');
|
|
1331
1692
|
console.log(` ${V('@comment')} ${D('[-- "description"]')}`);
|
|
1332
1693
|
console.log(D(' Free-form developer security note (no structural effect).'));
|
|
1333
1694
|
console.log(EX(' // @comment -- "TODO — add rate limiting before v2 launch"'));
|
|
@@ -1349,7 +1710,7 @@ program
|
|
|
1349
1710
|
// ── EXTERNAL REFERENCES ──
|
|
1350
1711
|
console.log(H(' ── External References ─────────────────────────────────────'));
|
|
1351
1712
|
console.log('');
|
|
1352
|
-
console.log(D(' Append space-separated refs after severity on @threat and @
|
|
1713
|
+
console.log(D(' Append space-separated refs after severity on @threat, @exposes, and @confirmed:'));
|
|
1353
1714
|
console.log(EX(' cwe:CWE-89 owasp:A03:2021 capec:CAPEC-66 attack:T1190'));
|
|
1354
1715
|
console.log('');
|
|
1355
1716
|
console.log(D(' Example:'));
|
|
@@ -1383,23 +1744,27 @@ else {
|
|
|
1383
1744
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
1384
1745
|
function printDiagnostics(diagnostics) {
|
|
1385
1746
|
for (const d of diagnostics) {
|
|
1386
|
-
|
|
1387
|
-
console.error(`${prefix} ${d.file}:${d.line}: ${d.message}`);
|
|
1747
|
+
console.error(`${diagnosticIcon(d.level)} ${d.file}:${d.line}: ${d.message}`);
|
|
1388
1748
|
if (d.raw)
|
|
1389
1749
|
console.error(` → ${d.raw}`);
|
|
1390
1750
|
}
|
|
1391
1751
|
if (diagnostics.length > 0) {
|
|
1752
|
+
const fatals = diagnostics.filter(d => d.level === 'fatal').length;
|
|
1392
1753
|
const errors = diagnostics.filter(d => d.level === 'error').length;
|
|
1393
1754
|
const warnings = diagnostics.filter(d => d.level === 'warning').length;
|
|
1394
|
-
|
|
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`);
|
|
1395
1760
|
}
|
|
1396
1761
|
}
|
|
1397
1762
|
function printStatus(model) {
|
|
1398
1763
|
console.log(`GuardLink Status: ${model.project}`);
|
|
1399
1764
|
console.log(`${'─'.repeat(40)}`);
|
|
1400
1765
|
console.log(`Files scanned: ${model.source_files}`);
|
|
1401
|
-
console.log(`
|
|
1402
|
-
console.log(`
|
|
1766
|
+
console.log(` Files annotated: ${model.annotated_files.length}`);
|
|
1767
|
+
console.log(` Files unannotated: ${model.unannotated_files.length}`);
|
|
1403
1768
|
console.log(`Annotations: ${model.annotations_parsed}`);
|
|
1404
1769
|
console.log(`${'─'.repeat(40)}`);
|
|
1405
1770
|
console.log(`Assets: ${model.assets.length}`);
|
|
@@ -1407,6 +1772,8 @@ function printStatus(model) {
|
|
|
1407
1772
|
console.log(`Controls: ${model.controls.length}`);
|
|
1408
1773
|
console.log(`Mitigations: ${model.mitigations.length}`);
|
|
1409
1774
|
console.log(`Exposures: ${model.exposures.length}`);
|
|
1775
|
+
if ((model.confirmed || []).length > 0)
|
|
1776
|
+
console.log(`Confirmed: ${model.confirmed.length} 🔴`);
|
|
1410
1777
|
console.log(`Acceptances: ${model.acceptances.length}`);
|
|
1411
1778
|
console.log(`Transfers: ${model.transfers.length}`);
|
|
1412
1779
|
console.log(`Flows: ${model.flows.length}`);
|
|
@@ -1416,6 +1783,10 @@ function printStatus(model) {
|
|
|
1416
1783
|
console.log(`Ownership: ${model.ownership.length}`);
|
|
1417
1784
|
console.log(`Data handling: ${model.data_handling.length}`);
|
|
1418
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
|
+
}
|
|
1419
1790
|
console.log(`Comments: ${model.comments.length}`);
|
|
1420
1791
|
console.log(`Shields: ${model.shields.length}`);
|
|
1421
1792
|
}
|