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.
Files changed (71) hide show
  1. package/README.md +47 -58
  2. package/code-review-agent/README.md +25 -4
  3. package/code-review-agent/TODO.md +1 -1
  4. package/code-review-agent/bin/cr-agent.ts +7 -1
  5. package/code-review-agent/dist/bin/cr-agent.js +7 -1
  6. package/code-review-agent/dist/bin/cr-agent.js.map +1 -1
  7. package/code-review-agent/dist/src/analyzer/engine.d.ts +5 -0
  8. package/code-review-agent/dist/src/analyzer/engine.d.ts.map +1 -1
  9. package/code-review-agent/dist/src/analyzer/engine.js +30 -3
  10. package/code-review-agent/dist/src/analyzer/engine.js.map +1 -1
  11. package/code-review-agent/dist/src/analyzer/postprocess.d.ts +15 -0
  12. package/code-review-agent/dist/src/analyzer/postprocess.d.ts.map +1 -0
  13. package/code-review-agent/dist/src/analyzer/postprocess.js +275 -0
  14. package/code-review-agent/dist/src/analyzer/postprocess.js.map +1 -0
  15. package/code-review-agent/dist/src/analyzer/semantic.d.ts +5 -1
  16. package/code-review-agent/dist/src/analyzer/semantic.d.ts.map +1 -1
  17. package/code-review-agent/dist/src/analyzer/semantic.js +80 -20
  18. package/code-review-agent/dist/src/analyzer/semantic.js.map +1 -1
  19. package/code-review-agent/dist/src/context/assembler.d.ts +8 -2
  20. package/code-review-agent/dist/src/context/assembler.d.ts.map +1 -1
  21. package/code-review-agent/dist/src/context/assembler.js +33 -1
  22. package/code-review-agent/dist/src/context/assembler.js.map +1 -1
  23. package/code-review-agent/dist/src/context/file.d.ts.map +1 -1
  24. package/code-review-agent/dist/src/context/file.js +11 -23
  25. package/code-review-agent/dist/src/context/file.js.map +1 -1
  26. package/code-review-agent/dist/src/context/security-summary.d.ts +19 -0
  27. package/code-review-agent/dist/src/context/security-summary.d.ts.map +1 -0
  28. package/code-review-agent/dist/src/context/security-summary.js +199 -0
  29. package/code-review-agent/dist/src/context/security-summary.js.map +1 -0
  30. package/code-review-agent/dist/src/graph/dependency.d.ts.map +1 -1
  31. package/code-review-agent/dist/src/graph/dependency.js +8 -1
  32. package/code-review-agent/dist/src/graph/dependency.js.map +1 -1
  33. package/code-review-agent/dist/src/graph/resolver.d.ts.map +1 -1
  34. package/code-review-agent/dist/src/graph/resolver.js +14 -5
  35. package/code-review-agent/dist/src/graph/resolver.js.map +1 -1
  36. package/code-review-agent/dist/src/index.d.ts +4 -1
  37. package/code-review-agent/dist/src/index.d.ts.map +1 -1
  38. package/code-review-agent/dist/src/index.js +2 -0
  39. package/code-review-agent/dist/src/index.js.map +1 -1
  40. package/code-review-agent/dist/src/types/config.d.ts +3 -0
  41. package/code-review-agent/dist/src/types/config.d.ts.map +1 -1
  42. package/code-review-agent/dist/src/types/config.js +9 -0
  43. package/code-review-agent/dist/src/types/config.js.map +1 -1
  44. package/code-review-agent/src/analyzer/engine.ts +36 -2
  45. package/code-review-agent/src/analyzer/postprocess.ts +311 -0
  46. package/code-review-agent/src/analyzer/semantic.ts +87 -18
  47. package/code-review-agent/src/context/assembler.ts +44 -2
  48. package/code-review-agent/src/context/file.ts +13 -18
  49. package/code-review-agent/src/context/security-summary.ts +225 -0
  50. package/code-review-agent/src/graph/dependency.ts +8 -1
  51. package/code-review-agent/src/graph/resolver.ts +14 -5
  52. package/code-review-agent/src/index.ts +4 -0
  53. package/code-review-agent/src/types/config.ts +16 -0
  54. package/code-review-agent/tests/analyzer/engine.test.ts +5 -0
  55. package/code-review-agent/tests/analyzer/postprocess.test.ts +450 -0
  56. package/code-review-agent/tests/analyzer/prompt-routing.test.ts +137 -0
  57. package/code-review-agent/tests/config-mode.test.ts +71 -0
  58. package/code-review-agent/tests/context/file.test.ts +16 -1
  59. package/code-review-agent/tests/context/security-summary.test.ts +181 -0
  60. package/code-review-agent/tests/fixtures/guarded-agent/router.py +6 -0
  61. package/code-review-agent/tests/fixtures/guarded-agent/tools/executor.py +10 -0
  62. package/code-review-agent/tests/fixtures/guarded-agent/tools/guard.py +4 -0
  63. package/code-review-agent/tests/fixtures/guarded-agent/vuln-tool.py +6 -0
  64. package/code-review-agent/tests/graph/dependency.test.ts +76 -0
  65. package/index.js +18 -18
  66. package/openclaw.plugin.json +1 -1
  67. package/package.json +3 -2
  68. package/scripts/postinstall.js +43 -4
  69. package/server.json +1 -1
  70. package/src/cli/init-hooks.js +3 -3
  71. package/src/cli/init.js +1 -1
