guardlink 1.2.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 +25 -0
- package/README.md +11 -2
- package/dist/agents/config.d.ts +11 -0
- package/dist/agents/config.d.ts.map +1 -1
- package/dist/agents/config.js +11 -0
- package/dist/agents/config.js.map +1 -1
- package/dist/agents/index.d.ts +3 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +3 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/launcher.d.ts +11 -8
- package/dist/agents/launcher.d.ts.map +1 -1
- package/dist/agents/launcher.js +11 -8
- 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 +9 -0
- package/dist/agents/prompts.js.map +1 -1
- package/dist/analyze/index.d.ts +10 -7
- package/dist/analyze/index.d.ts.map +1 -1
- package/dist/analyze/index.js +10 -7
- package/dist/analyze/index.js.map +1 -1
- package/dist/analyze/llm.d.ts +11 -10
- package/dist/analyze/llm.d.ts.map +1 -1
- package/dist/analyze/llm.js +11 -10
- package/dist/analyze/llm.js.map +1 -1
- package/dist/analyze/prompts.d.ts +3 -0
- package/dist/analyze/prompts.d.ts.map +1 -1
- package/dist/analyze/prompts.js +3 -0
- package/dist/analyze/prompts.js.map +1 -1
- package/dist/analyze/tools.d.ts +10 -4
- package/dist/analyze/tools.d.ts.map +1 -1
- package/dist/analyze/tools.js +10 -4
- package/dist/analyze/tools.js.map +1 -1
- 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 +12 -9
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +236 -11
- package/dist/cli/index.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 +30 -7
- 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 +132 -21
- 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 +134 -15
- 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 +2 -0
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -0
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/parse-file.d.ts +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/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 +4 -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 +20 -0
- package/dist/tui/commands.d.ts.map +1 -1
- package/dist/tui/commands.js +200 -2
- 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/index.d.ts +8 -8
- package/dist/tui/index.d.ts.map +1 -1
- package/dist/tui/index.js +29 -9
- 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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardLink — Review module.
|
|
3
|
+
*
|
|
4
|
+
* Interactive governance workflow for unmitigated exposures.
|
|
5
|
+
* Users walk through the GAL (Governance Acceptance List) and decide:
|
|
6
|
+
* accept — write @accepts + @audit (risk acknowledged, intentional)
|
|
7
|
+
* remediate — write @audit with planned-fix note
|
|
8
|
+
* skip — leave open for now
|
|
9
|
+
*
|
|
10
|
+
* @exposes #cli to #arbitrary-write [medium] cwe:CWE-73 -- "Writes @accepts/@audit annotations into source files"
|
|
11
|
+
* @mitigates #cli against #arbitrary-write using #path-validation -- "Only modifies files already in the parsed project"
|
|
12
|
+
* @audit #cli -- "Review decisions require human justification; no empty accepts allowed"
|
|
13
|
+
* @flows ThreatModel -> #cli via getReviewableExposures -- "Exposure list input"
|
|
14
|
+
* @flows #cli -> SourceFiles via writeFile -- "Annotation insertion output"
|
|
15
|
+
* @handles internal on #cli -- "Processes exposure metadata and user justification text"
|
|
16
|
+
*/
|
|
17
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
18
|
+
import { resolve } from 'node:path';
|
|
19
|
+
import { stripCommentPrefix } from '../parser/comment-strip.js';
|
|
20
|
+
import { findUnmitigatedExposures } from '../parser/validate.js';
|
|
21
|
+
// ─── Severity ordering ──────────────────────────────────────────────
|
|
22
|
+
const SEVERITY_ORDER = {
|
|
23
|
+
critical: 0, high: 1, medium: 2, low: 3,
|
|
24
|
+
};
|
|
25
|
+
// ─── Core logic ─────────────────────────────────────────────────────
|
|
26
|
+
/**
|
|
27
|
+
* Get all unmitigated exposures eligible for review, sorted by severity.
|
|
28
|
+
* Excludes test fixtures and files outside the src/ tree.
|
|
29
|
+
*/
|
|
30
|
+
export function getReviewableExposures(model) {
|
|
31
|
+
const unmitigated = findUnmitigatedExposures(model);
|
|
32
|
+
// Filter out test fixtures and non-source files
|
|
33
|
+
const filtered = unmitigated.filter(e => {
|
|
34
|
+
const f = e.location.file;
|
|
35
|
+
return !f.startsWith('tests/') && !f.startsWith('test/') && !f.includes('__tests__/') && !f.includes('fixtures/');
|
|
36
|
+
});
|
|
37
|
+
// Sort: critical → high → medium → low, then by file
|
|
38
|
+
filtered.sort((a, b) => {
|
|
39
|
+
const sa = SEVERITY_ORDER[a.severity || 'low'] ?? 3;
|
|
40
|
+
const sb = SEVERITY_ORDER[b.severity || 'low'] ?? 3;
|
|
41
|
+
if (sa !== sb)
|
|
42
|
+
return sa - sb;
|
|
43
|
+
return a.location.file.localeCompare(b.location.file);
|
|
44
|
+
});
|
|
45
|
+
return filtered.map((exposure, i) => ({
|
|
46
|
+
index: i + 1,
|
|
47
|
+
exposure,
|
|
48
|
+
id: `${exposure.location.file}:${exposure.location.line}`,
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Format a severity tag with color hint for display.
|
|
53
|
+
*/
|
|
54
|
+
export function severityLabel(s) {
|
|
55
|
+
if (!s)
|
|
56
|
+
return '[?]';
|
|
57
|
+
return `[${s}]`;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Detect the comment style and indentation from the @exposes source line.
|
|
61
|
+
* Supports JSDoc ( * @...), single-line (// @...), and hash (# @...) styles.
|
|
62
|
+
*/
|
|
63
|
+
function detectCommentStyle(rawLine) {
|
|
64
|
+
const indent = rawLine.match(/^(\s*)/)?.[1] || '';
|
|
65
|
+
const trimmed = rawLine.trimStart();
|
|
66
|
+
if (trimmed.startsWith('* @') || trimmed.startsWith('* @')) {
|
|
67
|
+
return { prefix: '* ', indent };
|
|
68
|
+
}
|
|
69
|
+
if (trimmed.startsWith('// @')) {
|
|
70
|
+
return { prefix: '// ', indent };
|
|
71
|
+
}
|
|
72
|
+
if (trimmed.startsWith('# @')) {
|
|
73
|
+
return { prefix: '# ', indent };
|
|
74
|
+
}
|
|
75
|
+
if (trimmed.startsWith('-- @')) {
|
|
76
|
+
return { prefix: '-- ', indent };
|
|
77
|
+
}
|
|
78
|
+
// Fallback: single-line JS style
|
|
79
|
+
return { prefix: '// ', indent };
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Check if a source line is a GuardLink annotation (used to walk past coupled blocks).
|
|
83
|
+
*/
|
|
84
|
+
function isAnnotationLine(line) {
|
|
85
|
+
const inner = stripCommentPrefix(line);
|
|
86
|
+
if (inner === null)
|
|
87
|
+
return false;
|
|
88
|
+
const trimmed = inner.trim();
|
|
89
|
+
// Annotation line: starts with @verb
|
|
90
|
+
if (trimmed.startsWith('@'))
|
|
91
|
+
return true;
|
|
92
|
+
// Continuation line: -- "..."
|
|
93
|
+
if (/^--\s*"/.test(trimmed))
|
|
94
|
+
return true;
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Find the insertion point after the coupled annotation block that contains
|
|
99
|
+
* the @exposes line at `exposureLine` (1-indexed).
|
|
100
|
+
*
|
|
101
|
+
* Walks forward from the exposure line past consecutive annotation lines
|
|
102
|
+
* to find the end of the block, then returns the 0-indexed line to insert after.
|
|
103
|
+
*/
|
|
104
|
+
function findInsertionIndex(lines, exposureLine) {
|
|
105
|
+
// exposureLine is 1-indexed, convert to 0-indexed
|
|
106
|
+
let idx = exposureLine - 1;
|
|
107
|
+
// Walk forward past consecutive annotation lines
|
|
108
|
+
while (idx + 1 < lines.length && isAnnotationLine(lines[idx + 1])) {
|
|
109
|
+
idx++;
|
|
110
|
+
}
|
|
111
|
+
// Insert after the last annotation line in the block
|
|
112
|
+
return idx + 1;
|
|
113
|
+
}
|
|
114
|
+
// ─── Annotation builders ────────────────────────────────────────────
|
|
115
|
+
function todayISO() {
|
|
116
|
+
return new Date().toISOString().slice(0, 10);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build the annotation lines to insert for an "accept" decision.
|
|
120
|
+
* Returns lines WITHOUT trailing newline.
|
|
121
|
+
*/
|
|
122
|
+
function buildAcceptLines(style, exposure, justification) {
|
|
123
|
+
const { prefix, indent } = style;
|
|
124
|
+
const date = todayISO();
|
|
125
|
+
return [
|
|
126
|
+
`${indent}${prefix}@accepts ${exposure.threat} on ${exposure.asset} -- "${escapeDesc(justification)}"`,
|
|
127
|
+
`${indent}${prefix}@audit ${exposure.asset} -- "Accepted via guardlink review on ${date}"`,
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build the annotation line to insert for a "remediate" decision.
|
|
132
|
+
*/
|
|
133
|
+
function buildRemediateLines(style, exposure, note) {
|
|
134
|
+
const { prefix, indent } = style;
|
|
135
|
+
const date = todayISO();
|
|
136
|
+
return [
|
|
137
|
+
`${indent}${prefix}@audit ${exposure.asset} -- "Planned remediation: ${escapeDesc(note)} — flagged via guardlink review on ${date}"`,
|
|
138
|
+
];
|
|
139
|
+
}
|
|
140
|
+
/** Escape double quotes in description strings */
|
|
141
|
+
function escapeDesc(s) {
|
|
142
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
143
|
+
}
|
|
144
|
+
// ─── File modification ──────────────────────────────────────────────
|
|
145
|
+
/**
|
|
146
|
+
* Insert annotation lines into a source file after the coupled block
|
|
147
|
+
* containing the given @exposes annotation.
|
|
148
|
+
*
|
|
149
|
+
* Returns the number of lines inserted.
|
|
150
|
+
*/
|
|
151
|
+
async function insertAnnotations(root, exposure, newLines) {
|
|
152
|
+
const filePath = resolve(root, exposure.location.file);
|
|
153
|
+
const content = await readFile(filePath, 'utf-8');
|
|
154
|
+
const lines = content.split('\n');
|
|
155
|
+
// Validate that the exposure line exists and looks right
|
|
156
|
+
const exposureIdx = exposure.location.line - 1; // 0-indexed
|
|
157
|
+
if (exposureIdx < 0 || exposureIdx >= lines.length) {
|
|
158
|
+
throw new Error(`Line ${exposure.location.line} out of range in ${exposure.location.file}`);
|
|
159
|
+
}
|
|
160
|
+
const insertIdx = findInsertionIndex(lines, exposure.location.line);
|
|
161
|
+
// Splice in the new lines
|
|
162
|
+
lines.splice(insertIdx, 0, ...newLines);
|
|
163
|
+
await writeFile(filePath, lines.join('\n'));
|
|
164
|
+
return newLines.length;
|
|
165
|
+
}
|
|
166
|
+
// ─── Public API ─────────────────────────────────────────────────────
|
|
167
|
+
/**
|
|
168
|
+
* Apply a review decision to an exposure.
|
|
169
|
+
* For 'accept': inserts @accepts + @audit after the coupled block.
|
|
170
|
+
* For 'remediate': inserts @audit with planned-fix note.
|
|
171
|
+
* For 'skip': does nothing.
|
|
172
|
+
*
|
|
173
|
+
* Returns the result including lines inserted.
|
|
174
|
+
*/
|
|
175
|
+
export async function applyReviewAction(root, reviewable, action) {
|
|
176
|
+
if (action.decision === 'skip') {
|
|
177
|
+
return { exposure: reviewable, action, linesInserted: 0 };
|
|
178
|
+
}
|
|
179
|
+
const { exposure } = reviewable;
|
|
180
|
+
const filePath = resolve(root, exposure.location.file);
|
|
181
|
+
const content = await readFile(filePath, 'utf-8');
|
|
182
|
+
const lines = content.split('\n');
|
|
183
|
+
// Detect comment style from the @exposes line
|
|
184
|
+
const exposureIdx = exposure.location.line - 1;
|
|
185
|
+
const style = detectCommentStyle(lines[exposureIdx]);
|
|
186
|
+
let newLines;
|
|
187
|
+
if (action.decision === 'accept') {
|
|
188
|
+
newLines = buildAcceptLines(style, exposure, action.justification);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
newLines = buildRemediateLines(style, exposure, action.justification);
|
|
192
|
+
}
|
|
193
|
+
const linesInserted = await insertAnnotations(root, exposure, newLines);
|
|
194
|
+
return { exposure: reviewable, action, linesInserted };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Format an exposure for display in CLI/TUI review UI.
|
|
198
|
+
*/
|
|
199
|
+
export function formatExposureForReview(r, total) {
|
|
200
|
+
const e = r.exposure;
|
|
201
|
+
const sev = e.severity || 'unknown';
|
|
202
|
+
const desc = e.description || '(no description)';
|
|
203
|
+
return [
|
|
204
|
+
`[${r.index}/${total}] ${e.asset} → ${e.threat} [${sev}]`,
|
|
205
|
+
` File: ${e.location.file}:${e.location.line}`,
|
|
206
|
+
` Exposure: "${desc}"`,
|
|
207
|
+
].join('\n');
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Summarize review session results.
|
|
211
|
+
*/
|
|
212
|
+
export function summarizeReview(results) {
|
|
213
|
+
const accepted = results.filter(r => r.action.decision === 'accept').length;
|
|
214
|
+
const remediated = results.filter(r => r.action.decision === 'remediate').length;
|
|
215
|
+
const skipped = results.filter(r => r.action.decision === 'skip').length;
|
|
216
|
+
const totalLines = results.reduce((sum, r) => sum + r.linesInserted, 0);
|
|
217
|
+
const parts = [];
|
|
218
|
+
if (accepted > 0)
|
|
219
|
+
parts.push(`${accepted} accepted`);
|
|
220
|
+
if (remediated > 0)
|
|
221
|
+
parts.push(`${remediated} marked for remediation`);
|
|
222
|
+
if (skipped > 0)
|
|
223
|
+
parts.push(`${skipped} skipped`);
|
|
224
|
+
return `Review complete: ${parts.join(', ')}. ${totalLines} annotation line(s) written.`;
|
|
225
|
+
}
|
|
226
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/review/index.ts"],"names":[],"mappings":"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"}
|
package/dist/tui/commands.d.ts
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each command function takes (args, ctx) and prints output directly.
|
|
5
5
|
* Returns void. Throws on fatal errors.
|
|
6
|
+
*
|
|
7
|
+
* @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o"
|
|
8
|
+
* @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access"
|
|
9
|
+
* @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files"
|
|
10
|
+
* @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root"
|
|
11
|
+
* @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes"
|
|
12
|
+
* @audit #tui -- "Child process spawning delegated to agents/launcher.ts"
|
|
13
|
+
* @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage"
|
|
14
|
+
* @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output"
|
|
15
|
+
* @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM"
|
|
16
|
+
* @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only"
|
|
17
|
+
* @flows UserArgs -> #tui via args -- "Command argument input"
|
|
18
|
+
* @flows #tui -> FileSystem via writeFile -- "Report/config output"
|
|
19
|
+
* @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path"
|
|
20
|
+
* @flows #tui -> #llm-client via chatCompletion -- "LLM API call path"
|
|
21
|
+
* @handles secrets on #tui -- "Processes and stores API keys via /model"
|
|
6
22
|
*/
|
|
7
23
|
import type { ThreatModel, ThreatModelExposure } from '../types/index.js';
|
|
8
24
|
export interface TuiContext {
|
|
@@ -37,6 +53,10 @@ export declare function cmdThreatReport(args: string, ctx: TuiContext): Promise<
|
|
|
37
53
|
export declare function cmdThreatReports(ctx: TuiContext): void;
|
|
38
54
|
export declare function cmdAnnotate(args: string, ctx: TuiContext): Promise<void>;
|
|
39
55
|
export declare function cmdChat(text: string, ctx: TuiContext): Promise<void>;
|
|
56
|
+
export declare function cmdClear(args: string, ctx: TuiContext): Promise<void>;
|
|
57
|
+
export declare function cmdSync(ctx: TuiContext): Promise<void>;
|
|
58
|
+
export declare function cmdUnannotated(ctx: TuiContext): void;
|
|
59
|
+
export declare function cmdReview(args: string, ctx: TuiContext): Promise<void>;
|
|
40
60
|
export declare function cmdReport(ctx: TuiContext): Promise<void>;
|
|
41
61
|
export declare function cmdDashboard(ctx: TuiContext): Promise<void>;
|
|
42
62
|
//# sourceMappingURL=commands.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/tui/commands.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/tui/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAYH,OAAO,KAAK,EAAE,WAAW,EAAmB,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAwB3F,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,WAAW,GAAG,IAAI,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,EAAE,EAAE,OAAO,eAAe,EAAE,SAAS,CAAC;IACtC,8DAA8D;IAC9D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,qEAAqE;IACrE,aAAa,EAAE,mBAAmB,EAAE,CAAC;CACtC;AAED,8CAA8C;AAC9C,wBAAsB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAGjE;AAoBD,wBAAgB,OAAO,IAAI,IAAI,CA6C9B;AAID,wBAAgB,MAAM,IAAI,IAAI,CAoJ7B;AAID,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CA+D/C;AAID,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CA8DhE;AAID,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAyB3D;AAID,wBAAgB,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CA0B7C;AAID,wBAAgB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CA8C/C;AAID,wBAAgB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CA6E9C;AAID,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAuH3D;AAID,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoC1E;AAID,wBAAsB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAmC7D;AAID,wBAAsB,WAAW,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkEhE;AAID,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B1E;AAID,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwC3E;AA6GD,wBAAsB,QAAQ,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAkI7D;AAkFD,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwIlF;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAsBtD;AAID,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8D9E;AAID,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA8E1E;AAID,wBAAsB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD3E;AAID,wBAAsB,OAAO,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B5D;AAID,wBAAgB,cAAc,CAAC,GAAG,EAAE,UAAU,GAAG,IAAI,CAkBpD;AAID,wBAAsB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiF5E;AAID,wBAAsB,SAAS,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9D;AAID,wBAAsB,YAAY,CAAC,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBjE"}
|
package/dist/tui/commands.js
CHANGED
|
@@ -3,11 +3,27 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Each command function takes (args, ctx) and prints output directly.
|
|
5
5
|
* Returns void. Throws on fatal errors.
|
|
6
|
+
*
|
|
7
|
+
* @exposes #tui to #path-traversal [high] cwe:CWE-22 -- "File paths from user args in /view, /sarif -o"
|
|
8
|
+
* @mitigates #tui against #path-traversal using #path-validation -- "resolve() with ctx.root constrains file access"
|
|
9
|
+
* @exposes #tui to #arbitrary-write [high] cwe:CWE-73 -- "/report, /sarif, /dashboard write files"
|
|
10
|
+
* @mitigates #tui against #arbitrary-write using #path-validation -- "Output paths resolved relative to project root"
|
|
11
|
+
* @exposes #tui to #cmd-injection [high] cwe:CWE-78 -- "/annotate and /threat-report spawn child processes"
|
|
12
|
+
* @audit #tui -- "Child process spawning delegated to agents/launcher.ts"
|
|
13
|
+
* @exposes #tui to #api-key-exposure [high] cwe:CWE-798 -- "/model handles API key input and storage"
|
|
14
|
+
* @mitigates #tui against #api-key-exposure using #key-redaction -- "API keys masked in /model show output"
|
|
15
|
+
* @exposes #tui to #prompt-injection [medium] cwe:CWE-77 -- "Freeform chat sends user text to LLM"
|
|
16
|
+
* @audit #tui -- "User freeform text passed to LLM via cmdChat; model context is read-only"
|
|
17
|
+
* @flows UserArgs -> #tui via args -- "Command argument input"
|
|
18
|
+
* @flows #tui -> FileSystem via writeFile -- "Report/config output"
|
|
19
|
+
* @flows #tui -> #agent-launcher via launchAgent -- "Agent spawn path"
|
|
20
|
+
* @flows #tui -> #llm-client via chatCompletion -- "LLM API call path"
|
|
21
|
+
* @handles secrets on #tui -- "Processes and stores API keys via /model"
|
|
6
22
|
*/
|
|
7
23
|
import { resolve, basename } from 'node:path';
|
|
8
24
|
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
|
-
import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures } from '../parser/index.js';
|
|
10
|
-
import { initProject, detectProject, promptAgentSelection } from '../init/index.js';
|
|
25
|
+
import { parseProject, findDanglingRefs, findUnmitigatedExposures, findAcceptedWithoutAudit, findAcceptedExposures, clearAnnotations } from '../parser/index.js';
|
|
26
|
+
import { initProject, detectProject, promptAgentSelection, syncAgentFiles } from '../init/index.js';
|
|
11
27
|
import { generateReport } from '../report/index.js';
|
|
12
28
|
import { generateDashboardHTML } from '../dashboard/index.js';
|
|
13
29
|
import { computeStats, computeSeverity, computeExposures } from '../dashboard/data.js';
|
|
@@ -18,6 +34,7 @@ import { C, severityBadge, severityText, severityTextPad, severityOrder, compute
|
|
|
18
34
|
import { resolveLLMConfig, saveTuiConfig, loadTuiConfig } from './config.js';
|
|
19
35
|
import { AGENTS, parseAgentFlag, launchAgent, launchAgentInline, copyToClipboard, buildAnnotatePrompt } from '../agents/index.js';
|
|
20
36
|
import { describeConfigSource } from '../agents/config.js';
|
|
37
|
+
import { getReviewableExposures, applyReviewAction, summarizeReview } from '../review/index.js';
|
|
21
38
|
// ─── Shared context ──────────────────────────────────────────────────
|
|
22
39
|
/** Prompt user to pick an agent interactively (TUI only) */
|
|
23
40
|
async function pickAgent(ctx) {
|
|
@@ -74,6 +91,9 @@ export function cmdHelp() {
|
|
|
74
91
|
['/threat-reports', 'List saved AI threat reports'],
|
|
75
92
|
['/annotate <prompt>', 'Launch coding agent to annotate codebase'],
|
|
76
93
|
['/model', 'Set AI provider (API or CLI agent: Claude Code, Codex, Gemini)'],
|
|
94
|
+
['/clear', 'Remove all annotations from source files (start fresh)'],
|
|
95
|
+
['/sync', 'Sync agent instruction files with current threat model'],
|
|
96
|
+
['/review [severity]', 'Interactive governance review of unmitigated exposures'],
|
|
77
97
|
['(freeform text)', 'Chat about your threat model with AI'],
|
|
78
98
|
['', ''],
|
|
79
99
|
['/report', 'Generate markdown + JSON report'],
|
|
@@ -253,6 +273,10 @@ export function cmdStatus(ctx) {
|
|
|
253
273
|
console.log(` ${C.dim('Assets:')} ${stats.assets} ${C.dim('Threats:')} ${stats.threats} ${C.dim('Controls:')} ${stats.controls}`);
|
|
254
274
|
console.log(` ${C.dim('Flows:')} ${stats.flows} ${C.dim('Boundaries:')} ${stats.boundaries} ${C.dim('Annotations:')} ${stats.annotations}`);
|
|
255
275
|
console.log(` ${C.dim('Coverage:')} ${stats.coverageAnnotated}/${stats.coverageTotal} symbols (${stats.coveragePercent}%)`);
|
|
276
|
+
console.log(` ${C.dim('Files:')} ${m.annotated_files.length} annotated, ${m.unannotated_files.length} not annotated of ${m.source_files} scanned`);
|
|
277
|
+
if (m.unannotated_files.length > 0) {
|
|
278
|
+
console.log(` ${C.dim('Run')} /unannotated ${C.dim('to list files without annotations')}`);
|
|
279
|
+
}
|
|
256
280
|
// Top threats
|
|
257
281
|
if (m.exposures.length > 0) {
|
|
258
282
|
const threatCounts = new Map();
|
|
@@ -690,6 +714,13 @@ export async function cmdParse(ctx) {
|
|
|
690
714
|
console.log(` ${C.success('✓')} Parsed ${C.bold(String(model.annotations_parsed))} annotations from ${model.source_files} files`);
|
|
691
715
|
console.log(` ${model.assets.length} assets · ${model.threats.length} threats · ${model.controls.length} controls`);
|
|
692
716
|
console.log(` ${model.exposures.length} exposures · ${model.mitigations.length} mitigations · Grade: ${gradeColored(grade)}`);
|
|
717
|
+
// Auto-sync agent instruction files with updated model
|
|
718
|
+
if (model.annotations_parsed > 0) {
|
|
719
|
+
const syncResult = syncAgentFiles({ root: ctx.root, model });
|
|
720
|
+
if (syncResult.updated.length > 0) {
|
|
721
|
+
console.log(C.dim(` ↻ Synced ${syncResult.updated.length} agent instruction file(s)`));
|
|
722
|
+
}
|
|
723
|
+
}
|
|
693
724
|
console.log('');
|
|
694
725
|
}
|
|
695
726
|
catch (err) {
|
|
@@ -1356,6 +1387,173 @@ Keep responses under 500 words unless the user asks for detail.`;
|
|
|
1356
1387
|
}
|
|
1357
1388
|
}
|
|
1358
1389
|
}
|
|
1390
|
+
// ─── /clear ──────────────────────────────────────────────────────
|
|
1391
|
+
export async function cmdClear(args, ctx) {
|
|
1392
|
+
const includeDefinitions = args.includes('--include-definitions');
|
|
1393
|
+
const isDryRun = args.includes('--dry-run');
|
|
1394
|
+
console.log(C.dim(' Scanning for annotations...'));
|
|
1395
|
+
const preview = await clearAnnotations({
|
|
1396
|
+
root: ctx.root,
|
|
1397
|
+
dryRun: true,
|
|
1398
|
+
includeDefinitions,
|
|
1399
|
+
});
|
|
1400
|
+
if (preview.totalRemoved === 0) {
|
|
1401
|
+
console.log('');
|
|
1402
|
+
console.log(C.dim(' No GuardLink annotations found in source files.'));
|
|
1403
|
+
console.log('');
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
console.log('');
|
|
1407
|
+
console.log(` Found ${C.bold(String(preview.totalRemoved))} annotation line(s) across ${C.bold(String(preview.modifiedFiles.length))} file(s):`);
|
|
1408
|
+
console.log('');
|
|
1409
|
+
for (const [file, count] of preview.perFile) {
|
|
1410
|
+
console.log(` ${file} ${C.dim(`(${count} line${count > 1 ? 's' : ''})`)}`);
|
|
1411
|
+
}
|
|
1412
|
+
console.log('');
|
|
1413
|
+
if (isDryRun) {
|
|
1414
|
+
console.log(C.dim(' (dry run) No files were modified.'));
|
|
1415
|
+
console.log('');
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const answer = await ask(ctx, ` ${C.warn('⚠')} Remove all annotations? This cannot be undone. (y/N): `);
|
|
1419
|
+
if (answer.trim().toLowerCase() !== 'y') {
|
|
1420
|
+
console.log(C.dim(' Cancelled.'));
|
|
1421
|
+
console.log('');
|
|
1422
|
+
return;
|
|
1423
|
+
}
|
|
1424
|
+
const result = await clearAnnotations({
|
|
1425
|
+
root: ctx.root,
|
|
1426
|
+
dryRun: false,
|
|
1427
|
+
includeDefinitions,
|
|
1428
|
+
});
|
|
1429
|
+
console.log('');
|
|
1430
|
+
console.log(` ${C.success('✓')} Removed ${C.bold(String(result.totalRemoved))} annotation line(s) from ${result.modifiedFiles.length} file(s).`);
|
|
1431
|
+
console.log(C.dim(' Run /annotate to re-annotate from scratch, or /parse to update the model.'));
|
|
1432
|
+
ctx.model = null;
|
|
1433
|
+
ctx.lastExposures = [];
|
|
1434
|
+
console.log('');
|
|
1435
|
+
}
|
|
1436
|
+
// ─── /sync ───────────────────────────────────────────────────────
|
|
1437
|
+
export async function cmdSync(ctx) {
|
|
1438
|
+
if (!ctx.model) {
|
|
1439
|
+
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
console.log(C.dim(' Syncing agent instruction files with current threat model...'));
|
|
1443
|
+
console.log('');
|
|
1444
|
+
const result = syncAgentFiles({ root: ctx.root, model: ctx.model });
|
|
1445
|
+
if (result.updated.length > 0) {
|
|
1446
|
+
console.log(` ${C.success('✓')} Updated ${C.bold(String(result.updated.length))} agent instruction file(s):`);
|
|
1447
|
+
console.log('');
|
|
1448
|
+
for (const f of result.updated) {
|
|
1449
|
+
console.log(` ${f}`);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
if (result.skipped.length > 0) {
|
|
1453
|
+
console.log('');
|
|
1454
|
+
console.log(C.dim(` Skipped: ${result.skipped.join(', ')}`));
|
|
1455
|
+
}
|
|
1456
|
+
console.log('');
|
|
1457
|
+
console.log(` ${C.dim(`${ctx.model.assets.length} assets, ${ctx.model.threats.length} threats, ${ctx.model.controls.length} controls, ${ctx.model.exposures.length} exposures synced.`)}`);
|
|
1458
|
+
console.log(C.dim(' Any coding agent (Cursor, Claude, Copilot, Windsurf, etc.) will see these IDs.'));
|
|
1459
|
+
console.log('');
|
|
1460
|
+
}
|
|
1461
|
+
// ─── /unannotated ────────────────────────────────────────────────────
|
|
1462
|
+
export function cmdUnannotated(ctx) {
|
|
1463
|
+
if (!ctx.model) {
|
|
1464
|
+
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const files = ctx.model.unannotated_files;
|
|
1468
|
+
if (files.length === 0) {
|
|
1469
|
+
console.log(`\n ${C.success('✓')} All source files have GuardLink annotations.\n`);
|
|
1470
|
+
return;
|
|
1471
|
+
}
|
|
1472
|
+
console.log(`\n ${C.warn('⚠')} ${C.bold(String(files.length))} source file(s) with no annotations:\n`);
|
|
1473
|
+
for (const f of files) {
|
|
1474
|
+
console.log(` ${f}`);
|
|
1475
|
+
}
|
|
1476
|
+
console.log(`\n ${C.dim('Not all files need annotations — only those that touch security boundaries.')}`);
|
|
1477
|
+
console.log('');
|
|
1478
|
+
}
|
|
1479
|
+
// ─── /review ─────────────────────────────────────────────────────────
|
|
1480
|
+
export async function cmdReview(args, ctx) {
|
|
1481
|
+
if (!ctx.model) {
|
|
1482
|
+
console.log(C.warn(' No threat model. Run /parse first.'));
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
let exposures = getReviewableExposures(ctx.model);
|
|
1486
|
+
// Parse severity filter from args (e.g., "/review critical,high")
|
|
1487
|
+
if (args) {
|
|
1488
|
+
const allowed = new Set(args.split(',').map(s => s.trim().toLowerCase()));
|
|
1489
|
+
exposures = exposures.filter(e => allowed.has(e.exposure.severity || 'low'));
|
|
1490
|
+
exposures = exposures.map((e, i) => ({ ...e, index: i + 1 }));
|
|
1491
|
+
}
|
|
1492
|
+
if (exposures.length === 0) {
|
|
1493
|
+
console.log(`\n ${C.success('✓')} No unmitigated exposures to review.\n`);
|
|
1494
|
+
return;
|
|
1495
|
+
}
|
|
1496
|
+
console.log(`\n ${C.bold('guardlink review')} — ${exposures.length} unmitigated exposure(s)\n`);
|
|
1497
|
+
const results = [];
|
|
1498
|
+
for (const reviewable of exposures) {
|
|
1499
|
+
const e = reviewable.exposure;
|
|
1500
|
+
const sev = severityText(e.severity || 'low');
|
|
1501
|
+
console.log(` ${C.bold(`[${reviewable.index}/${exposures.length}]`)} ${e.asset} → ${e.threat} ${sev}`);
|
|
1502
|
+
console.log(` File: ${fileLink(e.location.file, e.location.line)}`);
|
|
1503
|
+
console.log(` Exposure: ${C.dim('"' + (e.description || 'no description') + '"')}`);
|
|
1504
|
+
console.log('');
|
|
1505
|
+
console.log(` ${C.bold('a')} Accept ${C.dim('— risk acknowledged and intentional')}`);
|
|
1506
|
+
console.log(` ${C.bold('r')} Remediate ${C.dim('— mark as planned fix')}`);
|
|
1507
|
+
console.log(` ${C.bold('s')} Skip ${C.dim('— leave open for now')}`);
|
|
1508
|
+
console.log(` ${C.bold('q')} Quit`);
|
|
1509
|
+
console.log('');
|
|
1510
|
+
const choice = (await ask(ctx, ' Choice [a/r/s/q]: ')).toLowerCase();
|
|
1511
|
+
if (choice === 'q') {
|
|
1512
|
+
console.log(`\n ${C.dim('Review ended.')}\n`);
|
|
1513
|
+
break;
|
|
1514
|
+
}
|
|
1515
|
+
if (choice === 'a') {
|
|
1516
|
+
let justification = '';
|
|
1517
|
+
while (!justification) {
|
|
1518
|
+
justification = await ask(ctx, ' Justification (required): ');
|
|
1519
|
+
if (!justification)
|
|
1520
|
+
console.log(C.warn(' ⚠ Justification is mandatory for acceptance.'));
|
|
1521
|
+
}
|
|
1522
|
+
const result = await applyReviewAction(ctx.root, reviewable, { decision: 'accept', justification });
|
|
1523
|
+
results.push(result);
|
|
1524
|
+
console.log(` ${C.success('✓')} Accepted — ${result.linesInserted} line(s) written\n`);
|
|
1525
|
+
}
|
|
1526
|
+
else if (choice === 'r') {
|
|
1527
|
+
let note = '';
|
|
1528
|
+
while (!note) {
|
|
1529
|
+
note = await ask(ctx, ' Remediation note (required): ');
|
|
1530
|
+
if (!note)
|
|
1531
|
+
console.log(C.warn(' ⚠ Remediation note is mandatory.'));
|
|
1532
|
+
}
|
|
1533
|
+
const result = await applyReviewAction(ctx.root, reviewable, { decision: 'remediate', justification: note });
|
|
1534
|
+
results.push(result);
|
|
1535
|
+
console.log(` ${C.success('✓')} Marked for remediation — ${result.linesInserted} line(s) written\n`);
|
|
1536
|
+
}
|
|
1537
|
+
else {
|
|
1538
|
+
results.push({ exposure: reviewable, action: { decision: 'skip', justification: '' }, linesInserted: 0 });
|
|
1539
|
+
console.log(` ${C.dim('— Skipped')}\n`);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (results.length > 0) {
|
|
1543
|
+
console.log(`\n ${summarizeReview(results)}`);
|
|
1544
|
+
// Re-parse and sync if annotations were written
|
|
1545
|
+
if (results.some(r => r.linesInserted > 0)) {
|
|
1546
|
+
await refreshModel(ctx);
|
|
1547
|
+
try {
|
|
1548
|
+
const syncResult = syncAgentFiles({ root: ctx.root, model: ctx.model });
|
|
1549
|
+
if (syncResult.updated.length > 0)
|
|
1550
|
+
console.log(` ${C.dim('↻ Synced')} ${syncResult.updated.length} agent instruction file(s)`);
|
|
1551
|
+
}
|
|
1552
|
+
catch { }
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
console.log('');
|
|
1556
|
+
}
|
|
1359
1557
|
// ─── /report ─────────────────────────────────────────────────────────
|
|
1360
1558
|
export async function cmdReport(ctx) {
|
|
1361
1559
|
if (!ctx.model) {
|