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/agents/prompts.js
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
* GuardLink Agents — Prompt builders for annotation and analysis.
|
|
3
3
|
*
|
|
4
4
|
* Extracted from tui/commands.ts for shared use across CLI, TUI, MCP.
|
|
5
|
+
*
|
|
6
|
+
* @exposes #agent-launcher to #prompt-injection [high] cwe:CWE-77 -- "User prompt concatenated into agent instruction text"
|
|
7
|
+
* @audit #agent-launcher -- "Prompt injection mitigated by agent's own safety measures; GuardLink prompt is read-only context"
|
|
8
|
+
* @exposes #agent-launcher to #path-traversal [medium] cwe:CWE-22 -- "Reads reference docs from root-relative paths"
|
|
9
|
+
* @mitigates #agent-launcher against #path-traversal using #path-validation -- "resolve() with root constrains file access"
|
|
10
|
+
* @flows UserPrompt -> #agent-launcher via buildAnnotatePrompt -- "User instruction input"
|
|
11
|
+
* @flows ThreatModel -> #agent-launcher via model -- "Model context injection"
|
|
12
|
+
* @flows #agent-launcher -> AgentPrompt via return -- "Assembled prompt output"
|
|
13
|
+
* @handles internal on #agent-launcher -- "Serializes threat model IDs and flows into prompt"
|
|
5
14
|
*/
|
|
6
15
|
import { existsSync, readFileSync } from 'node:fs';
|
|
7
16
|
import { resolve } from 'node:path';
|
|
@@ -63,13 +72,13 @@ export function buildAnnotatePrompt(userPrompt, root, model) {
|
|
|
63
72
|
existingFlows += `\n ... and ${model.flows.length - 30} more`;
|
|
64
73
|
}
|
|
65
74
|
// Include unmitigated exposures so agent knows what still needs attention
|
|
75
|
+
// NOTE: Do NOT filter out @accepts — agents should see ALL exposures without real mitigations
|
|
66
76
|
const unmitigatedExposures = model.exposures.filter(e => {
|
|
67
|
-
return !model.mitigations.some(m => m.asset === e.asset && m.threat === e.threat)
|
|
68
|
-
&& !model.acceptances.some(a => a.asset === e.asset && a.threat === e.threat);
|
|
77
|
+
return !model.mitigations.some(m => m.asset === e.asset && m.threat === e.threat);
|
|
69
78
|
});
|
|
70
79
|
if (unmitigatedExposures.length > 0) {
|
|
71
80
|
const expLines = unmitigatedExposures.slice(0, 20).map(e => ` ${e.asset} exposed to ${e.threat} [${e.severity || 'unrated'}] (${e.location.file}:${e.location.line})`);
|
|
72
|
-
existingExposures = `\n\nOpen exposures (
|
|
81
|
+
existingExposures = `\n\nOpen exposures (no mitigation in code — add @mitigates if a control exists, or @audit to flag for human review):\n${expLines.join('\n')}`;
|
|
73
82
|
if (unmitigatedExposures.length > 20)
|
|
74
83
|
existingExposures += `\n ... and ${unmitigatedExposures.length - 20} more`;
|
|
75
84
|
}
|
|
@@ -113,7 +122,7 @@ At each boundary crossing and data transformation, ask:
|
|
|
113
122
|
- What validation/sanitization exists? (@mitigates)
|
|
114
123
|
- What sensitive data passes through here? (@handles)
|
|
115
124
|
- Is there an assumption that could be violated? (@assumes)
|
|
116
|
-
-
|
|
125
|
+
- Does this need human security review? (@audit)
|
|
117
126
|
- Is this risk handled by someone else? (@transfers)
|
|
118
127
|
|
|
119
128
|
### Step 4: Write Coupled Annotation Blocks
|
|
@@ -215,6 +224,33 @@ Don't rate everything P0. A SQL injection in an admin-only internal tool is diff
|
|
|
215
224
|
### @comment — Always Add Context
|
|
216
225
|
Every annotation block should include at least one @comment explaining non-obvious security decisions, assumptions, or context that helps future developers (and AI tools) understand the "why".
|
|
217
226
|
|
|
227
|
+
### @accepts — NEVER USE (Human-Only Decision)
|
|
228
|
+
@accepts marks a risk as intentionally unmitigated. This is a **human-only governance decision** — it requires conscious risk ownership by a person or team.
|
|
229
|
+
As an AI agent, you MUST NEVER write @accepts annotations. You cannot accept risk on behalf of humans.
|
|
230
|
+
|
|
231
|
+
Instead, when you find an exposure with no mitigation in the code:
|
|
232
|
+
1. Write the @exposes annotation to document the risk
|
|
233
|
+
2. Add @audit to flag it for human security review
|
|
234
|
+
3. Add @comment explaining what controls COULD be added
|
|
235
|
+
4. Optionally add @assumes to document any assumptions the code makes
|
|
236
|
+
|
|
237
|
+
Example — what to do when no mitigation exists:
|
|
238
|
+
\`\`\`
|
|
239
|
+
// @shield:begin -- "@accepts alternative examples, excluded from parsing"
|
|
240
|
+
//
|
|
241
|
+
// WRONG (AI rubber-stamping risk):
|
|
242
|
+
// @accepts #prompt-injection on #ai-endpoint -- "Relying on model safety filters"
|
|
243
|
+
//
|
|
244
|
+
// RIGHT (flag for human review):
|
|
245
|
+
// @exposes #ai-endpoint to #prompt-injection [P1] cwe:CWE-77 -- "User prompt passed directly to LLM API without sanitization"
|
|
246
|
+
// @audit #ai-endpoint -- "No prompt sanitization — needs human review to decide: add input filter or accept risk"
|
|
247
|
+
// @comment -- "Potential controls: #prompt-filter (input sanitization), #output-validator (response filtering)"
|
|
248
|
+
//
|
|
249
|
+
// @shield:end
|
|
250
|
+
\`\`\`
|
|
251
|
+
|
|
252
|
+
Leaving exposures unmitigated is HONEST. The dashboard and reports will surface them as open risks for humans to triage.
|
|
253
|
+
|
|
218
254
|
### @shield — DO NOT USE Unless Explicitly Asked
|
|
219
255
|
@shield and @shield:begin/@shield:end block AI coding assistants from reading the annotated code.
|
|
220
256
|
This means any shielded code becomes invisible to AI tools — they cannot analyze, refactor, or annotate it.
|
|
@@ -239,7 +275,7 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re
|
|
|
239
275
|
// @shield:begin -- "Relationship syntax examples, excluded from parsing"
|
|
240
276
|
// @exposes #auth to #sqli [P0] cwe:CWE-89 owasp:A03:2021 -- "User input concatenated into query"
|
|
241
277
|
// @mitigates #auth against #sqli using #prepared-stmts -- "Uses parameterized queries via sqlx"
|
|
242
|
-
// @
|
|
278
|
+
// @audit #auth -- "Timing attack risk — needs human review to decide if bcrypt constant-time comparison is sufficient"
|
|
243
279
|
// @transfers #ddos from #api to #cdn -- "Cloudflare handles L7 DDoS mitigation"
|
|
244
280
|
// @flows req.body.username -> db.query via string-concat -- "User input flows to SQL"
|
|
245
281
|
// @boundary between #frontend and #api (#web-boundary) -- "TLS-terminated public/private boundary"
|
|
@@ -299,8 +335,9 @@ Definitions go in .guardlink/definitions.{ts,js,py,rs}. Source files use only re
|
|
|
299
335
|
Group related definitions together with section comments.
|
|
300
336
|
|
|
301
337
|
4. **Annotate in coupled blocks.** For each security-relevant location, write the complete story:
|
|
302
|
-
@exposes + @mitigates (or @
|
|
338
|
+
@exposes + @mitigates (or @audit if no mitigation exists) + @flows + @comment at minimum.
|
|
303
339
|
Think: "what's the risk, what's the defense, how does data flow here, and what should the next developer know?"
|
|
340
|
+
NEVER write @accepts — that is a human-only governance decision. Use @audit to flag unmitigated risks for review.
|
|
304
341
|
|
|
305
342
|
5. **Use the project's comment style** (// for JS/TS/Go/Rust, # for Python/Ruby/Shell, etc.)
|
|
306
343
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/agents/prompts.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../src/agents/prompts.ts"],"names":[],"mappings":"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"}
|
package/dist/analyze/index.d.ts
CHANGED
|
@@ -5,19 +5,24 @@
|
|
|
5
5
|
* specific prompt, streams the response, and saves timestamped results
|
|
6
6
|
* to .guardlink/threat-reports/.
|
|
7
7
|
*
|
|
8
|
-
* @exposes #llm-client to #
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
11
|
-
* @mitigates #llm-client against #arbitrary-write using #path-validation -- "
|
|
12
|
-
* @
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
8
|
+
* @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths"
|
|
9
|
+
* @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access"
|
|
10
|
+
* @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/"
|
|
11
|
+
* @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/"
|
|
12
|
+
* @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM"
|
|
13
|
+
* @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis"
|
|
14
|
+
* @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input"
|
|
15
|
+
* @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read"
|
|
16
|
+
* @flows #llm-client -> ReportFile via writeFileSync -- "Report output"
|
|
17
|
+
* @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets"
|
|
15
18
|
*/
|
|
16
19
|
import type { ThreatModel } from '../types/index.js';
|
|
17
20
|
import { type AnalysisFramework } from './prompts.js';
|
|
18
21
|
import { type LLMConfig } from './llm.js';
|
|
19
22
|
export { type AnalysisFramework, FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage } from './prompts.js';
|
|
20
23
|
export { type LLMConfig, type LLMProvider, buildConfig, autoDetectConfig } from './llm.js';
|
|
24
|
+
export { GUARDLINK_TOOLS, createToolExecutor } from './tools.js';
|
|
25
|
+
export type { ToolDefinition, ToolCall, ToolResult, ToolExecutor } from './llm.js';
|
|
21
26
|
export interface ThreatReportOptions {
|
|
22
27
|
root: string;
|
|
23
28
|
model: ThreatModel;
|
|
@@ -26,6 +31,18 @@ export interface ThreatReportOptions {
|
|
|
26
31
|
customPrompt?: string;
|
|
27
32
|
stream?: boolean;
|
|
28
33
|
onChunk?: (text: string) => void;
|
|
34
|
+
/** Max lines of context to include around each annotated line (default: 8) */
|
|
35
|
+
snippetContext?: number;
|
|
36
|
+
/** Max total characters for all code snippets combined (default: 40000) */
|
|
37
|
+
snippetBudget?: number;
|
|
38
|
+
/** Enable web search grounding (OpenAI Responses API) */
|
|
39
|
+
webSearch?: boolean;
|
|
40
|
+
/** Enable extended thinking (Anthropic) / reasoning (DeepSeek) */
|
|
41
|
+
extendedThinking?: boolean;
|
|
42
|
+
/** Token budget for thinking (default: 10000) */
|
|
43
|
+
thinkingBudget?: number;
|
|
44
|
+
/** Enable agentic tool use (CVE lookup, model validation, codebase search) */
|
|
45
|
+
enableTools?: boolean;
|
|
29
46
|
}
|
|
30
47
|
export interface ThreatReportResult {
|
|
31
48
|
framework: AnalysisFramework;
|
|
@@ -36,10 +53,29 @@ export interface ThreatReportResult {
|
|
|
36
53
|
savedTo?: string;
|
|
37
54
|
inputTokens?: number;
|
|
38
55
|
outputTokens?: number;
|
|
56
|
+
/** Thinking/reasoning content (if extended thinking was enabled) */
|
|
57
|
+
thinking?: string;
|
|
58
|
+
thinkingTokens?: number;
|
|
39
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Collect project-level context for the LLM: language/framework, key
|
|
62
|
+
* dependencies, and deployment signals (Dockerfile, CI, etc.).
|
|
63
|
+
* Keeps output compact — targets ~2-4 KB.
|
|
64
|
+
*/
|
|
65
|
+
export declare function buildProjectContext(root: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Extract source code snippets around annotated lines.
|
|
68
|
+
*
|
|
69
|
+
* For each annotation that has a file + line location, reads the
|
|
70
|
+
* surrounding `contextLines` lines from disk and returns a formatted
|
|
71
|
+
* block. Deduplicates overlapping ranges within the same file.
|
|
72
|
+
* Respects a total character budget to keep token usage bounded.
|
|
73
|
+
*/
|
|
74
|
+
export declare function extractCodeSnippets(root: string, model: ThreatModel, contextLines?: number, budgetChars?: number): string;
|
|
40
75
|
/**
|
|
41
76
|
* Serialize the threat model to a compact representation for LLM context.
|
|
42
|
-
*
|
|
77
|
+
* Includes file:line locations for all security-relevant annotations so
|
|
78
|
+
* the LLM can cross-reference with code snippets.
|
|
43
79
|
*/
|
|
44
80
|
export declare function serializeModel(model: ThreatModel): string;
|
|
45
81
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analyze/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analyze/index.ts"],"names":[],"mappings":"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"}
|
package/dist/analyze/index.js
CHANGED
|
@@ -5,24 +5,267 @@
|
|
|
5
5
|
* specific prompt, streams the response, and saves timestamped results
|
|
6
6
|
* to .guardlink/threat-reports/.
|
|
7
7
|
*
|
|
8
|
-
* @exposes #llm-client to #
|
|
9
|
-
* @
|
|
10
|
-
* @
|
|
11
|
-
* @mitigates #llm-client against #arbitrary-write using #path-validation -- "
|
|
12
|
-
* @
|
|
13
|
-
* @
|
|
14
|
-
* @
|
|
8
|
+
* @exposes #llm-client to #path-traversal [medium] cwe:CWE-22 -- "buildProjectContext reads files from root-relative paths"
|
|
9
|
+
* @mitigates #llm-client against #path-traversal using #path-validation -- "join() with root constrains file access"
|
|
10
|
+
* @exposes #llm-client to #arbitrary-write [medium] cwe:CWE-73 -- "writeFileSync saves threat reports to .guardlink/"
|
|
11
|
+
* @mitigates #llm-client against #arbitrary-write using #path-validation -- "Output path is fixed to .guardlink/threat-reports/"
|
|
12
|
+
* @exposes #llm-client to #data-exposure [low] cwe:CWE-200 -- "Serializes full threat model and code snippets for LLM"
|
|
13
|
+
* @audit #llm-client -- "Threat model data intentionally sent to LLM for analysis"
|
|
14
|
+
* @flows ThreatModel -> #llm-client via serializeModel -- "Model serialization input"
|
|
15
|
+
* @flows ProjectFiles -> #llm-client via readFileSync -- "Project context read"
|
|
16
|
+
* @flows #llm-client -> ReportFile via writeFileSync -- "Report output"
|
|
17
|
+
* @handles internal on #llm-client -- "Processes project dependencies, env examples, code snippets"
|
|
15
18
|
*/
|
|
16
19
|
import { existsSync, mkdirSync, writeFileSync, readdirSync, readFileSync } from 'node:fs';
|
|
17
|
-
import { join } from 'node:path';
|
|
20
|
+
import { join, relative } from 'node:path';
|
|
18
21
|
import { FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage } from './prompts.js';
|
|
19
22
|
import { chatCompletion } from './llm.js';
|
|
23
|
+
import { GUARDLINK_TOOLS, createToolExecutor } from './tools.js';
|
|
20
24
|
export { FRAMEWORK_LABELS, FRAMEWORK_PROMPTS, buildUserMessage } from './prompts.js';
|
|
21
25
|
export { buildConfig, autoDetectConfig } from './llm.js';
|
|
26
|
+
export { GUARDLINK_TOOLS, createToolExecutor } from './tools.js';
|
|
27
|
+
// ─── Project context builder ─────────────────────────────────────────
|
|
28
|
+
/**
|
|
29
|
+
* Collect project-level context for the LLM: language/framework, key
|
|
30
|
+
* dependencies, and deployment signals (Dockerfile, CI, etc.).
|
|
31
|
+
* Keeps output compact — targets ~2-4 KB.
|
|
32
|
+
*/
|
|
33
|
+
export function buildProjectContext(root) {
|
|
34
|
+
const lines = [];
|
|
35
|
+
// package.json — language, framework, key deps
|
|
36
|
+
const pkgPath = join(root, 'package.json');
|
|
37
|
+
if (existsSync(pkgPath)) {
|
|
38
|
+
try {
|
|
39
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
40
|
+
lines.push(`## package.json`);
|
|
41
|
+
if (pkg.name)
|
|
42
|
+
lines.push(`name: ${pkg.name}`);
|
|
43
|
+
if (pkg.version)
|
|
44
|
+
lines.push(`version: ${pkg.version}`);
|
|
45
|
+
if (pkg.description)
|
|
46
|
+
lines.push(`description: ${pkg.description}`);
|
|
47
|
+
const allDeps = {
|
|
48
|
+
...pkg.dependencies,
|
|
49
|
+
...pkg.devDependencies,
|
|
50
|
+
};
|
|
51
|
+
if (Object.keys(allDeps).length) {
|
|
52
|
+
lines.push(`dependencies (${Object.keys(allDeps).length} total):`);
|
|
53
|
+
// Include all deps — LLM needs them to reason about known-vulnerable packages
|
|
54
|
+
for (const [name, ver] of Object.entries(allDeps)) {
|
|
55
|
+
lines.push(` ${name}: ${ver}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (pkg.scripts && Object.keys(pkg.scripts).length) {
|
|
59
|
+
lines.push(`scripts: ${Object.keys(pkg.scripts).join(', ')}`);
|
|
60
|
+
}
|
|
61
|
+
if (pkg.engines)
|
|
62
|
+
lines.push(`engines: ${JSON.stringify(pkg.engines)}`);
|
|
63
|
+
lines.push('');
|
|
64
|
+
}
|
|
65
|
+
catch { /* skip malformed */ }
|
|
66
|
+
}
|
|
67
|
+
// requirements.txt — Python projects
|
|
68
|
+
const reqPath = join(root, 'requirements.txt');
|
|
69
|
+
if (existsSync(reqPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const reqs = readFileSync(reqPath, 'utf-8').trim();
|
|
72
|
+
lines.push('## requirements.txt');
|
|
73
|
+
lines.push(reqs);
|
|
74
|
+
lines.push('');
|
|
75
|
+
}
|
|
76
|
+
catch { /* skip */ }
|
|
77
|
+
}
|
|
78
|
+
// pyproject.toml — Python projects
|
|
79
|
+
const pyprojectPath = join(root, 'pyproject.toml');
|
|
80
|
+
if (existsSync(pyprojectPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const content = readFileSync(pyprojectPath, 'utf-8');
|
|
83
|
+
// Extract just the [tool.poetry.dependencies] or [project] section
|
|
84
|
+
const depsMatch = content.match(/\[(?:tool\.poetry\.)?dependencies\][\s\S]*?(?=\[|$)/m);
|
|
85
|
+
if (depsMatch) {
|
|
86
|
+
lines.push('## pyproject.toml (dependencies)');
|
|
87
|
+
lines.push(depsMatch[0].trim());
|
|
88
|
+
lines.push('');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch { /* skip */ }
|
|
92
|
+
}
|
|
93
|
+
// go.mod — Go projects
|
|
94
|
+
const gomodPath = join(root, 'go.mod');
|
|
95
|
+
if (existsSync(gomodPath)) {
|
|
96
|
+
try {
|
|
97
|
+
const content = readFileSync(gomodPath, 'utf-8');
|
|
98
|
+
lines.push('## go.mod');
|
|
99
|
+
lines.push(content.trim());
|
|
100
|
+
lines.push('');
|
|
101
|
+
}
|
|
102
|
+
catch { /* skip */ }
|
|
103
|
+
}
|
|
104
|
+
// Dockerfile — deployment model
|
|
105
|
+
for (const name of ['Dockerfile', 'Dockerfile.prod', 'Dockerfile.production']) {
|
|
106
|
+
const dfPath = join(root, name);
|
|
107
|
+
if (existsSync(dfPath)) {
|
|
108
|
+
try {
|
|
109
|
+
const content = readFileSync(dfPath, 'utf-8').trim();
|
|
110
|
+
lines.push(`## ${name}`);
|
|
111
|
+
lines.push(content);
|
|
112
|
+
lines.push('');
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
catch { /* skip */ }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// docker-compose.yml — service topology
|
|
119
|
+
for (const name of ['docker-compose.yml', 'docker-compose.yaml', 'compose.yml', 'compose.yaml']) {
|
|
120
|
+
const dcPath = join(root, name);
|
|
121
|
+
if (existsSync(dcPath)) {
|
|
122
|
+
try {
|
|
123
|
+
const content = readFileSync(dcPath, 'utf-8').trim();
|
|
124
|
+
lines.push(`## ${name}`);
|
|
125
|
+
// Cap at 100 lines to avoid blowing token budget
|
|
126
|
+
const dcLines = content.split('\n');
|
|
127
|
+
lines.push(dcLines.slice(0, 100).join('\n'));
|
|
128
|
+
if (dcLines.length > 100)
|
|
129
|
+
lines.push(`... (${dcLines.length - 100} more lines)`);
|
|
130
|
+
lines.push('');
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
catch { /* skip */ }
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// CI config — deployment signals
|
|
137
|
+
const ciFiles = [
|
|
138
|
+
'.github/workflows',
|
|
139
|
+
'.gitlab-ci.yml',
|
|
140
|
+
'.circleci/config.yml',
|
|
141
|
+
'Jenkinsfile',
|
|
142
|
+
'.travis.yml',
|
|
143
|
+
];
|
|
144
|
+
for (const ci of ciFiles) {
|
|
145
|
+
const ciPath = join(root, ci);
|
|
146
|
+
if (existsSync(ciPath)) {
|
|
147
|
+
lines.push(`## CI/CD: ${ci} (detected)`);
|
|
148
|
+
// Don't include full CI content — just note its presence
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// .env.example — environment variable signals
|
|
152
|
+
for (const name of ['.env.example', '.env.sample', '.env.template']) {
|
|
153
|
+
const envPath = join(root, name);
|
|
154
|
+
if (existsSync(envPath)) {
|
|
155
|
+
try {
|
|
156
|
+
const content = readFileSync(envPath, 'utf-8').trim();
|
|
157
|
+
lines.push(`## ${name} (environment variables)`);
|
|
158
|
+
lines.push(content);
|
|
159
|
+
lines.push('');
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
catch { /* skip */ }
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return lines.join('\n').trim();
|
|
166
|
+
}
|
|
167
|
+
// ─── Code snippet extractor ──────────────────────────────────────────
|
|
168
|
+
/**
|
|
169
|
+
* Extract source code snippets around annotated lines.
|
|
170
|
+
*
|
|
171
|
+
* For each annotation that has a file + line location, reads the
|
|
172
|
+
* surrounding `contextLines` lines from disk and returns a formatted
|
|
173
|
+
* block. Deduplicates overlapping ranges within the same file.
|
|
174
|
+
* Respects a total character budget to keep token usage bounded.
|
|
175
|
+
*/
|
|
176
|
+
export function extractCodeSnippets(root, model, contextLines = 8, budgetChars = 40_000) {
|
|
177
|
+
const refs = [];
|
|
178
|
+
for (const e of model.exposures) {
|
|
179
|
+
refs.push({ file: e.location.file, line: e.location.line, label: `@exposes ${e.asset} to ${e.threat} [${e.severity ?? 'unset'}]` });
|
|
180
|
+
}
|
|
181
|
+
for (const m of model.mitigations) {
|
|
182
|
+
refs.push({ file: m.location.file, line: m.location.line, label: `@mitigates ${m.asset} against ${m.threat}` });
|
|
183
|
+
}
|
|
184
|
+
for (const a of model.acceptances) {
|
|
185
|
+
refs.push({ file: a.location.file, line: a.location.line, label: `@accepts ${a.threat} on ${a.asset}` });
|
|
186
|
+
}
|
|
187
|
+
for (const a of model.assumptions) {
|
|
188
|
+
refs.push({ file: a.location.file, line: a.location.line, label: `@assumes on ${a.asset}` });
|
|
189
|
+
}
|
|
190
|
+
for (const b of model.boundaries) {
|
|
191
|
+
refs.push({ file: b.location.file, line: b.location.line, label: `@boundary ${b.asset_a} | ${b.asset_b}` });
|
|
192
|
+
}
|
|
193
|
+
for (const f of model.flows) {
|
|
194
|
+
refs.push({ file: f.location.file, line: f.location.line, label: `@flows ${f.source} -> ${f.target}` });
|
|
195
|
+
}
|
|
196
|
+
// Group by file, merge overlapping line ranges
|
|
197
|
+
const byFile = new Map();
|
|
198
|
+
for (const ref of refs) {
|
|
199
|
+
if (!ref.file || !ref.line)
|
|
200
|
+
continue;
|
|
201
|
+
const absFile = ref.file.startsWith('/') ? ref.file : join(root, ref.file);
|
|
202
|
+
const start = Math.max(1, ref.line - contextLines);
|
|
203
|
+
const end = ref.line + contextLines;
|
|
204
|
+
if (!byFile.has(absFile))
|
|
205
|
+
byFile.set(absFile, []);
|
|
206
|
+
const ranges = byFile.get(absFile);
|
|
207
|
+
// Merge with existing range if overlapping
|
|
208
|
+
let merged = false;
|
|
209
|
+
for (const r of ranges) {
|
|
210
|
+
if (start <= r.end + 1 && end >= r.start - 1) {
|
|
211
|
+
r.start = Math.min(r.start, start);
|
|
212
|
+
r.end = Math.max(r.end, end);
|
|
213
|
+
r.labels.push(ref.label);
|
|
214
|
+
merged = true;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (!merged)
|
|
219
|
+
ranges.push({ start, end, labels: [ref.label] });
|
|
220
|
+
}
|
|
221
|
+
const blocks = [];
|
|
222
|
+
let totalChars = 0;
|
|
223
|
+
for (const [absFile, ranges] of byFile) {
|
|
224
|
+
if (totalChars >= budgetChars)
|
|
225
|
+
break;
|
|
226
|
+
if (!existsSync(absFile))
|
|
227
|
+
continue;
|
|
228
|
+
let fileLines;
|
|
229
|
+
try {
|
|
230
|
+
fileLines = readFileSync(absFile, 'utf-8').split('\n');
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const relPath = relative(root, absFile);
|
|
236
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
237
|
+
for (const range of ranges) {
|
|
238
|
+
if (totalChars >= budgetChars)
|
|
239
|
+
break;
|
|
240
|
+
const from = Math.max(0, range.start - 1);
|
|
241
|
+
const to = Math.min(fileLines.length, range.end);
|
|
242
|
+
const snippet = fileLines.slice(from, to)
|
|
243
|
+
.map((l, i) => `${String(from + i + 1).padStart(4)} | ${l}`)
|
|
244
|
+
.join('\n');
|
|
245
|
+
const uniqueLabels = [...new Set(range.labels)];
|
|
246
|
+
const block = `### ${relPath}:${range.start}-${range.end}
|
|
247
|
+
// Annotations: ${uniqueLabels.join('; ')}
|
|
248
|
+
\`\`\`
|
|
249
|
+
${snippet}
|
|
250
|
+
\`\`\``;
|
|
251
|
+
if (totalChars + block.length > budgetChars) {
|
|
252
|
+
// Include a truncated note and stop
|
|
253
|
+
blocks.push(`### ${relPath}:${range.start}-${range.end}
|
|
254
|
+
// [snippet omitted — budget exhausted]`);
|
|
255
|
+
totalChars = budgetChars;
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
blocks.push(block);
|
|
259
|
+
totalChars += block.length;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return blocks.join('\n\n');
|
|
263
|
+
}
|
|
22
264
|
// ─── Serialization ───────────────────────────────────────────────────
|
|
23
265
|
/**
|
|
24
266
|
* Serialize the threat model to a compact representation for LLM context.
|
|
25
|
-
*
|
|
267
|
+
* Includes file:line locations for all security-relevant annotations so
|
|
268
|
+
* the LLM can cross-reference with code snippets.
|
|
26
269
|
*/
|
|
27
270
|
export function serializeModel(model) {
|
|
28
271
|
const compact = {
|
|
@@ -34,65 +277,78 @@ export function serializeModel(model) {
|
|
|
34
277
|
if (model.assets.length)
|
|
35
278
|
compact.assets = model.assets.map(a => ({
|
|
36
279
|
path: a.path.join('.'), id: a.id, description: a.description,
|
|
280
|
+
file: a.location.file, line: a.location.line,
|
|
37
281
|
}));
|
|
38
282
|
if (model.threats.length)
|
|
39
283
|
compact.threats = model.threats.map(t => ({
|
|
40
284
|
name: t.name, id: t.id, severity: t.severity,
|
|
41
285
|
refs: t.external_refs.length ? t.external_refs : undefined,
|
|
42
286
|
description: t.description,
|
|
287
|
+
file: t.location.file, line: t.location.line,
|
|
43
288
|
}));
|
|
44
289
|
if (model.controls.length)
|
|
45
290
|
compact.controls = model.controls.map(c => ({
|
|
46
291
|
name: c.name, id: c.id, description: c.description,
|
|
292
|
+
file: c.location.file, line: c.location.line,
|
|
47
293
|
}));
|
|
48
294
|
if (model.mitigations.length)
|
|
49
295
|
compact.mitigations = model.mitigations.map(m => ({
|
|
50
296
|
asset: m.asset, threat: m.threat, control: m.control,
|
|
51
|
-
description: m.description,
|
|
297
|
+
description: m.description,
|
|
298
|
+
file: m.location.file, line: m.location.line,
|
|
52
299
|
}));
|
|
53
300
|
if (model.exposures.length)
|
|
54
301
|
compact.exposures = model.exposures.map(e => ({
|
|
55
302
|
asset: e.asset, threat: e.threat, severity: e.severity,
|
|
56
303
|
refs: e.external_refs.length ? e.external_refs : undefined,
|
|
57
|
-
description: e.description,
|
|
304
|
+
description: e.description,
|
|
305
|
+
file: e.location.file, line: e.location.line,
|
|
58
306
|
}));
|
|
59
307
|
if (model.acceptances.length)
|
|
60
308
|
compact.acceptances = model.acceptances.map(a => ({
|
|
61
309
|
asset: a.asset, threat: a.threat, description: a.description,
|
|
310
|
+
file: a.location.file, line: a.location.line,
|
|
62
311
|
}));
|
|
63
312
|
if (model.transfers.length)
|
|
64
313
|
compact.transfers = model.transfers.map(t => ({
|
|
65
314
|
threat: t.threat, source: t.source, target: t.target,
|
|
315
|
+
file: t.location.file, line: t.location.line,
|
|
66
316
|
}));
|
|
67
317
|
if (model.flows.length)
|
|
68
318
|
compact.flows = model.flows.map(f => ({
|
|
69
319
|
source: f.source, target: f.target, mechanism: f.mechanism,
|
|
320
|
+
file: f.location.file, line: f.location.line,
|
|
70
321
|
}));
|
|
71
322
|
if (model.boundaries.length)
|
|
72
323
|
compact.boundaries = model.boundaries.map(b => ({
|
|
73
324
|
a: b.asset_a, b: b.asset_b, id: b.id, description: b.description,
|
|
325
|
+
file: b.location.file, line: b.location.line,
|
|
74
326
|
}));
|
|
75
327
|
if (model.data_handling.length)
|
|
76
328
|
compact.data_handling = model.data_handling.map(h => ({
|
|
77
329
|
classification: h.classification, asset: h.asset,
|
|
330
|
+
file: h.location.file, line: h.location.line,
|
|
78
331
|
}));
|
|
79
332
|
if (model.assumptions.length)
|
|
80
333
|
compact.assumptions = model.assumptions.map(a => ({
|
|
81
334
|
asset: a.asset, description: a.description,
|
|
335
|
+
file: a.location.file, line: a.location.line,
|
|
82
336
|
}));
|
|
83
337
|
if (model.comments.length)
|
|
84
338
|
compact.comments = model.comments.map(c => ({
|
|
85
|
-
description: c.description, file: c.location.file,
|
|
339
|
+
description: c.description, file: c.location.file, line: c.location.line,
|
|
86
340
|
}));
|
|
87
341
|
if (model.validations.length)
|
|
88
342
|
compact.validations = model.validations.map(v => ({
|
|
89
343
|
control: v.control, asset: v.asset,
|
|
344
|
+
file: v.location.file, line: v.location.line,
|
|
90
345
|
}));
|
|
91
|
-
// Coverage summary
|
|
346
|
+
// Coverage summary — include unannotated critical symbols so LLM sees gaps
|
|
92
347
|
compact.coverage = {
|
|
93
348
|
total_symbols: model.coverage.total_symbols,
|
|
94
349
|
annotated: model.coverage.annotated_symbols,
|
|
95
350
|
percent: model.coverage.coverage_percent,
|
|
351
|
+
unannotated_critical: model.coverage.unannotated_critical,
|
|
96
352
|
};
|
|
97
353
|
// Unmitigated exposures summary
|
|
98
354
|
const mitigatedSet = new Set();
|
|
@@ -104,6 +360,7 @@ export function serializeModel(model) {
|
|
|
104
360
|
if (unmitigated.length) {
|
|
105
361
|
compact.unmitigated_exposures = unmitigated.map(e => ({
|
|
106
362
|
asset: e.asset, threat: e.threat, severity: e.severity,
|
|
363
|
+
file: e.location.file, line: e.location.line,
|
|
107
364
|
}));
|
|
108
365
|
}
|
|
109
366
|
return JSON.stringify(compact, null, 2);
|
|
@@ -203,12 +460,29 @@ const THREAT_REPORTS_DIR = 'threat-reports';
|
|
|
203
460
|
const LEGACY_ANALYSES_DIR = 'analyses';
|
|
204
461
|
export async function generateThreatReport(opts) {
|
|
205
462
|
const { root, model, framework, llmConfig, customPrompt } = opts;
|
|
463
|
+
const snippetContext = opts.snippetContext ?? 8;
|
|
464
|
+
const snippetBudget = opts.snippetBudget ?? 40_000;
|
|
206
465
|
const modelJson = serializeModel(model);
|
|
466
|
+
const projectContext = buildProjectContext(root);
|
|
467
|
+
const codeSnippets = extractCodeSnippets(root, model, snippetContext, snippetBudget);
|
|
207
468
|
const systemPrompt = FRAMEWORK_PROMPTS[framework];
|
|
208
|
-
const userMessage = buildUserMessage(modelJson, framework, customPrompt);
|
|
469
|
+
const userMessage = buildUserMessage(modelJson, framework, customPrompt, projectContext || undefined, codeSnippets || undefined);
|
|
209
470
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
471
|
+
// Build enhanced config with optional upgrades
|
|
472
|
+
const enhancedConfig = { ...llmConfig };
|
|
473
|
+
if (opts.webSearch)
|
|
474
|
+
enhancedConfig.webSearch = true;
|
|
475
|
+
if (opts.extendedThinking) {
|
|
476
|
+
enhancedConfig.extendedThinking = true;
|
|
477
|
+
if (opts.thinkingBudget)
|
|
478
|
+
enhancedConfig.thinkingBudget = opts.thinkingBudget;
|
|
479
|
+
}
|
|
480
|
+
if (opts.enableTools !== false) {
|
|
481
|
+
enhancedConfig.tools = GUARDLINK_TOOLS;
|
|
482
|
+
enhancedConfig.toolExecutor = createToolExecutor(root, model);
|
|
483
|
+
}
|
|
210
484
|
// Call LLM
|
|
211
|
-
const response = await chatCompletion(
|
|
485
|
+
const response = await chatCompletion(enhancedConfig, systemPrompt, userMessage, opts.stream ? opts.onChunk : undefined);
|
|
212
486
|
// Save to .guardlink/threat-reports/
|
|
213
487
|
const reportsDir = join(root, '.guardlink', THREAT_REPORTS_DIR);
|
|
214
488
|
if (!existsSync(reportsDir)) {
|
|
@@ -243,6 +517,8 @@ annotations: ${model.annotations_parsed}
|
|
|
243
517
|
savedTo: `.guardlink/${THREAT_REPORTS_DIR}/${filename}`,
|
|
244
518
|
inputTokens: response.inputTokens,
|
|
245
519
|
outputTokens: response.outputTokens,
|
|
520
|
+
thinking: response.thinking,
|
|
521
|
+
thinkingTokens: response.thinkingTokens,
|
|
246
522
|
};
|
|
247
523
|
}
|
|
248
524
|
/** Read .md files from a .guardlink subdirectory */
|