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.
- package/CHANGELOG.md +65 -0
- package/README.md +14 -0
- package/dist/agents/config.d.ts +8 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js +28 -5
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +2 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +1 -1
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/launcher.d.ts +14 -0
- package/dist/agents/launcher.d.ts.map +1 -1
- package/dist/agents/launcher.js +126 -1
- package/dist/agents/launcher.js.map +1 -1
- package/dist/agents/prompts.d.ts +2 -2
- package/dist/agents/prompts.d.ts.map +1 -1
- package/dist/agents/prompts.js +251 -31
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/index.d.ts +34 -1
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +281 -8
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/llm.d.ts +54 -3
- package/dist/analyze/llm.d.ts.map +1 -1
- package/dist/analyze/llm.js +418 -97
- package/dist/analyze/llm.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 +227 -111
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyze/tools.d.ts +22 -0
- package/dist/analyze/tools.d.ts.map +1 -0
- package/dist/analyze/tools.js +230 -0
- package/dist/analyze/tools.js.map +1 -0
- package/dist/analyzer/sarif.js +1 -1
- package/dist/cli/index.d.ts +15 -7
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +290 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/data.d.ts +5 -0
- package/dist/dashboard/data.d.ts.map +1 -1
- package/dist/dashboard/data.js +24 -12
- package/dist/dashboard/data.js.map +1 -1
- package/dist/dashboard/diagrams.d.ts.map +1 -1
- package/dist/dashboard/diagrams.js +310 -37
- package/dist/dashboard/diagrams.js.map +1 -1
- package/dist/dashboard/generate.d.ts.map +1 -1
- package/dist/dashboard/generate.js +197 -64
- package/dist/dashboard/generate.js.map +1 -1
- package/dist/init/picker.d.ts.map +1 -1
- package/dist/init/picker.js +2 -2
- package/dist/init/picker.js.map +1 -1
- package/dist/init/templates.d.ts.map +1 -1
- package/dist/init/templates.js +52 -32
- package/dist/init/templates.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +14 -28
- package/dist/mcp/server.js.map +1 -1
- package/dist/parser/index.d.ts +1 -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-line.js +3 -3
- package/dist/parser/parse-line.js.map +1 -1
- package/dist/parser/parse-project.js +1 -1
- package/dist/parser/validate.d.ts +31 -0
- package/dist/parser/validate.d.ts.map +1 -0
- package/dist/parser/validate.js +149 -0
- package/dist/parser/validate.js.map +1 -0
- package/dist/report/report.d.ts.map +1 -1
- package/dist/report/report.js +64 -0
- package/dist/report/report.js.map +1 -1
- package/dist/tui/commands.d.ts +3 -3
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +390 -206
- package/dist/tui/commands.js.map +1 -1
- package/dist/tui/config.d.ts +2 -0
- package/dist/tui/config.d.ts.map +1 -1
- package/dist/tui/config.js.map +1 -1
- package/dist/tui/format.d.ts +7 -0
- package/dist/tui/format.d.ts.map +1 -1
- package/dist/tui/format.js +59 -0
- package/dist/tui/format.js.map +1 -1
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +32 -19
- package/dist/tui/index.js.map +1 -1
- package/dist/tui/input.d.ts +2 -2
- package/dist/tui/input.js +2 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.d.ts.map +1 -1
- 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]
|
|
7
|
-
* guardlink parse [dir]
|
|
8
|
-
* guardlink status [dir]
|
|
9
|
-
* guardlink validate [dir]
|
|
10
|
-
* guardlink
|
|
11
|
-
* guardlink
|
|
12
|
-
* guardlink
|
|
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.
|
|
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
|
-
|
|
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
|
|
344
|
-
.argument('[
|
|
345
|
-
.
|
|
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('--
|
|
352
|
-
.option('--
|
|
353
|
-
.option('--
|
|
354
|
-
.option('--
|
|
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 (
|
|
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
|
-
|
|
383
|
+
const input = promptParts.join(' ').trim();
|
|
384
|
+
// Determine framework vs custom prompt
|
|
362
385
|
const validFrameworks = ['stride', 'dread', 'pasta', 'attacker', 'rapid', 'general'];
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
//
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
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(
|
|
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
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
424
|
-
console.error('
|
|
425
|
-
|
|
426
|
-
|
|
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🔍 ${
|
|
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
|
|
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 (!
|
|
669
|
+
if (!validProviders.includes(value)) {
|
|
594
670
|
console.error(`Unknown provider: ${value}`);
|
|
595
|
-
console.error(
|
|
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(
|
|
698
|
-
console.log(
|
|
699
|
-
console.log(
|
|
700
|
-
console.log(
|
|
701
|
-
console.log(
|
|
702
|
-
console.log(
|
|
703
|
-
console.log(
|
|
704
|
-
console.log(
|
|
705
|
-
console.log(
|
|
706
|
-
console.log(
|
|
707
|
-
|
|
708
|
-
console.log(
|
|
709
|
-
console.log(
|
|
710
|
-
console.log(` ${
|
|
711
|
-
console.log(
|
|
712
|
-
console.log(
|
|
713
|
-
console.log(
|
|
714
|
-
console.log(
|
|
715
|
-
console.log(
|
|
716
|
-
console.log(` [
|
|
717
|
-
console.log(
|
|
718
|
-
console.log(
|
|
719
|
-
console.log(
|
|
720
|
-
console.log(
|
|
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
|