@@ -0,0 +1,450 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { postFilterFindings, suppressCarrierFindings } from '../../src/analyzer/postprocess.js';
3
+ import type { Finding } from '../../src/types/findings.js';
4
+
5
+ function makeFinding(overrides: Partial<Finding> = {}): Finding {
6
+ return {
7
+ title: 'Test finding',
8
+ severity: 'medium',
9
+ category: 'security',
10
+ location: { file: 'app.js', startLine: 10, endLine: 10 },
11
+ reasoning: 'Test reasoning',
12
+ intentAlignment: 'unclear',
13
+ confidence: 0.85,
14
+ suggestedAction: 'Fix it',
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ describe('postFilterFindings', () => {
20
+ it('returns all findings unchanged in review mode', () => {
21
+ const findings: Finding[] = [
22
+ makeFinding({ category: 'logic-bug' }),
23
+ makeFinding({ category: 'type-error' }),
24
+ makeFinding({ category: 'security' }),
25
+ ];
26
+
27
+ const result = postFilterFindings(findings, 'review');
28
+ expect(result).toHaveLength(3);
29
+ });
30
+
31
+ it('keeps security category findings in security mode', () => {
32
+ const findings: Finding[] = [
33
+ makeFinding({ category: 'security', title: 'SQL Injection' }),
34
+ ];
35
+
36
+ const result = postFilterFindings(findings, 'security');
37
+ expect(result).toHaveLength(1);
38
+ });
39
+
40
+ it('keeps boundary category findings in security mode', () => {
41
+ const findings: Finding[] = [
42
+ makeFinding({ category: 'boundary', title: 'Path traversal' }),
43
+ ];
44
+
45
+ const result = postFilterFindings(findings, 'security');
46
+ expect(result).toHaveLength(1);
47
+ });
48
+
49
+ it('drops logic-bug without security evidence in security mode', () => {
50
+ const findings: Finding[] = [
51
+ makeFinding({ category: 'logic-bug', title: 'Off-by-one error in loop' }),
52
+ ];
53
+
54
+ const result = postFilterFindings(findings, 'security');
55
+ expect(result).toHaveLength(0);
56
+ });
57
+
58
+ it('drops type-error without security evidence in security mode', () => {
59
+ const findings: Finding[] = [
60
+ makeFinding({ category: 'type-error', title: 'Wrong type passed to function' }),
61
+ ];
62
+
63
+ const result = postFilterFindings(findings, 'security');
64
+ expect(result).toHaveLength(0);
65
+ });
66
+
67
+ it('drops unhandled-exception without security evidence in security mode', () => {
68
+ const findings: Finding[] = [
69
+ makeFinding({ category: 'unhandled-exception', title: 'Promise rejection not caught' }),
70
+ ];
71
+
72
+ const result = postFilterFindings(findings, 'security');
73
+ expect(result).toHaveLength(0);
74
+ });
75
+
76
+ it('keeps logic-bug with CWE in security mode', () => {
77
+ const findings: Finding[] = [
78
+ makeFinding({ category: 'logic-bug', title: 'Integer overflow', cwe: 'CWE-190' }),
79
+ ];
80
+
81
+ const result = postFilterFindings(findings, 'security');
82
+ expect(result).toHaveLength(1);
83
+ });
84
+
85
+ it('keeps logic-bug with OWASP mapping in security mode', () => {
86
+ const findings: Finding[] = [
87
+ makeFinding({ category: 'logic-bug', title: 'Auth bypass', owasp: 'A01:2021' }),
88
+ ];
89
+
90
+ const result = postFilterFindings(findings, 'security');
91
+ expect(result).toHaveLength(1);
92
+ });
93
+
94
+ it('keeps non-security category with security keywords in title', () => {
95
+ const findings: Finding[] = [
96
+ makeFinding({ category: 'logic-bug', title: 'SQL injection via string concat' }),
97
+ ];
98
+
99
+ const result = postFilterFindings(findings, 'security');
100
+ expect(result).toHaveLength(1);
101
+ });
102
+
103
+ it('keeps non-security category with security keywords in reasoning', () => {
104
+ const findings: Finding[] = [
105
+ makeFinding({
106
+ category: 'null-ref',
107
+ title: 'Null dereference in handler',
108
+ reasoning: 'This could lead to credential leak if error response exposes internal state',
109
+ }),
110
+ ];
111
+
112
+ const result = postFilterFindings(findings, 'security');
113
+ expect(result).toHaveLength(1);
114
+ });
115
+
116
+ it('keeps violates-intent with high confidence in security mode', () => {
117
+ const findings: Finding[] = [
118
+ makeFinding({
119
+ category: 'other',
120
+ title: 'Unexpected file write',
121
+ intentAlignment: 'violates-intent',
122
+ confidence: 0.9,
123
+ }),
124
+ ];
125
+
126
+ const result = postFilterFindings(findings, 'security');
127
+ expect(result).toHaveLength(1);
128
+ });
129
+
130
+ it('drops violates-intent with low confidence in security mode', () => {
131
+ const findings: Finding[] = [
132
+ makeFinding({
133
+ category: 'other',
134
+ title: 'Unexpected file write',
135
+ intentAlignment: 'violates-intent',
136
+ confidence: 0.6,
137
+ }),
138
+ ];
139
+
140
+ const result = postFilterFindings(findings, 'security');
141
+ expect(result).toHaveLength(0);
142
+ });
143
+
144
+ it('filters mixed findings correctly in security mode', () => {
145
+ const findings: Finding[] = [
146
+ makeFinding({ category: 'security', title: 'XSS vulnerability' }),
147
+ makeFinding({ category: 'logic-bug', title: 'Off-by-one' }),
148
+ makeFinding({ category: 'type-error', title: 'Wrong return type' }),
149
+ makeFinding({ category: 'null-ref', title: 'Command injection in handler', cwe: 'CWE-78' }),
150
+ makeFinding({ category: 'boundary', title: 'Path traversal' }),
151
+ ];
152
+
153
+ const result = postFilterFindings(findings, 'security');
154
+ expect(result).toHaveLength(3);
155
+ expect(result.map((f) => f.title)).toEqual([
156
+ 'XSS vulnerability',
157
+ 'Command injection in handler',
158
+ 'Path traversal',
159
+ ]);
160
+ });
161
+ });
162
+
163
+ describe('guard finding suppression', () => {
164
+ it('suppresses finding with strong guard evidence and no concrete bypass', () => {
165
+ const findings: Finding[] = [
166
+ makeFinding({
167
+ category: 'security',
168
+ title: 'Command execution in subprocess handler',
169
+ reasoning: 'The function calls subprocess.run with shell=False and a hardcoded allowlist of commands. The allowlist could theoretically be expanded in the future.',
170
+ confidence: 0.65,
171
+ }),
172
+ ];
173
+
174
+ const result = postFilterFindings(findings, 'security');
175
+ expect(result).toHaveLength(0);
176
+ });
177
+
178
+ it('keeps finding with strong guard evidence but concrete bypass', () => {
179
+ const findings: Finding[] = [
180
+ makeFinding({
181
+ category: 'security',
182
+ title: 'Command injection via unsanitized argument',
183
+ reasoning: 'While subprocess.run uses shell=False, the command arguments are constructed from user input without validation, allowing injection of arbitrary flags.',
184
+ confidence: 0.9,
185
+ }),
186
+ ];
187
+
188
+ const result = postFilterFindings(findings, 'security');
189
+ expect(result).toHaveLength(1);
190
+ });
191
+
192
+ it('keeps low-confidence finding with strong guard when bypass is concrete (not theoretical)', () => {
193
+ const findings: Finding[] = [
194
+ makeFinding({
195
+ category: 'security',
196
+ title: 'Argument injection in subprocess.run call',
197
+ reasoning: 'The function uses subprocess.run with shell=False, but the arguments list includes unsanitized user input that can inject additional flags to the command.',
198
+ confidence: 0.6,
199
+ }),
200
+ ];
201
+
202
+ const result = postFilterFindings(findings, 'security');
203
+ expect(result).toHaveLength(1);
204
+ });
205
+
206
+ it('suppresses guard-module finding with weak bypass language', () => {
207
+ const findings: Finding[] = [
208
+ makeFinding({
209
+ category: 'security',
210
+ title: 'Policy validation bypass',
211
+ location: { file: 'src/guard/validator.js', startLine: 10, endLine: 10 },
212
+ reasoning: 'The validator checks against an allowlist but the policy may be bypassed if new entries are added.',
213
+ confidence: 0.7,
214
+ }),
215
+ ];
216
+
217
+ const result = postFilterFindings(findings, 'security');
218
+ expect(result).toHaveLength(0);
219
+ });
220
+
221
+ it('keeps high-confidence guard finding even with guard module path', () => {
222
+ const findings: Finding[] = [
223
+ makeFinding({
224
+ category: 'security',
225
+ title: 'SQL injection in query builder',
226
+ location: { file: 'src/guard/query-builder.js', startLine: 10, endLine: 10 },
227
+ reasoning: 'User input is concatenated directly into the SQL string, bypassing the parameterized query interface.',
228
+ confidence: 0.95,
229
+ }),
230
+ ];
231
+
232
+ const result = postFilterFindings(findings, 'security');
233
+ expect(result).toHaveLength(1);
234
+ });
235
+
236
+ it('suppresses finding about allowlist that could theoretically change', () => {
237
+ const findings: Finding[] = [
238
+ makeFinding({
239
+ category: 'security',
240
+ title: 'Subprocess execution with hardcoded commands',
241
+ reasoning: 'The code executes subprocess commands from a hardcoded allowlist. Future changes could expand this allowlist to include dangerous commands.',
242
+ confidence: 0.6,
243
+ }),
244
+ ];
245
+
246
+ const result = postFilterFindings(findings, 'security');
247
+ expect(result).toHaveLength(0);
248
+ });
249
+ });
250
+
251
+ describe('suppressCarrierFindings', () => {
252
+ it('returns single finding unchanged', () => {
253
+ const findings: Finding[] = [makeFinding()];
254
+
255
+ const result = suppressCarrierFindings(findings);
256
+ expect(result).toHaveLength(1);
257
+ });
258
+
259
+ it('collapses findings with same CWE, keeping highest confidence', () => {
260
+ const findings: Finding[] = [
261
+ makeFinding({
262
+ title: 'Input passed to query builder',
263
+ location: { file: 'routes.js', startLine: 5, endLine: 5 },
264
+ cwe: 'CWE-89',
265
+ confidence: 0.75,
266
+ }),
267
+ makeFinding({
268
+ title: 'SQL injection in database query',
269
+ location: { file: 'db.js', startLine: 20, endLine: 20 },
270
+ cwe: 'CWE-89',
271
+ confidence: 0.95,
272
+ }),
273
+ ];
274
+
275
+ const result = suppressCarrierFindings(findings);
276
+ expect(result).toHaveLength(1);
277
+ expect(result[0].title).toBe('SQL injection in database query');
278
+ expect(result[0].confidence).toBe(0.95);
279
+ });
280
+
281
+ it('keeps findings with different CWEs', () => {
282
+ const findings: Finding[] = [
283
+ makeFinding({ cwe: 'CWE-89', confidence: 0.9 }),
284
+ makeFinding({ cwe: 'CWE-79', confidence: 0.85 }),
285
+ ];
286
+
287
+ const result = suppressCarrierFindings(findings);
288
+ expect(result).toHaveLength(2);
289
+ });
290
+
291
+ it('keeps findings without CWE as separate entries', () => {
292
+ const findings: Finding[] = [
293
+ makeFinding({ title: 'Issue A' }),
294
+ makeFinding({ title: 'Issue B' }),
295
+ ];
296
+
297
+ const result = suppressCarrierFindings(findings);
298
+ expect(result).toHaveLength(2);
299
+ });
300
+
301
+ it('prefers sink-located finding over carrier in router (SSRF case)', () => {
302
+ const findings: Finding[] = [
303
+ makeFinding({
304
+ title: 'User-controlled URL passed to fetch handler',
305
+ location: { file: 'src/router/api-handler.js', startLine: 30, endLine: 30 },
306
+ reasoning: 'The URL is forwarded through the router to the fetch service',
307
+ cwe: 'CWE-918',
308
+ confidence: 0.8,
309
+ }),
310
+ makeFinding({
311
+ title: 'SSRF via unvalidated URL in fetch call',
312
+ location: { file: 'src/service/http-client.js', startLine: 15, endLine: 15 },
313
+ reasoning: 'The function executes a fetch request with an attacker-controlled URL',
314
+ cwe: 'CWE-918',
315
+ confidence: 0.85,
316
+ }),
317
+ ];
318
+
319
+ const result = suppressCarrierFindings(findings);
320
+ expect(result).toHaveLength(1);
321
+ expect(result[0].location.file).toBe('src/service/http-client.js');
322
+ });
323
+
324
+ it('prefers tool/sink file over planner/wrapper file', () => {
325
+ const findings: Finding[] = [
326
+ makeFinding({
327
+ title: 'Command dispatched via planner',
328
+ location: { file: 'planner.py', startLine: 50, endLine: 50 },
329
+ reasoning: 'User input is passed to the tool executor via the planner',
330
+ cwe: 'CWE-78',
331
+ confidence: 0.75,
332
+ }),
333
+ makeFinding({
334
+ title: 'OS command injection in tool executor',
335
+ location: { file: 'tools/executor.py', startLine: 20, endLine: 20 },
336
+ reasoning: 'The tool executor calls subprocess with user-controlled arguments',
337
+ cwe: 'CWE-78',
338
+ confidence: 0.9,
339
+ }),
340
+ ];
341
+
342
+ const result = suppressCarrierFindings(findings);
343
+ expect(result).toHaveLength(1);
344
+ expect(result[0].location.file).toBe('tools/executor.py');
345
+ });
346
+
347
+ it('keeps distinct findings even when same CWE in different contexts', () => {
348
+ const findings: Finding[] = [
349
+ makeFinding({
350
+ title: 'XSS in admin panel template',
351
+ location: { file: 'src/views/admin.js', startLine: 10, endLine: 10 },
352
+ cwe: 'CWE-79',
353
+ confidence: 0.9,
354
+ }),
355
+ ];
356
+
357
+ // Single finding - no suppression should occur
358
+ const result = suppressCarrierFindings(findings);
359
+ expect(result).toHaveLength(1);
360
+ });
361
+
362
+ it('preserves same-title no-CWE findings in different files when neither is carrier/sink', () => {
363
+ const findings: Finding[] = [
364
+ makeFinding({
365
+ title: 'Missing authorization check',
366
+ location: { file: 'src/routes/users.js', startLine: 10, endLine: 10 },
367
+ reasoning: 'No authorization check before database write',
368
+ confidence: 0.85,
369
+ }),
370
+ makeFinding({
371
+ title: 'Missing authorization check',
372
+ location: { file: 'src/routes/admin.js', startLine: 20, endLine: 20 },
373
+ reasoning: 'No authorization check before database write',
374
+ confidence: 0.8,
375
+ }),
376
+ ];
377
+
378
+ const result = suppressCarrierFindings(findings);
379
+ expect(result).toHaveLength(2);
380
+ });
381
+
382
+ it('collapses same-title no-CWE carrier/sink pair when language signals present', () => {
383
+ const findings: Finding[] = [
384
+ makeFinding({
385
+ title: 'Unsafe file write',
386
+ location: { file: 'src/router/upload-handler.js', startLine: 10, endLine: 10 },
387
+ reasoning: 'User-uploaded path is forwarded through the handler to the file service',
388
+ confidence: 0.75,
389
+ }),
390
+ makeFinding({
391
+ title: 'Unsafe file write',
392
+ location: { file: 'src/service/file-client.js', startLine: 20, endLine: 20 },
393
+ reasoning: 'The service writes to disk at the user-specified path',
394
+ confidence: 0.85,
395
+ }),
396
+ ];
397
+
398
+ const result = suppressCarrierFindings(findings);
399
+ expect(result).toHaveLength(1);
400
+ expect(result[0].location.file).toBe('src/service/file-client.js');
401
+ });
402
+
403
+ it('preserves same-title findings in controller/service without carrier/sink language', () => {
404
+ // Both findings describe distinct issues — no forwarding/routing language
405
+ const findings: Finding[] = [
406
+ makeFinding({
407
+ title: 'Missing authorization check',
408
+ location: { file: 'src/controller/users.js', startLine: 10, endLine: 10 },
409
+ reasoning: 'No authorization check before database write',
410
+ confidence: 0.85,
411
+ }),
412
+ makeFinding({
413
+ title: 'Missing authorization check',
414
+ location: { file: 'src/service/admin.js', startLine: 20, endLine: 20 },
415
+ reasoning: 'No authorization check before database write',
416
+ confidence: 0.8,
417
+ }),
418
+ ];
419
+
420
+ const result = suppressCarrierFindings(findings);
421
+ expect(result).toHaveLength(2);
422
+ });
423
+
424
+ it('suppresses all carrier duplicates when carrier file has multiple same-title rows', () => {
425
+ const findings: Finding[] = [
426
+ makeFinding({
427
+ title: 'Unsafe file write',
428
+ location: { file: 'src/router/handler.js', startLine: 10, endLine: 10 },
429
+ reasoning: 'User path forwarded through the router to the write service',
430
+ confidence: 0.7,
431
+ }),
432
+ makeFinding({
433
+ title: 'Unsafe file write',
434
+ location: { file: 'src/router/handler.js', startLine: 30, endLine: 30 },
435
+ reasoning: 'Another path forwarded through the router to the write service',
436
+ confidence: 0.65,
437
+ }),
438
+ makeFinding({
439
+ title: 'Unsafe file write',
440
+ location: { file: 'src/service/writer.js', startLine: 20, endLine: 20 },
441
+ reasoning: 'The service writes to disk at the user-specified path',
442
+ confidence: 0.85,
443
+ }),
444
+ ];
445
+
446
+ const result = suppressCarrierFindings(findings);
447
+ expect(result).toHaveLength(1);
448
+ expect(result[0].location.file).toBe('src/service/writer.js');
449
+ });
450
+ });
@@ -0,0 +1,137 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SemanticAnalyzer } from '../../src/analyzer/semantic.js';
3
+ import { MockLLMProvider } from '../helpers/mock-provider.js';
4
+ import type { FileContext, ProjectContext } from '../../src/types/analysis.js';
5
+ import type { IntentProfile } from '../../src/types/findings.js';
6
+
7
+ const mockIntent: IntentProfile = {
8
+ purpose: 'REST API for user management',
9
+ expectedBehaviors: ['Handle HTTP requests'],
10
+ unexpectedBehaviors: ['Execute eval'],
11
+ framework: 'express',
12
+ riskDomain: 'web-api',
13
+ };
14
+
15
+ const mockProject: ProjectContext = {
16
+ readme: '# API',
17
+ packageMeta: null,
18
+ directoryTree: 'src/',
19
+ envVars: [],
20
+ hasDockerfile: false,
21
+ hasCI: false,
22
+ language: 'javascript/typescript',
23
+ framework: 'express',
24
+ };
25
+
26
+ const mockFile: FileContext = {
27
+ filePath: 'app.js',
28
+ content: 'const x = 1;',
29
+ language: 'javascript',
30
+ lineCount: 1,
31
+ imports: [],
32
+ importedBy: [],
33
+ siblingFiles: [],
34
+ isTestFile: false,
35
+ isConfigFile: false,
36
+ isGenerated: false,
37
+ };
38
+
39
+ const mockAnalysisResponse = {
40
+ findings: [{
41
+ title: 'Test finding',
42
+ severity: 'medium' as const,
43
+ category: 'security' as const,
44
+ location: { file: 'app.js', startLine: 1, endLine: 1 },
45
+ reasoning: 'Test',
46
+ intentAlignment: 'unclear' as const,
47
+ confidence: 0.8,
48
+ suggestedAction: 'Fix',
49
+ }],
50
+ };
51
+
52
+ describe('prompt routing by mode', () => {
53
+ it('uses broad review prompt in review mode', async () => {
54
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
55
+ const triage = new MockLLMProvider({});
56
+
57
+ const analyzer = new SemanticAnalyzer(provider, triage, 'review');
58
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
59
+
60
+ const systemMsg = provider.structuredCalls[0].messages[0].content;
61
+ expect(systemMsg).toContain('logic errors, security vulnerabilities, race conditions');
62
+ expect(systemMsg).not.toContain('SINK LOCALIZATION');
63
+ });
64
+
65
+ it('uses focused security prompt in security mode', async () => {
66
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
67
+ const triage = new MockLLMProvider({});
68
+
69
+ const analyzer = new SemanticAnalyzer(provider, triage, 'security');
70
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
71
+
72
+ const systemMsg = provider.structuredCalls[0].messages[0].content;
73
+ expect(systemMsg).toContain('EXPLOITABLE SECURITY VULNERABILITIES');
74
+ expect(systemMsg).toContain('SINK LOCALIZATION');
75
+ expect(systemMsg).not.toContain('logic errors, security vulnerabilities, race conditions');
76
+ });
77
+
78
+ it('uses security-specific user message in security mode', async () => {
79
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
80
+ const triage = new MockLLMProvider({});
81
+
82
+ const analyzer = new SemanticAnalyzer(provider, triage, 'security');
83
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
84
+
85
+ const userMsg = provider.structuredCalls[0].messages[1].content;
86
+ expect(userMsg).toContain('security vulnerabilities');
87
+ expect(userMsg).not.toContain('real bugs and vulnerabilities');
88
+ });
89
+
90
+ it('uses broad user message in review mode', async () => {
91
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
92
+ const triage = new MockLLMProvider({});
93
+
94
+ const analyzer = new SemanticAnalyzer(provider, triage, 'review');
95
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
96
+
97
+ const userMsg = provider.structuredCalls[0].messages[1].content;
98
+ expect(userMsg).toContain('real bugs and vulnerabilities');
99
+ });
100
+
101
+ it('defaults to review mode when mode not specified', async () => {
102
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
103
+ const triage = new MockLLMProvider({});
104
+
105
+ const analyzer = new SemanticAnalyzer(provider, triage);
106
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
107
+
108
+ const systemMsg = provider.structuredCalls[0].messages[0].content;
109
+ expect(systemMsg).toContain('logic errors, security vulnerabilities, race conditions');
110
+ });
111
+
112
+ it('both modes include untrusted input warning', async () => {
113
+ for (const mode of ['review', 'security'] as const) {
114
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
115
+ const triage = new MockLLMProvider({});
116
+
117
+ const analyzer = new SemanticAnalyzer(provider, triage, mode);
118
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
119
+
120
+ const systemMsg = provider.structuredCalls[0].messages[0].content;
121
+ expect(systemMsg).toContain('UNTRUSTED INPUT');
122
+ }
123
+ });
124
+
125
+ it('both modes include intent-aware analysis block', async () => {
126
+ for (const mode of ['review', 'security'] as const) {
127
+ const provider = new MockLLMProvider({ file_analysis: mockAnalysisResponse });
128
+ const triage = new MockLLMProvider({});
129
+
130
+ const analyzer = new SemanticAnalyzer(provider, triage, mode);
131
+ await analyzer.analyzeFile(mockIntent, mockProject, mockFile);
132
+
133
+ const systemMsg = provider.structuredCalls[0].messages[0].content;
134
+ expect(systemMsg).toContain('Intent-Aware Analysis');
135
+ }
136
+ });
137
+ });
@@ -0,0 +1,71 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { resolveOptions } from '../src/types/config.js';
3
+ import type { AnalysisOptions } from '../src/types/config.js';
4
+
5
+ describe('mode configuration', () => {
6
+ it('defaults mode to review', () => {
7
+ const options = resolveOptions({}, null, {});
8
+ expect(options.mode).toBe('review');
9
+ });
10
+
11
+ it('accepts mode from CLI flags', () => {
12
+ const options = resolveOptions({ mode: 'security' }, null, {});
13
+ expect(options.mode).toBe('security');
14
+ });
15
+
16
+ it('accepts mode from config file', () => {
17
+ const options = resolveOptions({}, { mode: 'security' }, {});
18
+ expect(options.mode).toBe('security');
19
+ });
20
+
21
+ it('accepts mode from environment variable', () => {
22
+ const options = resolveOptions({}, null, { CR_AGENT_MODE: 'security' });
23
+ expect(options.mode).toBe('security');
24
+ });
25
+
26
+ it('CLI flags take priority over config', () => {
27
+ const options = resolveOptions(
28
+ { mode: 'review' },
29
+ { mode: 'security' },
30
+ {},
31
+ );
32
+ expect(options.mode).toBe('review');
33
+ });
34
+
35
+ it('config takes priority over env var', () => {
36
+ const options = resolveOptions(
37
+ {},
38
+ { mode: 'security' },
39
+ { CR_AGENT_MODE: 'review' },
40
+ );
41
+ expect(options.mode).toBe('security');
42
+ });
43
+
44
+ it('env var wins when CLI mode is omitted (no Commander default)', () => {
45
+ // This tests the fix: CLI flags.mode is undefined when --mode not passed
46
+ const options = resolveOptions(
47
+ { mode: undefined },
48
+ null,
49
+ { CR_AGENT_MODE: 'security' },
50
+ );
51
+ expect(options.mode).toBe('security');
52
+ });
53
+
54
+ it('config wins when CLI mode is omitted', () => {
55
+ const options = resolveOptions(
56
+ { mode: undefined },
57
+ { mode: 'security' },
58
+ {},
59
+ );
60
+ expect(options.mode).toBe('security');
61
+ });
62
+
63
+ it('explicit CLI --mode review overrides env and config', () => {
64
+ const options = resolveOptions(
65
+ { mode: 'review' },
66
+ { mode: 'security' },
67
+ { CR_AGENT_MODE: 'security' },
68
+ );
69
+ expect(options.mode).toBe('review');
70
+ });
71
+ });
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { isTestFile } from '../../src/context/file.js';
2
+ import * as path from 'node:path';
3
+ import { isTestFile, buildFileContext } from '../../src/context/file.js';
3
4
 
4
5
  describe('isTestFile', () => {
5
6
  it('matches top-level test directories on POSIX paths', () => {
@@ -19,3 +20,17 @@ describe('isTestFile', () => {
19
20
  expect(isTestFile('fixtures/vuln-api-server/server.js')).toBe(false);
20
21
  });
21
22
  });
23
+
24
+ describe('buildFileContext Python imports', () => {
25
+ it('emits both package and submodule for from X import Y', () => {
26
+ const fixtureDir = path.resolve(__dirname, '../fixtures/guarded-agent');
27
+ const routerPath = path.join(fixtureDir, 'router.py');
28
+ const ctx = buildFileContext(routerPath, fixtureDir);
29
+
30
+ // `from tools.executor import execute_tool` should produce both
31
+ // 'tools.executor' (the from-part) and 'tools.executor.execute_tool' (submodule form)
32
+ expect(ctx.imports).toContain('tools.executor');
33
+ // The submodule form: tools.executor.execute_tool
34
+ expect(ctx.imports.some((i) => i.startsWith('tools.executor'))).toBe(true);
35
+ });
36
+ });