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