agent-security-scanner-mcp 4.0.0 → 4.1.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/README.md +47 -58
- package/code-review-agent/README.md +25 -4
- package/code-review-agent/TODO.md +1 -1
- package/code-review-agent/bin/cr-agent.ts +7 -1
- package/code-review-agent/dist/bin/cr-agent.js +7 -1
- package/code-review-agent/dist/bin/cr-agent.js.map +1 -1
- package/code-review-agent/dist/src/analyzer/engine.d.ts +5 -0
- package/code-review-agent/dist/src/analyzer/engine.d.ts.map +1 -1
- package/code-review-agent/dist/src/analyzer/engine.js +30 -3
- package/code-review-agent/dist/src/analyzer/engine.js.map +1 -1
- package/code-review-agent/dist/src/analyzer/postprocess.d.ts +15 -0
- package/code-review-agent/dist/src/analyzer/postprocess.d.ts.map +1 -0
- package/code-review-agent/dist/src/analyzer/postprocess.js +275 -0
- package/code-review-agent/dist/src/analyzer/postprocess.js.map +1 -0
- package/code-review-agent/dist/src/analyzer/semantic.d.ts +5 -1
- package/code-review-agent/dist/src/analyzer/semantic.d.ts.map +1 -1
- package/code-review-agent/dist/src/analyzer/semantic.js +80 -20
- package/code-review-agent/dist/src/analyzer/semantic.js.map +1 -1
- package/code-review-agent/dist/src/context/assembler.d.ts +8 -2
- package/code-review-agent/dist/src/context/assembler.d.ts.map +1 -1
- package/code-review-agent/dist/src/context/assembler.js +33 -1
- package/code-review-agent/dist/src/context/assembler.js.map +1 -1
- package/code-review-agent/dist/src/context/file.d.ts.map +1 -1
- package/code-review-agent/dist/src/context/file.js +11 -23
- package/code-review-agent/dist/src/context/file.js.map +1 -1
- package/code-review-agent/dist/src/context/security-summary.d.ts +19 -0
- package/code-review-agent/dist/src/context/security-summary.d.ts.map +1 -0
- package/code-review-agent/dist/src/context/security-summary.js +199 -0
- package/code-review-agent/dist/src/context/security-summary.js.map +1 -0
- package/code-review-agent/dist/src/graph/dependency.d.ts.map +1 -1
- package/code-review-agent/dist/src/graph/dependency.js +8 -1
- package/code-review-agent/dist/src/graph/dependency.js.map +1 -1
- package/code-review-agent/dist/src/graph/resolver.d.ts.map +1 -1
- package/code-review-agent/dist/src/graph/resolver.js +14 -5
- package/code-review-agent/dist/src/graph/resolver.js.map +1 -1
- package/code-review-agent/dist/src/index.d.ts +4 -1
- package/code-review-agent/dist/src/index.d.ts.map +1 -1
- package/code-review-agent/dist/src/index.js +2 -0
- package/code-review-agent/dist/src/index.js.map +1 -1
- package/code-review-agent/dist/src/types/config.d.ts +3 -0
- package/code-review-agent/dist/src/types/config.d.ts.map +1 -1
- package/code-review-agent/dist/src/types/config.js +9 -0
- package/code-review-agent/dist/src/types/config.js.map +1 -1
- package/code-review-agent/src/analyzer/engine.ts +36 -2
- package/code-review-agent/src/analyzer/postprocess.ts +311 -0
- package/code-review-agent/src/analyzer/semantic.ts +87 -18
- package/code-review-agent/src/context/assembler.ts +44 -2
- package/code-review-agent/src/context/file.ts +13 -18
- package/code-review-agent/src/context/security-summary.ts +225 -0
- package/code-review-agent/src/graph/dependency.ts +8 -1
- package/code-review-agent/src/graph/resolver.ts +14 -5
- package/code-review-agent/src/index.ts +4 -0
- package/code-review-agent/src/types/config.ts +16 -0
- package/code-review-agent/tests/analyzer/engine.test.ts +5 -0
- package/code-review-agent/tests/analyzer/postprocess.test.ts +450 -0
- package/code-review-agent/tests/analyzer/prompt-routing.test.ts +137 -0
- package/code-review-agent/tests/config-mode.test.ts +71 -0
- package/code-review-agent/tests/context/file.test.ts +16 -1
- package/code-review-agent/tests/context/security-summary.test.ts +181 -0
- package/code-review-agent/tests/fixtures/guarded-agent/router.py +6 -0
- package/code-review-agent/tests/fixtures/guarded-agent/tools/executor.py +10 -0
- package/code-review-agent/tests/fixtures/guarded-agent/tools/guard.py +4 -0
- package/code-review-agent/tests/fixtures/guarded-agent/vuln-tool.py +6 -0
- package/code-review-agent/tests/graph/dependency.test.ts +76 -0
- package/index.js +18 -18
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -2
- package/scripts/postinstall.js +43 -4
- package/server.json +1 -1
- package/src/cli/init-hooks.js +3 -3
- package/src/cli/init.js +1 -1
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import type { FileContext, DependencyGraph } from '../types/analysis.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Keywords that indicate security-relevant lines worth including in summaries.
|
|
7
|
+
*/
|
|
8
|
+
const SECURITY_RELEVANT_PATTERNS = [
|
|
9
|
+
// Dangerous sinks
|
|
10
|
+
/\b(subprocess|exec|eval|system|popen|spawn|shell_exec|os\.system|os\.popen)\b/,
|
|
11
|
+
/\b(requests?\.(get|post|put|delete|patch|head)|fetch|urllib|http\.request|axios)\b/,
|
|
12
|
+
/\b(query|execute|cursor\.execute|\.raw\(|\.query\(|sequelize|knex)\b/,
|
|
13
|
+
/\b(fs\.(readFile|writeFile|unlink|rmdir|rename)|open\(|os\.remove|shutil)\b/,
|
|
14
|
+
// Guard / policy patterns
|
|
15
|
+
/\b(allowlist|allow_list|whitelist|denylist|deny_list|blocklist|blacklist)\b/,
|
|
16
|
+
/\b(validate|sanitize|authorize|authenticate|check_perm|has_perm)\b/,
|
|
17
|
+
/\b(guard|policy|permission|auth_check|is_allowed\w*|can_access\w*|ALLOWED_\w+)\b/,
|
|
18
|
+
/\b(shell\s*=\s*(True|False)|parameterized|prepared_statement|bind_param)\b/,
|
|
19
|
+
// Routing / dispatching
|
|
20
|
+
/\b(app\.(get|post|put|delete|patch|use)|router\.(get|post|put|delete))\b/,
|
|
21
|
+
/\b(dispatch|handle_request|route_to|forward_to)\b/,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Maximum number of nearby files to summarize.
|
|
26
|
+
*/
|
|
27
|
+
const MAX_RELATED_FILES = 4;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Maximum lines to extract per file summary.
|
|
31
|
+
*/
|
|
32
|
+
const MAX_SUMMARY_LINES = 15;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maximum bytes to read from any related file.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_FILE_READ_BYTES = 64 * 1024;
|
|
38
|
+
|
|
39
|
+
export interface RelatedFileSummary {
|
|
40
|
+
filePath: string;
|
|
41
|
+
relationship: 'imports' | 'imported-by' | 'sibling';
|
|
42
|
+
relevantLines: string[];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build compact security-relevant summaries of files related to the one
|
|
47
|
+
* being analyzed. This gives the LLM enough context to understand:
|
|
48
|
+
* - Whether a called module has guards (allowlist, validation)
|
|
49
|
+
* - Whether an imported file contains a dangerous sink
|
|
50
|
+
* - Whether sibling files provide auth/policy enforcement
|
|
51
|
+
*/
|
|
52
|
+
export function buildRelatedFileSummaries(
|
|
53
|
+
file: FileContext,
|
|
54
|
+
projectRoot: string,
|
|
55
|
+
graph?: DependencyGraph,
|
|
56
|
+
): RelatedFileSummary[] {
|
|
57
|
+
const summaries: RelatedFileSummary[] = [];
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
|
|
60
|
+
// Priority 1: files this file imports (may contain sinks or guards)
|
|
61
|
+
for (const imp of file.imports) {
|
|
62
|
+
if (summaries.length >= MAX_RELATED_FILES) break;
|
|
63
|
+
const resolved = resolveLocalFile(imp, file.filePath, projectRoot);
|
|
64
|
+
if (!resolved) continue;
|
|
65
|
+
const relativePath = path.relative(projectRoot, resolved);
|
|
66
|
+
if (seen.has(relativePath)) continue;
|
|
67
|
+
seen.add(relativePath);
|
|
68
|
+
|
|
69
|
+
const summary = summarizeFile(resolved, projectRoot, 'imports');
|
|
70
|
+
if (summary) summaries.push(summary);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Priority 2: files that import this file (may be routers/controllers)
|
|
74
|
+
for (const importer of file.importedBy) {
|
|
75
|
+
if (summaries.length >= MAX_RELATED_FILES) break;
|
|
76
|
+
const fullPath = path.resolve(projectRoot, importer);
|
|
77
|
+
const normalized = path.relative(projectRoot, fullPath);
|
|
78
|
+
if (seen.has(normalized)) continue;
|
|
79
|
+
seen.add(normalized);
|
|
80
|
+
|
|
81
|
+
const summary = summarizeFile(fullPath, projectRoot, 'imported-by');
|
|
82
|
+
if (summary) summaries.push(summary);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Priority 3: security-relevant sibling files (guard, policy, tool, etc.)
|
|
86
|
+
const securitySiblingKeywords = /\b(guard|policy|validator|auth|tool|command|executor|service|middleware)\b/i;
|
|
87
|
+
for (const sibling of file.siblingFiles) {
|
|
88
|
+
if (summaries.length >= MAX_RELATED_FILES) break;
|
|
89
|
+
if (!securitySiblingKeywords.test(sibling)) continue;
|
|
90
|
+
|
|
91
|
+
const siblingPath = path.resolve(path.dirname(path.resolve(projectRoot, file.filePath)), sibling);
|
|
92
|
+
const normalized = path.relative(projectRoot, siblingPath);
|
|
93
|
+
if (seen.has(normalized)) continue;
|
|
94
|
+
seen.add(normalized);
|
|
95
|
+
|
|
96
|
+
const summary = summarizeFile(siblingPath, projectRoot, 'sibling');
|
|
97
|
+
if (summary) summaries.push(summary);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return summaries;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Extract security-relevant lines from a file.
|
|
105
|
+
*/
|
|
106
|
+
function summarizeFile(
|
|
107
|
+
filePath: string,
|
|
108
|
+
projectRoot: string,
|
|
109
|
+
relationship: RelatedFileSummary['relationship'],
|
|
110
|
+
): RelatedFileSummary | null {
|
|
111
|
+
try {
|
|
112
|
+
const stat = fs.statSync(filePath);
|
|
113
|
+
if (!stat.isFile() || stat.size > MAX_FILE_READ_BYTES) return null;
|
|
114
|
+
} catch {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let content: string;
|
|
119
|
+
try {
|
|
120
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const lines = content.split('\n');
|
|
126
|
+
const relevantLines: string[] = [];
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < lines.length && relevantLines.length < MAX_SUMMARY_LINES; i++) {
|
|
129
|
+
const line = lines[i];
|
|
130
|
+
if (SECURITY_RELEVANT_PATTERNS.some((p) => p.test(line))) {
|
|
131
|
+
relevantLines.push(`L${i + 1}: ${line.trim()}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// No relevant lines found — skip this file
|
|
136
|
+
if (relevantLines.length === 0) return null;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
filePath: path.relative(projectRoot, filePath),
|
|
140
|
+
relationship,
|
|
141
|
+
relevantLines,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Try to resolve a local import specifier to an actual file path.
|
|
147
|
+
* Handles:
|
|
148
|
+
* - Relative imports: ./foo, ../bar
|
|
149
|
+
* - Python bare module imports: tools.executor → tools/executor.py
|
|
150
|
+
* - Python single-token imports: guard → guard.py, tools → tools/__init__.py
|
|
151
|
+
*/
|
|
152
|
+
function resolveLocalFile(
|
|
153
|
+
specifier: string,
|
|
154
|
+
fromFile: string,
|
|
155
|
+
projectRoot: string,
|
|
156
|
+
): string | null {
|
|
157
|
+
const fromDir = path.dirname(path.resolve(projectRoot, fromFile));
|
|
158
|
+
|
|
159
|
+
let basePath: string;
|
|
160
|
+
|
|
161
|
+
if (specifier.startsWith('.')) {
|
|
162
|
+
// Relative import (JS/TS/Python relative)
|
|
163
|
+
basePath = path.resolve(fromDir, specifier);
|
|
164
|
+
} else if (/^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*$/.test(specifier) && !specifier.includes('/')) {
|
|
165
|
+
// Python bare module import:
|
|
166
|
+
// tools.executor → tools/executor
|
|
167
|
+
// guard → guard
|
|
168
|
+
// tools → tools
|
|
169
|
+
const asPath = specifier.replace(/\./g, '/');
|
|
170
|
+
basePath = path.resolve(fromDir, asPath);
|
|
171
|
+
|
|
172
|
+
// Also try from project root (Python resolves from project root or cwd)
|
|
173
|
+
const fromRoot = path.resolve(projectRoot, asPath);
|
|
174
|
+
const rootCandidates = [
|
|
175
|
+
`${fromRoot}.py`,
|
|
176
|
+
path.join(fromRoot, '__init__.py'),
|
|
177
|
+
];
|
|
178
|
+
for (const candidate of rootCandidates) {
|
|
179
|
+
try {
|
|
180
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
181
|
+
} catch { /* not found */ }
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
// Non-local third-party import
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Try exact path, then common extensions
|
|
189
|
+
const candidates = [
|
|
190
|
+
basePath,
|
|
191
|
+
`${basePath}.ts`,
|
|
192
|
+
`${basePath}.js`,
|
|
193
|
+
`${basePath}.py`,
|
|
194
|
+
`${basePath}.go`,
|
|
195
|
+
path.join(basePath, 'index.ts'),
|
|
196
|
+
path.join(basePath, 'index.js'),
|
|
197
|
+
`${basePath}.tsx`,
|
|
198
|
+
`${basePath}.jsx`,
|
|
199
|
+
path.join(basePath, '__init__.py'),
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
for (const candidate of candidates) {
|
|
203
|
+
try {
|
|
204
|
+
if (fs.statSync(candidate).isFile()) {
|
|
205
|
+
return candidate;
|
|
206
|
+
}
|
|
207
|
+
} catch { /* not found, try next */ }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Format related file summaries for inclusion in the LLM prompt.
|
|
215
|
+
*/
|
|
216
|
+
export function formatRelatedFileSummaries(summaries: RelatedFileSummary[]): string {
|
|
217
|
+
if (summaries.length === 0) return '';
|
|
218
|
+
|
|
219
|
+
const parts = summaries.map((s) => {
|
|
220
|
+
const header = `${s.filePath} (${s.relationship}):`;
|
|
221
|
+
return [header, ...s.relevantLines].join('\n ');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return parts.join('\n\n');
|
|
225
|
+
}
|
|
@@ -64,7 +64,14 @@ export class DependencyGraphBuilder {
|
|
|
64
64
|
for (const imp of imports) {
|
|
65
65
|
if (!imp.isLocal) continue;
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
// Try resolving from the file's directory first, then from project root
|
|
68
|
+
// (Python bare imports resolve from sys.path which includes project root)
|
|
69
|
+
let resolved = resolveImportPath(imp.specifier, file, language);
|
|
70
|
+
if (!resolved && !imp.specifier.startsWith('.')) {
|
|
71
|
+
// Create a synthetic "from project root" path for resolution
|
|
72
|
+
const rootSentinel = path.join(this.projectRoot, '__resolve_root__.py');
|
|
73
|
+
resolved = resolveImportPath(imp.specifier, rootSentinel, language);
|
|
74
|
+
}
|
|
68
75
|
if (resolved) {
|
|
69
76
|
const resolvedRel = path.relative(this.projectRoot, resolved);
|
|
70
77
|
resolvedImports.push(resolvedRel);
|
|
@@ -35,10 +35,14 @@ export function extractImports(content: string, language: string): ImportInfo[]
|
|
|
35
35
|
imports.push({ specifier: spec, isLocal: isLocalImport(spec, language), resolved: null });
|
|
36
36
|
}
|
|
37
37
|
} else if (language === 'python') {
|
|
38
|
-
// from
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
// `from package import name` — emit both `package` and `package.name`
|
|
39
|
+
// since `name` might be a submodule (file) or a symbol within the package.
|
|
40
|
+
for (const m of content.matchAll(/from\s+(\S+)\s+import\s+(\w+)/g)) {
|
|
41
|
+
const pkg = m[1];
|
|
42
|
+
const name = m[2];
|
|
43
|
+
imports.push({ specifier: pkg, isLocal: isLocalImport(pkg, language), resolved: null });
|
|
44
|
+
const sub = `${pkg}.${name}`;
|
|
45
|
+
imports.push({ specifier: sub, isLocal: isLocalImport(sub, language), resolved: null });
|
|
42
46
|
}
|
|
43
47
|
// import module
|
|
44
48
|
for (const m of content.matchAll(/^import\s+(\S+)/gm)) {
|
|
@@ -119,6 +123,7 @@ export function resolveImportPath(
|
|
|
119
123
|
const initFile = path.join(base, '__init__.py');
|
|
120
124
|
if (fs.statSync(initFile).isFile()) return initFile;
|
|
121
125
|
} catch { /* not found */ }
|
|
126
|
+
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
return null;
|
|
@@ -129,7 +134,11 @@ export function isLocalImport(specifier: string, language: string): boolean {
|
|
|
129
134
|
return specifier.startsWith('./') || specifier.startsWith('../');
|
|
130
135
|
}
|
|
131
136
|
if (language === 'python') {
|
|
132
|
-
|
|
137
|
+
// Relative imports (starts with .) are always local.
|
|
138
|
+
// Bare imports (tools, tools.executor) may be local — let resolveImportPath
|
|
139
|
+
// do a filesystem check rather than rejecting them outright.
|
|
140
|
+
if (specifier.startsWith('.')) return true;
|
|
141
|
+
return /^[a-zA-Z_]\w*(\.[a-zA-Z_]\w*)*$/.test(specifier);
|
|
133
142
|
}
|
|
134
143
|
if (language === 'go') {
|
|
135
144
|
return !specifier.includes('.');
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export { AnalysisEngine, type ProgressCallback } from './analyzer/engine.js';
|
|
3
3
|
export { IntentProfiler } from './analyzer/intent.js';
|
|
4
4
|
export { SemanticAnalyzer } from './analyzer/semantic.js';
|
|
5
|
+
export { postFilterFindings, suppressCarrierFindings } from './analyzer/postprocess.js';
|
|
5
6
|
|
|
6
7
|
// LLM providers
|
|
7
8
|
export { AnthropicProvider } from './llm/anthropic.js';
|
|
@@ -16,6 +17,8 @@ export { zodToJsonSchema, zodToAnthropicTool, zodToOpenAIResponseFormat } from '
|
|
|
16
17
|
export { buildProjectContext, formatProjectContextForLLM } from './context/project.js';
|
|
17
18
|
export { buildFileContext, isTestFile, isConfigFile, isGeneratedFile } from './context/file.js';
|
|
18
19
|
export { ContextAssembler } from './context/assembler.js';
|
|
20
|
+
export { buildRelatedFileSummaries, formatRelatedFileSummaries } from './context/security-summary.js';
|
|
21
|
+
export type { RelatedFileSummary } from './context/security-summary.js';
|
|
19
22
|
|
|
20
23
|
// Graph
|
|
21
24
|
export { DependencyGraphBuilder } from './graph/dependency.js';
|
|
@@ -32,6 +35,7 @@ export type {
|
|
|
32
35
|
DependencyGraph,
|
|
33
36
|
} from './types/analysis.js';
|
|
34
37
|
export type {
|
|
38
|
+
AnalysisMode,
|
|
35
39
|
AnalysisOptions,
|
|
36
40
|
CRAgentConfig,
|
|
37
41
|
} from './types/config.js';
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
2
|
import * as path from 'node:path';
|
|
3
3
|
|
|
4
|
+
export type AnalysisMode = 'review' | 'security';
|
|
5
|
+
|
|
4
6
|
export interface AnalysisOptions {
|
|
7
|
+
mode: AnalysisMode;
|
|
5
8
|
provider: 'anthropic' | 'openai' | 'claude-cli';
|
|
6
9
|
model?: string;
|
|
7
10
|
triageModel?: string;
|
|
@@ -15,6 +18,7 @@ export interface AnalysisOptions {
|
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
export interface CRAgentConfig {
|
|
21
|
+
mode?: AnalysisMode;
|
|
18
22
|
provider?: 'anthropic' | 'openai' | 'claude-cli';
|
|
19
23
|
model?: string;
|
|
20
24
|
triageModel?: string;
|
|
@@ -25,6 +29,7 @@ export interface CRAgentConfig {
|
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
const DEFAULTS: AnalysisOptions = {
|
|
32
|
+
mode: 'review',
|
|
28
33
|
provider: 'anthropic',
|
|
29
34
|
confidenceThreshold: 0.7,
|
|
30
35
|
format: 'text',
|
|
@@ -50,7 +55,18 @@ export function resolveOptions(
|
|
|
50
55
|
config: CRAgentConfig | null,
|
|
51
56
|
env: Record<string, string | undefined> = process.env,
|
|
52
57
|
): AnalysisOptions {
|
|
58
|
+
const mode =
|
|
59
|
+
cliFlags.mode ??
|
|
60
|
+
config?.mode ??
|
|
61
|
+
(env.CR_AGENT_MODE as AnalysisMode | undefined) ??
|
|
62
|
+
DEFAULTS.mode;
|
|
63
|
+
|
|
64
|
+
if (mode !== 'review' && mode !== 'security') {
|
|
65
|
+
throw new Error(`Invalid analysis mode "${mode}". Must be "review" or "security".`);
|
|
66
|
+
}
|
|
67
|
+
|
|
53
68
|
return {
|
|
69
|
+
mode,
|
|
54
70
|
provider:
|
|
55
71
|
cliFlags.provider ??
|
|
56
72
|
config?.provider ??
|
|
@@ -86,6 +86,7 @@ describe('AnalysisEngine', () => {
|
|
|
86
86
|
|
|
87
87
|
it('analyzes vuln-api-server and finds vulnerabilities', async () => {
|
|
88
88
|
const options: AnalysisOptions = {
|
|
89
|
+
mode: 'review',
|
|
89
90
|
provider: 'anthropic',
|
|
90
91
|
confidenceThreshold: 0.7,
|
|
91
92
|
format: 'text',
|
|
@@ -108,6 +109,7 @@ describe('AnalysisEngine', () => {
|
|
|
108
109
|
|
|
109
110
|
it('returns sorted findings (critical first)', async () => {
|
|
110
111
|
const options: AnalysisOptions = {
|
|
112
|
+
mode: 'review',
|
|
111
113
|
provider: 'anthropic',
|
|
112
114
|
confidenceThreshold: 0.5,
|
|
113
115
|
format: 'text',
|
|
@@ -133,6 +135,7 @@ describe('AnalysisEngine', () => {
|
|
|
133
135
|
|
|
134
136
|
it('filters findings below confidence threshold', async () => {
|
|
135
137
|
const options: AnalysisOptions = {
|
|
138
|
+
mode: 'review',
|
|
136
139
|
provider: 'anthropic',
|
|
137
140
|
confidenceThreshold: 0.99,
|
|
138
141
|
format: 'text',
|
|
@@ -154,6 +157,7 @@ describe('AnalysisEngine', () => {
|
|
|
154
157
|
|
|
155
158
|
it('computes stats correctly', async () => {
|
|
156
159
|
const options: AnalysisOptions = {
|
|
160
|
+
mode: 'review',
|
|
157
161
|
provider: 'anthropic',
|
|
158
162
|
confidenceThreshold: 0.7,
|
|
159
163
|
format: 'text',
|
|
@@ -174,6 +178,7 @@ describe('AnalysisEngine', () => {
|
|
|
174
178
|
|
|
175
179
|
it('clamps private worker count when concurrency is zero', async () => {
|
|
176
180
|
const options: AnalysisOptions = {
|
|
181
|
+
mode: 'review',
|
|
177
182
|
provider: 'anthropic',
|
|
178
183
|
confidenceThreshold: 0.7,
|
|
179
184
|
format: 'text',
|