ship-safe 6.1.0 → 6.2.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 +735 -594
- package/cli/agents/api-fuzzer.js +345 -345
- package/cli/agents/auth-bypass-agent.js +348 -348
- package/cli/agents/base-agent.js +272 -272
- package/cli/agents/cicd-scanner.js +236 -201
- package/cli/agents/config-auditor.js +521 -521
- package/cli/agents/deep-analyzer.js +6 -2
- package/cli/agents/git-history-scanner.js +170 -170
- package/cli/agents/html-reporter.js +40 -4
- package/cli/agents/index.js +84 -84
- package/cli/agents/injection-tester.js +500 -500
- package/cli/agents/llm-redteam.js +251 -251
- package/cli/agents/mobile-scanner.js +231 -231
- package/cli/agents/orchestrator.js +322 -322
- package/cli/agents/pii-compliance-agent.js +301 -301
- package/cli/agents/scoring-engine.js +248 -248
- package/cli/agents/supabase-rls-agent.js +154 -154
- package/cli/agents/supply-chain-agent.js +650 -507
- package/cli/bin/ship-safe.js +452 -426
- package/cli/commands/agent.js +608 -608
- package/cli/commands/audit.js +986 -979
- package/cli/commands/baseline.js +193 -193
- package/cli/commands/ci.js +342 -342
- package/cli/commands/deps.js +516 -516
- package/cli/commands/doctor.js +159 -159
- package/cli/commands/fix.js +218 -218
- package/cli/commands/hooks.js +268 -0
- package/cli/commands/init.js +407 -407
- package/cli/commands/mcp.js +304 -304
- package/cli/commands/red-team.js +7 -1
- package/cli/commands/remediate.js +798 -798
- package/cli/commands/rotate.js +571 -571
- package/cli/commands/scan.js +569 -567
- package/cli/commands/score.js +449 -448
- package/cli/commands/watch.js +281 -281
- package/cli/hooks/patterns.js +313 -0
- package/cli/hooks/post-tool-use.js +140 -0
- package/cli/hooks/pre-tool-use.js +186 -0
- package/cli/index.js +73 -69
- package/cli/providers/llm-provider.js +397 -287
- package/cli/utils/autofix-rules.js +74 -74
- package/cli/utils/cache-manager.js +311 -311
- package/cli/utils/output.js +1 -0
- package/cli/utils/patterns.js +1121 -1121
- package/cli/utils/pdf-generator.js +94 -94
- package/package.json +69 -68
- package/cli/__tests__/agents.test.js +0 -1301
- package/configs/supabase/rls-templates.sql +0 -242
|
@@ -1,1301 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Ship Safe Unit Tests
|
|
3
|
-
* =====================
|
|
4
|
-
*
|
|
5
|
-
* Tests agent pattern matching, scoring engine, cache manager,
|
|
6
|
-
* deduplication, and ReDoS safety.
|
|
7
|
-
*
|
|
8
|
-
* Run: npm test
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { describe, it } from 'node:test';
|
|
12
|
-
import assert from 'node:assert/strict';
|
|
13
|
-
import fs from 'fs';
|
|
14
|
-
import path from 'path';
|
|
15
|
-
import os from 'os';
|
|
16
|
-
|
|
17
|
-
// =============================================================================
|
|
18
|
-
// HELPERS
|
|
19
|
-
// =============================================================================
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Write a temp file and return its absolute path.
|
|
23
|
-
*/
|
|
24
|
-
function writeTempFile(content, ext = '.js') {
|
|
25
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-test-'));
|
|
26
|
-
const file = path.join(dir, `test${ext}`);
|
|
27
|
-
fs.writeFileSync(file, content);
|
|
28
|
-
return { dir, file };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function cleanup(dir) {
|
|
32
|
-
try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ }
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// =============================================================================
|
|
36
|
-
// INJECTION TESTER
|
|
37
|
-
// =============================================================================
|
|
38
|
-
|
|
39
|
-
describe('InjectionTester', async () => {
|
|
40
|
-
const { InjectionTester } = await import('../agents/injection-tester.js');
|
|
41
|
-
const agent = new InjectionTester();
|
|
42
|
-
|
|
43
|
-
it('detects SQL injection via template literal', async () => {
|
|
44
|
-
const { dir, file } = writeTempFile('const q = `SELECT * FROM users WHERE id = ${userId}`;');
|
|
45
|
-
try {
|
|
46
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
47
|
-
assert.ok(findings.length > 0, 'Should detect SQL injection');
|
|
48
|
-
assert.ok(findings.some(f => f.rule === 'SQL_INJECTION_TEMPLATE_LITERAL'));
|
|
49
|
-
} finally { cleanup(dir); }
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('detects eval() with user input', async () => {
|
|
53
|
-
const { dir, file } = writeTempFile('eval(req.body.code);');
|
|
54
|
-
try {
|
|
55
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
56
|
-
assert.ok(findings.some(f => f.rule === 'CODE_INJECTION_EVAL'));
|
|
57
|
-
} finally { cleanup(dir); }
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('detects command injection via exec template', async () => {
|
|
61
|
-
const { dir, file } = writeTempFile('execSync(`rm -rf ${userPath}`);');
|
|
62
|
-
try {
|
|
63
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
64
|
-
assert.ok(findings.some(f => f.rule === 'CMD_INJECTION_EXEC_TEMPLATE'));
|
|
65
|
-
} finally { cleanup(dir); }
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('detects Python f-string SQL injection', async () => {
|
|
69
|
-
const { dir, file } = writeTempFile('cursor.execute(f"SELECT * FROM users WHERE name = {name}")', '.py');
|
|
70
|
-
try {
|
|
71
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
72
|
-
assert.ok(findings.some(f => f.rule === 'PYTHON_SQL_FSTRING'));
|
|
73
|
-
} finally { cleanup(dir); }
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('detects Python subprocess shell=True', async () => {
|
|
77
|
-
const { dir, file } = writeTempFile('subprocess.run(cmd, shell=True)', '.py');
|
|
78
|
-
try {
|
|
79
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
80
|
-
assert.ok(findings.some(f => f.rule === 'PYTHON_SUBPROCESS_SHELL'));
|
|
81
|
-
} finally { cleanup(dir); }
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('returns no findings for safe code', async () => {
|
|
85
|
-
const { dir, file } = writeTempFile('const x = 1 + 2;\nconsole.log(x);');
|
|
86
|
-
try {
|
|
87
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
88
|
-
// Filter out low-confidence generic matches
|
|
89
|
-
const significant = findings.filter(f => f.confidence !== 'low');
|
|
90
|
-
assert.equal(significant.length, 0, 'Safe code should have no significant findings');
|
|
91
|
-
} finally { cleanup(dir); }
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
// =============================================================================
|
|
96
|
-
// AUTH BYPASS AGENT
|
|
97
|
-
// =============================================================================
|
|
98
|
-
|
|
99
|
-
describe('AuthBypassAgent', async () => {
|
|
100
|
-
const { AuthBypassAgent } = await import('../agents/auth-bypass-agent.js');
|
|
101
|
-
const agent = new AuthBypassAgent();
|
|
102
|
-
|
|
103
|
-
it('detects JWT algorithm none', async () => {
|
|
104
|
-
const { dir, file } = writeTempFile('jwt.verify(token, secret, { algorithms: ["none"] });');
|
|
105
|
-
try {
|
|
106
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
107
|
-
assert.ok(findings.some(f => f.rule === 'JWT_ALG_NONE'));
|
|
108
|
-
} finally { cleanup(dir); }
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('detects Django DEBUG = True', async () => {
|
|
112
|
-
const { dir, file } = writeTempFile('DEBUG = True\nALLOWED_HOSTS = ["*"]', '.py');
|
|
113
|
-
try {
|
|
114
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
115
|
-
assert.ok(findings.some(f => f.rule === 'DJANGO_DEBUG_TRUE'));
|
|
116
|
-
} finally { cleanup(dir); }
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('detects Flask hardcoded secret key', async () => {
|
|
120
|
-
const { dir, file } = writeTempFile('app.secret_key = "mysecret123"', '.py');
|
|
121
|
-
try {
|
|
122
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
123
|
-
assert.ok(findings.some(f => f.rule === 'FLASK_SECRET_KEY_HARDCODED'));
|
|
124
|
-
} finally { cleanup(dir); }
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('detects TLS reject unauthorized disabled', async () => {
|
|
128
|
-
const { dir, file } = writeTempFile('process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";');
|
|
129
|
-
try {
|
|
130
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
131
|
-
assert.ok(findings.some(f => f.rule === 'TLS_REJECT_UNAUTHORIZED'));
|
|
132
|
-
} finally { cleanup(dir); }
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// =============================================================================
|
|
137
|
-
// API FUZZER
|
|
138
|
-
// =============================================================================
|
|
139
|
-
|
|
140
|
-
describe('APIFuzzer', async () => {
|
|
141
|
-
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
142
|
-
const agent = new APIFuzzer();
|
|
143
|
-
|
|
144
|
-
it('detects spread request body (mass assignment)', async () => {
|
|
145
|
-
const { dir, file } = writeTempFile('const data = { ...req.body };');
|
|
146
|
-
try {
|
|
147
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
148
|
-
assert.ok(findings.some(f => f.rule === 'API_SPREAD_BODY'));
|
|
149
|
-
} finally { cleanup(dir); }
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('detects API key in URL', async () => {
|
|
153
|
-
const { dir, file } = writeTempFile('const url = `https://api.example.com?key=${apiKey}`;');
|
|
154
|
-
try {
|
|
155
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
156
|
-
assert.ok(findings.some(f => f.rule === 'API_KEY_IN_URL'));
|
|
157
|
-
} finally { cleanup(dir); }
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('detects debug endpoint', async () => {
|
|
161
|
-
const { dir, file } = writeTempFile('app.get("/debug/info", handler);');
|
|
162
|
-
try {
|
|
163
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
164
|
-
assert.ok(findings.some(f => f.rule === 'API_DEBUG_ENDPOINT'));
|
|
165
|
-
} finally { cleanup(dir); }
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// =============================================================================
|
|
170
|
-
// SSRF PROBER
|
|
171
|
-
// =============================================================================
|
|
172
|
-
|
|
173
|
-
describe('SSRFProber', async () => {
|
|
174
|
-
const { SSRFProber } = await import('../agents/ssrf-prober.js');
|
|
175
|
-
const agent = new SSRFProber();
|
|
176
|
-
|
|
177
|
-
it('detects user input in fetch()', async () => {
|
|
178
|
-
const { dir, file } = writeTempFile('const res = await fetch(req.query.url);');
|
|
179
|
-
try {
|
|
180
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
181
|
-
assert.ok(findings.some(f => f.rule === 'SSRF_USER_URL_FETCH'));
|
|
182
|
-
} finally { cleanup(dir); }
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('detects cloud metadata endpoint', async () => {
|
|
186
|
-
const { dir, file } = writeTempFile('const meta = await fetch("http://169.254.169.254/latest/meta-data/");');
|
|
187
|
-
try {
|
|
188
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
189
|
-
assert.ok(findings.some(f => f.rule === 'SSRF_CLOUD_METADATA'));
|
|
190
|
-
} finally { cleanup(dir); }
|
|
191
|
-
});
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
// =============================================================================
|
|
195
|
-
// LLM RED TEAM
|
|
196
|
-
// =============================================================================
|
|
197
|
-
|
|
198
|
-
describe('LLMRedTeam', async () => {
|
|
199
|
-
const { LLMRedTeam } = await import('../agents/llm-redteam.js');
|
|
200
|
-
const agent = new LLMRedTeam();
|
|
201
|
-
|
|
202
|
-
it('detects LLM output to eval', async () => {
|
|
203
|
-
const { dir, file } = writeTempFile('eval(completion.content);');
|
|
204
|
-
try {
|
|
205
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
206
|
-
assert.ok(findings.some(f => f.rule === 'LLM_OUTPUT_TO_EVAL'));
|
|
207
|
-
} finally { cleanup(dir); }
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('detects system prompt in client code', async () => {
|
|
211
|
-
const { dir, file } = writeTempFile('const systemPrompt = "You are a helpful assistant";');
|
|
212
|
-
try {
|
|
213
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
214
|
-
assert.ok(findings.some(f => f.rule === 'LLM_SYSTEM_PROMPT_CLIENT'));
|
|
215
|
-
} finally { cleanup(dir); }
|
|
216
|
-
});
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
// =============================================================================
|
|
220
|
-
// CONFIG AUDITOR
|
|
221
|
-
// =============================================================================
|
|
222
|
-
|
|
223
|
-
describe('ConfigAuditor', async () => {
|
|
224
|
-
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
225
|
-
const agent = new ConfigAuditor();
|
|
226
|
-
|
|
227
|
-
it('detects CORS wildcard', async () => {
|
|
228
|
-
const { dir, file } = writeTempFile('app.use(cors({ origin: "*" }));');
|
|
229
|
-
try {
|
|
230
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
231
|
-
assert.ok(findings.some(f => f.rule === 'CORS_WILDCARD'));
|
|
232
|
-
} finally { cleanup(dir); }
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('detects Go SQL sprintf', async () => {
|
|
236
|
-
const { dir, file } = writeTempFile('query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", id)', '.go');
|
|
237
|
-
try {
|
|
238
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
239
|
-
assert.ok(findings.some(f => f.rule === 'GO_SQL_SPRINTF'));
|
|
240
|
-
} finally { cleanup(dir); }
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('detects Rust unsafe block', async () => {
|
|
244
|
-
const { dir, file } = writeTempFile('unsafe {\n ptr::read(p)\n}', '.rs');
|
|
245
|
-
try {
|
|
246
|
-
// Config auditor needs .go/.rs in code file extensions
|
|
247
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
248
|
-
assert.ok(findings.some(f => f.rule === 'RUST_UNSAFE_BLOCK'));
|
|
249
|
-
} finally { cleanup(dir); }
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// =============================================================================
|
|
254
|
-
// SCORING ENGINE
|
|
255
|
-
// =============================================================================
|
|
256
|
-
|
|
257
|
-
describe('ScoringEngine', async () => {
|
|
258
|
-
const { ScoringEngine } = await import('../agents/scoring-engine.js');
|
|
259
|
-
|
|
260
|
-
it('computes perfect score with no findings', () => {
|
|
261
|
-
const engine = new ScoringEngine();
|
|
262
|
-
const result = engine.compute([], []);
|
|
263
|
-
assert.equal(result.score, 100);
|
|
264
|
-
assert.equal(result.grade.letter, 'A');
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it('deducts for critical findings (capped at category weight)', () => {
|
|
268
|
-
const engine = new ScoringEngine();
|
|
269
|
-
const findings = [
|
|
270
|
-
{ severity: 'critical', category: 'secrets', confidence: 'high' },
|
|
271
|
-
];
|
|
272
|
-
const result = engine.compute(findings, []);
|
|
273
|
-
assert.ok(result.score < 100, 'Score should decrease with critical finding');
|
|
274
|
-
// 25 pts deduction capped at category weight of 15
|
|
275
|
-
assert.equal(result.categories.secrets.deduction, 15);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('applies confidence multiplier', () => {
|
|
279
|
-
const engine = new ScoringEngine();
|
|
280
|
-
const highConf = [{ severity: 'high', category: 'injection', confidence: 'high' }];
|
|
281
|
-
const lowConf = [{ severity: 'high', category: 'injection', confidence: 'low' }];
|
|
282
|
-
|
|
283
|
-
const highResult = engine.compute(highConf, []);
|
|
284
|
-
const lowResult = engine.compute(lowConf, []);
|
|
285
|
-
assert.ok(lowResult.score > highResult.score, 'Low confidence should deduct less');
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('caps deduction at category weight', () => {
|
|
289
|
-
const engine = new ScoringEngine();
|
|
290
|
-
// 10 critical findings in secrets (25 pts each = 250, but capped at 15)
|
|
291
|
-
const findings = Array(10).fill({ severity: 'critical', category: 'secrets', confidence: 'high' });
|
|
292
|
-
const result = engine.compute(findings, []);
|
|
293
|
-
assert.equal(result.categories.secrets.deduction, 15);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('handles dependency vulnerabilities', () => {
|
|
297
|
-
const engine = new ScoringEngine();
|
|
298
|
-
const depVulns = [{ severity: 'critical' }, { severity: 'high' }];
|
|
299
|
-
const result = engine.compute([], depVulns);
|
|
300
|
-
assert.ok(result.score < 100);
|
|
301
|
-
assert.ok(result.categories.deps.deduction > 0);
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
it('saves and loads history', () => {
|
|
305
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-score-'));
|
|
306
|
-
try {
|
|
307
|
-
const engine = new ScoringEngine();
|
|
308
|
-
const result = engine.compute([], []);
|
|
309
|
-
engine.saveToHistory(dir, result);
|
|
310
|
-
engine.saveToHistory(dir, result);
|
|
311
|
-
|
|
312
|
-
const history = engine.loadHistory(dir);
|
|
313
|
-
assert.equal(history.length, 2);
|
|
314
|
-
|
|
315
|
-
const trend = engine.getTrend(dir, 100);
|
|
316
|
-
assert.ok(trend);
|
|
317
|
-
assert.equal(trend.diff, 0);
|
|
318
|
-
assert.equal(trend.direction, 'unchanged');
|
|
319
|
-
} finally { cleanup(dir); }
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
// =============================================================================
|
|
324
|
-
// CACHE MANAGER
|
|
325
|
-
// =============================================================================
|
|
326
|
-
|
|
327
|
-
describe('CacheManager', async () => {
|
|
328
|
-
const { CacheManager } = await import('../utils/cache-manager.js');
|
|
329
|
-
|
|
330
|
-
it('save/load/diff cycle works', () => {
|
|
331
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-cache-'));
|
|
332
|
-
const testFile = path.join(dir, 'test.js');
|
|
333
|
-
fs.writeFileSync(testFile, 'const x = 1;');
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const cache = new CacheManager(dir);
|
|
337
|
-
const findings = [{ file: testFile, line: 1, rule: 'TEST', severity: 'low', category: 'test' }];
|
|
338
|
-
cache.save([testFile], findings, null, { score: 90, grade: { letter: 'A' } });
|
|
339
|
-
|
|
340
|
-
// Load and verify
|
|
341
|
-
const loaded = cache.load();
|
|
342
|
-
assert.ok(loaded, 'Cache should load successfully');
|
|
343
|
-
assert.equal(loaded.stats.totalFiles, 1);
|
|
344
|
-
|
|
345
|
-
// Diff with same files — no changes
|
|
346
|
-
const diff = cache.diff([testFile]);
|
|
347
|
-
assert.equal(diff.changedFiles.length, 0);
|
|
348
|
-
assert.equal(diff.unchangedCount, 1);
|
|
349
|
-
assert.equal(diff.cachedFindings.length, 1);
|
|
350
|
-
} finally { cleanup(dir); }
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
it('detects changed files', () => {
|
|
354
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-cache-'));
|
|
355
|
-
const testFile = path.join(dir, 'test.js');
|
|
356
|
-
fs.writeFileSync(testFile, 'const x = 1;');
|
|
357
|
-
|
|
358
|
-
try {
|
|
359
|
-
const cache = new CacheManager(dir);
|
|
360
|
-
cache.save([testFile], [], null, null);
|
|
361
|
-
cache.load();
|
|
362
|
-
|
|
363
|
-
// Modify file
|
|
364
|
-
fs.writeFileSync(testFile, 'const x = 2; // changed');
|
|
365
|
-
const diff = cache.diff([testFile]);
|
|
366
|
-
assert.equal(diff.changedFiles.length, 1);
|
|
367
|
-
assert.equal(diff.modifiedCount, 1);
|
|
368
|
-
} finally { cleanup(dir); }
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('invalidates cache', () => {
|
|
372
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-cache-'));
|
|
373
|
-
const testFile = path.join(dir, 'test.js');
|
|
374
|
-
fs.writeFileSync(testFile, 'x');
|
|
375
|
-
|
|
376
|
-
try {
|
|
377
|
-
const cache = new CacheManager(dir);
|
|
378
|
-
cache.save([testFile], [], null, null);
|
|
379
|
-
assert.ok(cache.load());
|
|
380
|
-
|
|
381
|
-
cache.invalidate();
|
|
382
|
-
assert.equal(cache.load(), null);
|
|
383
|
-
} finally { cleanup(dir); }
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
it('LLM cache save/load works', () => {
|
|
387
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-llm-'));
|
|
388
|
-
|
|
389
|
-
try {
|
|
390
|
-
const cache = new CacheManager(dir);
|
|
391
|
-
const finding = { file: '/test.js', line: 1, rule: 'TEST', matched: 'x' };
|
|
392
|
-
const key = cache.getLLMCacheKey(finding);
|
|
393
|
-
|
|
394
|
-
cache.saveLLMClassifications({
|
|
395
|
-
[key]: { classification: 'true_positive', reason: 'test', fix: 'fix it', cachedAt: new Date().toISOString() },
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
const loaded = cache.loadLLMClassifications();
|
|
399
|
-
assert.ok(loaded[key]);
|
|
400
|
-
assert.equal(loaded[key].classification, 'true_positive');
|
|
401
|
-
} finally { cleanup(dir); }
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
|
|
405
|
-
// =============================================================================
|
|
406
|
-
// REDOS SAFETY
|
|
407
|
-
// =============================================================================
|
|
408
|
-
|
|
409
|
-
describe('ReDoS Safety', async () => {
|
|
410
|
-
// Import agents to get their patterns
|
|
411
|
-
const { default: InjectionTester } = await import('../agents/injection-tester.js');
|
|
412
|
-
const { default: AuthBypassAgent } = await import('../agents/auth-bypass-agent.js');
|
|
413
|
-
const { default: APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
414
|
-
const { default: LLMRedTeam } = await import('../agents/llm-redteam.js');
|
|
415
|
-
const { default: SSRFProber } = await import('../agents/ssrf-prober.js');
|
|
416
|
-
const { default: ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
417
|
-
|
|
418
|
-
// Adversarial inputs that trigger catastrophic backtracking in vulnerable patterns
|
|
419
|
-
const adversarialInputs = [
|
|
420
|
-
'a'.repeat(100),
|
|
421
|
-
'/' + '\\s*'.repeat(50),
|
|
422
|
-
'{' + ' '.repeat(100) + '}',
|
|
423
|
-
'req.body' + '.x'.repeat(50),
|
|
424
|
-
'http://' + 'a'.repeat(100) + '/path',
|
|
425
|
-
'; '.repeat(100),
|
|
426
|
-
'cookie=' + 'a=b; '.repeat(50),
|
|
427
|
-
];
|
|
428
|
-
|
|
429
|
-
it('all agent patterns complete within 50ms on adversarial input', async () => {
|
|
430
|
-
const agents = [
|
|
431
|
-
new InjectionTester(),
|
|
432
|
-
new AuthBypassAgent(),
|
|
433
|
-
new APIFuzzer(),
|
|
434
|
-
new LLMRedTeam(),
|
|
435
|
-
new SSRFProber(),
|
|
436
|
-
new ConfigAuditor(),
|
|
437
|
-
];
|
|
438
|
-
|
|
439
|
-
for (const input of adversarialInputs) {
|
|
440
|
-
const { dir, file } = writeTempFile(input);
|
|
441
|
-
try {
|
|
442
|
-
for (const agent of agents) {
|
|
443
|
-
const start = performance.now();
|
|
444
|
-
await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
445
|
-
const elapsed = performance.now() - start;
|
|
446
|
-
assert.ok(
|
|
447
|
-
elapsed < 2000, // 2s generous limit per agent per file
|
|
448
|
-
`${agent.name} took ${elapsed.toFixed(0)}ms on adversarial input (limit: 2000ms)`
|
|
449
|
-
);
|
|
450
|
-
}
|
|
451
|
-
} finally { cleanup(dir); }
|
|
452
|
-
}
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
// =============================================================================
|
|
457
|
-
// ORCHESTRATOR
|
|
458
|
-
// =============================================================================
|
|
459
|
-
|
|
460
|
-
describe('Orchestrator', async () => {
|
|
461
|
-
const { Orchestrator } = await import('../agents/orchestrator.js');
|
|
462
|
-
|
|
463
|
-
it('handles agent timeout gracefully', async () => {
|
|
464
|
-
const orchestrator = new Orchestrator();
|
|
465
|
-
|
|
466
|
-
// Mock agent that takes forever
|
|
467
|
-
const slowAgent = {
|
|
468
|
-
name: 'SlowAgent',
|
|
469
|
-
category: 'test',
|
|
470
|
-
analyze: () => new Promise(resolve => setTimeout(resolve, 60000)),
|
|
471
|
-
};
|
|
472
|
-
orchestrator.register(slowAgent);
|
|
473
|
-
|
|
474
|
-
// Use a very short timeout
|
|
475
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-orch-'));
|
|
476
|
-
fs.writeFileSync(path.join(dir, 'test.js'), 'x');
|
|
477
|
-
|
|
478
|
-
try {
|
|
479
|
-
const result = await orchestrator.runAll(dir, { quiet: true, timeout: 100 });
|
|
480
|
-
assert.equal(result.agentResults.length, 1);
|
|
481
|
-
assert.equal(result.agentResults[0].success, false);
|
|
482
|
-
assert.ok(result.agentResults[0].error.includes('timed out'));
|
|
483
|
-
} finally { cleanup(dir); }
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it('deduplicates findings', () => {
|
|
487
|
-
const orchestrator = new Orchestrator();
|
|
488
|
-
const findings = [
|
|
489
|
-
{ file: 'a.js', line: 1, rule: 'R1', severity: 'high' },
|
|
490
|
-
{ file: 'a.js', line: 1, rule: 'R1', severity: 'high' },
|
|
491
|
-
{ file: 'a.js', line: 2, rule: 'R1', severity: 'high' },
|
|
492
|
-
];
|
|
493
|
-
const deduped = orchestrator.deduplicate(findings);
|
|
494
|
-
assert.equal(deduped.length, 2);
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
it('tunes confidence for test files', () => {
|
|
498
|
-
const orchestrator = new Orchestrator();
|
|
499
|
-
const findings = [
|
|
500
|
-
{ file: '/project/__tests__/foo.test.js', line: 1, rule: 'R1', severity: 'high', confidence: 'high', matched: 'eval(x)' },
|
|
501
|
-
{ file: '/project/src/app.js', line: 1, rule: 'R2', severity: 'high', confidence: 'high', matched: 'eval(x)' },
|
|
502
|
-
];
|
|
503
|
-
const tuned = orchestrator.tuneConfidence(findings);
|
|
504
|
-
assert.equal(tuned[0].confidence, 'low'); // test file → low
|
|
505
|
-
assert.equal(tuned[1].confidence, 'high'); // src file → unchanged
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
it('tunes confidence for doc files', () => {
|
|
509
|
-
const orchestrator = new Orchestrator();
|
|
510
|
-
const findings = [
|
|
511
|
-
{ file: '/project/README.md', line: 5, rule: 'R1', severity: 'high', confidence: 'high', matched: 'password = "test"' },
|
|
512
|
-
];
|
|
513
|
-
const tuned = orchestrator.tuneConfidence(findings);
|
|
514
|
-
assert.equal(tuned[0].confidence, 'low');
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
it('tunes confidence for example paths', () => {
|
|
518
|
-
const orchestrator = new Orchestrator();
|
|
519
|
-
const findings = [
|
|
520
|
-
{ file: '/project/examples/demo.js', line: 1, rule: 'R1', severity: 'high', confidence: 'high', matched: 'eval(x)' },
|
|
521
|
-
];
|
|
522
|
-
const tuned = orchestrator.tuneConfidence(findings);
|
|
523
|
-
assert.equal(tuned[0].confidence, 'medium');
|
|
524
|
-
});
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
// =============================================================================
|
|
528
|
-
// SUPABASE RLS AGENT
|
|
529
|
-
// =============================================================================
|
|
530
|
-
|
|
531
|
-
describe('SupabaseRLSAgent', () => {
|
|
532
|
-
it('detects service_role key in client code', async () => {
|
|
533
|
-
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
534
|
-
const agent = new SupabaseRLSAgent();
|
|
535
|
-
const { dir, file } = writeTempFile(`const supabase = createClient(url, SUPABASE_SERVICE_ROLE_KEY);`);
|
|
536
|
-
try {
|
|
537
|
-
const findings = agent.scanFileWithPatterns(file, [
|
|
538
|
-
{ rule: 'SUPABASE_SERVICE_KEY_CLIENT', regex: /SUPABASE_SERVICE_ROLE_KEY|service_role_key|serviceRoleKey|supabaseAdmin/g, severity: 'critical', title: 'test', description: 'test' }
|
|
539
|
-
]);
|
|
540
|
-
assert.ok(findings.some(f => f.rule === 'SUPABASE_SERVICE_KEY_CLIENT'));
|
|
541
|
-
} finally { cleanup(dir); }
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
it('detects missing RLS on table', async () => {
|
|
545
|
-
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
546
|
-
const agent = new SupabaseRLSAgent();
|
|
547
|
-
|
|
548
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-rls-'));
|
|
549
|
-
const sqlFile = path.join(dir, 'migration.sql');
|
|
550
|
-
fs.writeFileSync(sqlFile, `CREATE TABLE users (id uuid PRIMARY KEY, name text);\nCREATE TABLE posts (id uuid PRIMARY KEY);`);
|
|
551
|
-
const jsFile = path.join(dir, 'app.js');
|
|
552
|
-
fs.writeFileSync(jsFile, 'const x = 1;');
|
|
553
|
-
|
|
554
|
-
try {
|
|
555
|
-
const findings = await agent.analyze({
|
|
556
|
-
rootPath: dir,
|
|
557
|
-
files: [sqlFile, jsFile],
|
|
558
|
-
recon: {},
|
|
559
|
-
options: {},
|
|
560
|
-
});
|
|
561
|
-
// Should flag both tables as missing RLS
|
|
562
|
-
const rlsFindings = findings.filter(f => f.rule === 'SUPABASE_NO_RLS_POLICY');
|
|
563
|
-
assert.ok(rlsFindings.length >= 2, `Expected >=2 RLS findings, got ${rlsFindings.length}`);
|
|
564
|
-
} finally { cleanup(dir); }
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('does not flag tables with RLS enabled', async () => {
|
|
568
|
-
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
569
|
-
const agent = new SupabaseRLSAgent();
|
|
570
|
-
|
|
571
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-rls2-'));
|
|
572
|
-
const sqlFile = path.join(dir, 'migration.sql');
|
|
573
|
-
fs.writeFileSync(sqlFile, `CREATE TABLE users (id uuid PRIMARY KEY);\nALTER TABLE users ENABLE ROW LEVEL SECURITY;`);
|
|
574
|
-
|
|
575
|
-
try {
|
|
576
|
-
const findings = await agent.analyze({
|
|
577
|
-
rootPath: dir,
|
|
578
|
-
files: [sqlFile],
|
|
579
|
-
recon: {},
|
|
580
|
-
options: {},
|
|
581
|
-
});
|
|
582
|
-
const rlsFindings = findings.filter(f => f.rule === 'SUPABASE_NO_RLS_POLICY');
|
|
583
|
-
assert.equal(rlsFindings.length, 0);
|
|
584
|
-
} finally { cleanup(dir); }
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
// =============================================================================
|
|
589
|
-
// CONFIG AUDITOR — NEW TERRAFORM/K8S PATTERNS
|
|
590
|
-
// =============================================================================
|
|
591
|
-
|
|
592
|
-
describe('ConfigAuditor (v4.3 patterns)', () => {
|
|
593
|
-
it('detects publicly accessible RDS', async () => {
|
|
594
|
-
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
595
|
-
const agent = new ConfigAuditor();
|
|
596
|
-
const { dir, file } = writeTempFile(`resource "aws_db_instance" "main" {\n publicly_accessible = true\n}`, '.tf');
|
|
597
|
-
try {
|
|
598
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
599
|
-
assert.ok(findings.some(f => f.rule === 'TERRAFORM_RDS_PUBLIC'));
|
|
600
|
-
} finally { cleanup(dir); }
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it('detects CloudFront allowing HTTP', async () => {
|
|
604
|
-
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
605
|
-
const agent = new ConfigAuditor();
|
|
606
|
-
const { dir, file } = writeTempFile(`viewer_protocol_policy = "allow-all"`, '.tf');
|
|
607
|
-
try {
|
|
608
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
609
|
-
assert.ok(findings.some(f => f.rule === 'TERRAFORM_CLOUDFRONT_HTTP'));
|
|
610
|
-
} finally { cleanup(dir); }
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
it('detects K8s :latest image tag', async () => {
|
|
614
|
-
const { ConfigAuditor } = await import('../agents/config-auditor.js');
|
|
615
|
-
const agent = new ConfigAuditor();
|
|
616
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-k8s-'));
|
|
617
|
-
const k8sDir = path.join(dir, 'k8s');
|
|
618
|
-
fs.mkdirSync(k8sDir);
|
|
619
|
-
const file = path.join(k8sDir, 'deployment.yaml');
|
|
620
|
-
fs.writeFileSync(file, `kind: Deployment\nspec:\n containers:\n - image: nginx:latest`);
|
|
621
|
-
try {
|
|
622
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
623
|
-
assert.ok(findings.some(f => f.rule === 'K8S_LATEST_IMAGE'));
|
|
624
|
-
} finally { cleanup(dir); }
|
|
625
|
-
});
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
// =============================================================================
|
|
629
|
-
// API FUZZER — RATE LIMITING & OPENAPI
|
|
630
|
-
// =============================================================================
|
|
631
|
-
|
|
632
|
-
describe('APIFuzzer (v4.3 patterns)', () => {
|
|
633
|
-
it('detects missing rate limiting in Express app', async () => {
|
|
634
|
-
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
635
|
-
const agent = new APIFuzzer();
|
|
636
|
-
|
|
637
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-api-'));
|
|
638
|
-
const file = path.join(dir, 'app.js');
|
|
639
|
-
fs.writeFileSync(file, `import express from 'express';\nconst app = express();\napp.listen(3000);`);
|
|
640
|
-
|
|
641
|
-
try {
|
|
642
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
643
|
-
assert.ok(findings.some(f => f.rule === 'API_NO_RATE_LIMIT'));
|
|
644
|
-
} finally { cleanup(dir); }
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
it('does not flag when rate limiter is present', async () => {
|
|
648
|
-
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
649
|
-
const agent = new APIFuzzer();
|
|
650
|
-
|
|
651
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-api2-'));
|
|
652
|
-
const file = path.join(dir, 'app.js');
|
|
653
|
-
fs.writeFileSync(file, `import express from 'express';\nimport rateLimit from 'express-rate-limit';\nconst app = express();\napp.listen(3000);`);
|
|
654
|
-
|
|
655
|
-
try {
|
|
656
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
657
|
-
assert.ok(!findings.some(f => f.rule === 'API_NO_RATE_LIMIT'));
|
|
658
|
-
} finally { cleanup(dir); }
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it('detects secrets in OpenAPI examples', async () => {
|
|
662
|
-
const { APIFuzzer } = await import('../agents/api-fuzzer.js');
|
|
663
|
-
const agent = new APIFuzzer();
|
|
664
|
-
|
|
665
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'shipsafe-oas-'));
|
|
666
|
-
const file = path.join(dir, 'openapi.yaml');
|
|
667
|
-
fs.writeFileSync(file, `openapi: 3.0.0\npaths:\n /users:\n get:\n parameters:\n - name: token\n example: sk-proj-abc123xyz`);
|
|
668
|
-
|
|
669
|
-
try {
|
|
670
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
671
|
-
assert.ok(findings.some(f => f.rule === 'OPENAPI_EXAMPLE_SECRETS' || f.rule === 'OPENAPI_NO_SECURITY'));
|
|
672
|
-
} finally { cleanup(dir); }
|
|
673
|
-
});
|
|
674
|
-
});
|
|
675
|
-
|
|
676
|
-
// =============================================================================
|
|
677
|
-
// AUTOFIX RULES
|
|
678
|
-
// =============================================================================
|
|
679
|
-
|
|
680
|
-
describe('Autofix Rules', () => {
|
|
681
|
-
it('fixes rejectUnauthorized: false', async () => {
|
|
682
|
-
const { applyAutofix } = await import('../utils/autofix-rules.js');
|
|
683
|
-
const line = ' rejectUnauthorized: false,';
|
|
684
|
-
const fixed = applyAutofix('TLS_REJECT_UNAUTHORIZED', line);
|
|
685
|
-
assert.ok(fixed.includes('rejectUnauthorized: true'));
|
|
686
|
-
assert.ok(!fixed.includes('false'));
|
|
687
|
-
});
|
|
688
|
-
|
|
689
|
-
it('fixes DEBUG = true', async () => {
|
|
690
|
-
const { applyAutofix } = await import('../utils/autofix-rules.js');
|
|
691
|
-
assert.ok(applyAutofix('DEBUG_MODE_PRODUCTION', 'DEBUG = true').includes('false'));
|
|
692
|
-
assert.ok(applyAutofix('DEBUG_MODE_PRODUCTION', 'DEBUG = True').includes('False'));
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it('fixes shell: true', async () => {
|
|
696
|
-
const { applyAutofix } = await import('../utils/autofix-rules.js');
|
|
697
|
-
const fixed = applyAutofix('CMD_INJECTION_SHELL_TRUE', ' shell: true');
|
|
698
|
-
assert.ok(fixed.includes('shell: false'));
|
|
699
|
-
});
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// =============================================================================
|
|
703
|
-
// CODE CONTEXT
|
|
704
|
-
// =============================================================================
|
|
705
|
-
|
|
706
|
-
describe('Code Context in Findings', () => {
|
|
707
|
-
it('attaches codeContext to findings from scanFileWithPatterns', async () => {
|
|
708
|
-
const { InjectionTester } = await import('../agents/injection-tester.js');
|
|
709
|
-
const agent = new InjectionTester();
|
|
710
|
-
const code = `const x = 1;\nconst y = 2;\nconst z = eval(userInput);\nconst w = 3;\nconst v = 4;`;
|
|
711
|
-
const { dir, file } = writeTempFile(code);
|
|
712
|
-
try {
|
|
713
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
714
|
-
const evalFinding = findings.find(f => f.rule && f.rule.includes('EVAL'));
|
|
715
|
-
if (evalFinding) {
|
|
716
|
-
assert.ok(evalFinding.codeContext, 'Finding should have codeContext');
|
|
717
|
-
assert.ok(Array.isArray(evalFinding.codeContext));
|
|
718
|
-
assert.ok(evalFinding.codeContext.some(c => c.highlight === true));
|
|
719
|
-
}
|
|
720
|
-
} finally { cleanup(dir); }
|
|
721
|
-
});
|
|
722
|
-
});
|
|
723
|
-
|
|
724
|
-
// =============================================================================
|
|
725
|
-
// MCP SECURITY AGENT (v5.0)
|
|
726
|
-
// =============================================================================
|
|
727
|
-
|
|
728
|
-
describe('MCPSecurityAgent', async () => {
|
|
729
|
-
const { MCPSecurityAgent } = await import('../agents/mcp-security-agent.js');
|
|
730
|
-
const agent = new MCPSecurityAgent();
|
|
731
|
-
|
|
732
|
-
it('detects MCP tool with shell execution', async () => {
|
|
733
|
-
const { dir, file } = writeTempFile('server.tool("run_cmd", async (a) => { return execSync(a.cmd); });');
|
|
734
|
-
try {
|
|
735
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
736
|
-
assert.ok(findings.some(f => f.rule === 'MCP_TOOL_SHELL_EXEC'), 'Should detect shell exec in MCP tool');
|
|
737
|
-
} finally { cleanup(dir); }
|
|
738
|
-
});
|
|
739
|
-
|
|
740
|
-
it('detects MCP tool with file system write', async () => {
|
|
741
|
-
const { dir, file } = writeTempFile('server.tool("write", async (a) => { fs.writeFileSync(a.path, a.data); });');
|
|
742
|
-
try {
|
|
743
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
744
|
-
assert.ok(findings.some(f => f.rule === 'MCP_TOOL_FS_WRITE'), 'Should detect fs write in MCP tool');
|
|
745
|
-
} finally { cleanup(dir); }
|
|
746
|
-
});
|
|
747
|
-
|
|
748
|
-
it('detects MCP tool arguments passed to eval', async () => {
|
|
749
|
-
const { dir, file } = writeTempFile('server.tool("exec", async (a) => { return eval(a.code); });');
|
|
750
|
-
try {
|
|
751
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
752
|
-
assert.ok(findings.some(f => f.rule === 'MCP_TOOL_ARGS_TO_EVAL'), 'Should detect eval in MCP tool');
|
|
753
|
-
} finally { cleanup(dir); }
|
|
754
|
-
});
|
|
755
|
-
|
|
756
|
-
it('detects HTTP without TLS for remote MCP', async () => {
|
|
757
|
-
const { dir, file } = writeTempFile(`
|
|
758
|
-
const transport = new SSEServerTransport("http://remote-server.com:8080/mcp");
|
|
759
|
-
`);
|
|
760
|
-
try {
|
|
761
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
762
|
-
assert.ok(findings.some(f => f.rule === 'MCP_HTTP_NO_TLS'), 'Should detect HTTP without TLS');
|
|
763
|
-
} finally { cleanup(dir); }
|
|
764
|
-
});
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
// =============================================================================
|
|
768
|
-
// AGENTIC SECURITY AGENT (v5.0)
|
|
769
|
-
// =============================================================================
|
|
770
|
-
|
|
771
|
-
describe('AgenticSecurityAgent', async () => {
|
|
772
|
-
const { AgenticSecurityAgent } = await import('../agents/agentic-security-agent.js');
|
|
773
|
-
const agent = new AgenticSecurityAgent();
|
|
774
|
-
|
|
775
|
-
it('detects auto-execute without confirmation', async () => {
|
|
776
|
-
const { dir, file } = writeTempFile(`
|
|
777
|
-
const config = {
|
|
778
|
-
auto_approve: true,
|
|
779
|
-
requireConfirmation: false,
|
|
780
|
-
};
|
|
781
|
-
`);
|
|
782
|
-
try {
|
|
783
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
784
|
-
assert.ok(findings.some(f => f.rule === 'AGENT_TOOL_NO_CONFIRMATION'), 'Should detect auto-approve');
|
|
785
|
-
} finally { cleanup(dir); }
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
it('detects user input in agent memory', async () => {
|
|
789
|
-
const { dir, file } = writeTempFile(`
|
|
790
|
-
memory.push(userMessage);
|
|
791
|
-
context.add(input);
|
|
792
|
-
`);
|
|
793
|
-
try {
|
|
794
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
795
|
-
assert.ok(findings.some(f => f.rule === 'AGENT_MEMORY_USER_WRITE'), 'Should detect memory poisoning');
|
|
796
|
-
} finally { cleanup(dir); }
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
it('detects agent with shell tool access', async () => {
|
|
800
|
-
const { dir, file } = writeTempFile(`
|
|
801
|
-
const tools = [searchTool, child_process.exec];
|
|
802
|
-
const functions = [subprocess.run];
|
|
803
|
-
`);
|
|
804
|
-
try {
|
|
805
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
806
|
-
assert.ok(findings.some(f => f.rule === 'AGENT_TOOL_SHELL_ACCESS'), 'Should detect shell access in tools');
|
|
807
|
-
} finally { cleanup(dir); }
|
|
808
|
-
});
|
|
809
|
-
});
|
|
810
|
-
|
|
811
|
-
// =============================================================================
|
|
812
|
-
// RAG SECURITY AGENT (v5.0)
|
|
813
|
-
// =============================================================================
|
|
814
|
-
|
|
815
|
-
describe('RAGSecurityAgent', async () => {
|
|
816
|
-
const { RAGSecurityAgent } = await import('../agents/rag-security-agent.js');
|
|
817
|
-
const agent = new RAGSecurityAgent();
|
|
818
|
-
|
|
819
|
-
it('detects user upload to vector store', async () => {
|
|
820
|
-
const { dir, file } = writeTempFile('const docs = multer().single("file"); await vectorStore.addDocuments(docs);');
|
|
821
|
-
try {
|
|
822
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
823
|
-
assert.ok(findings.some(f => f.rule === 'RAG_USER_UPLOAD_TO_VECTORDB'), 'Should detect user upload to vector DB');
|
|
824
|
-
} finally { cleanup(dir); }
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
it('detects trust_remote_code=True', async () => {
|
|
828
|
-
const { dir, file } = writeTempFile(`
|
|
829
|
-
model = AutoModel.from_pretrained("user/model", trust_remote_code=True)
|
|
830
|
-
`, '.py');
|
|
831
|
-
try {
|
|
832
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
833
|
-
assert.ok(findings.some(f => f.rule === 'RAG_TRUST_REMOTE_CODE'), 'Should detect trust_remote_code');
|
|
834
|
-
} finally { cleanup(dir); }
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it('detects pickle model loading', async () => {
|
|
838
|
-
const { dir, file } = writeTempFile(`
|
|
839
|
-
model = torch.load("model.pkl")
|
|
840
|
-
`, '.py');
|
|
841
|
-
try {
|
|
842
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
843
|
-
assert.ok(findings.some(f => f.rule === 'RAG_PICKLE_EMBEDDING_MODEL'), 'Should detect pickle load');
|
|
844
|
-
} finally { cleanup(dir); }
|
|
845
|
-
});
|
|
846
|
-
});
|
|
847
|
-
|
|
848
|
-
// =============================================================================
|
|
849
|
-
// PII COMPLIANCE AGENT (v5.0)
|
|
850
|
-
// =============================================================================
|
|
851
|
-
|
|
852
|
-
describe('PIIComplianceAgent', async () => {
|
|
853
|
-
const { PIIComplianceAgent } = await import('../agents/pii-compliance-agent.js');
|
|
854
|
-
const agent = new PIIComplianceAgent();
|
|
855
|
-
|
|
856
|
-
it('detects PII in console.log', async () => {
|
|
857
|
-
const { dir, file } = writeTempFile('console.log("User email:", user.email);');
|
|
858
|
-
try {
|
|
859
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
860
|
-
assert.ok(findings.some(f => f.rule === 'PII_IN_CONSOLE_LOG'), 'Should detect PII in console.log');
|
|
861
|
-
} finally { cleanup(dir); }
|
|
862
|
-
});
|
|
863
|
-
|
|
864
|
-
it('detects PII sent to analytics', async () => {
|
|
865
|
-
const { dir, file } = writeTempFile(`
|
|
866
|
-
analytics.track("signup", { email: user.email, phone: user.phone });
|
|
867
|
-
`);
|
|
868
|
-
try {
|
|
869
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
870
|
-
assert.ok(findings.some(f => f.rule === 'PII_TO_ANALYTICS'), 'Should detect PII to analytics');
|
|
871
|
-
} finally { cleanup(dir); }
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
it('detects SSN pattern in source code', async () => {
|
|
875
|
-
const { dir, file } = writeTempFile('const testSSN = "123-45-6789";');
|
|
876
|
-
try {
|
|
877
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
878
|
-
assert.ok(findings.some(f => f.rule === 'PII_SSN_IN_CODE'), 'Should detect SSN pattern');
|
|
879
|
-
} finally { cleanup(dir); }
|
|
880
|
-
});
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
// =============================================================================
|
|
884
|
-
// VIBE CODE DETECTION (v5.0)
|
|
885
|
-
// =============================================================================
|
|
886
|
-
|
|
887
|
-
describe('Vibe Code Detection', async () => {
|
|
888
|
-
const { InjectionTester } = await import('../agents/injection-tester.js');
|
|
889
|
-
const agent = new InjectionTester();
|
|
890
|
-
|
|
891
|
-
it('detects TODO to add authentication', async () => {
|
|
892
|
-
const { dir, file } = writeTempFile('// TODO: add authentication\napp.post("/api/admin", handler);');
|
|
893
|
-
try {
|
|
894
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
895
|
-
assert.ok(findings.some(f => f.rule === 'VIBE_TODO_AUTH'), 'Should detect TODO auth');
|
|
896
|
-
} finally { cleanup(dir); }
|
|
897
|
-
});
|
|
898
|
-
|
|
899
|
-
it('detects placeholder secrets', async () => {
|
|
900
|
-
const { dir, file } = writeTempFile('const apiKey = "your-api-key-here";');
|
|
901
|
-
try {
|
|
902
|
-
const findings = await agent.analyze({ rootPath: dir, files: [file], recon: {}, options: {} });
|
|
903
|
-
assert.ok(findings.some(f => f.rule === 'VIBE_PLACEHOLDER_SECRET'), 'Should detect placeholder secret');
|
|
904
|
-
} finally { cleanup(dir); }
|
|
905
|
-
});
|
|
906
|
-
});
|
|
907
|
-
|
|
908
|
-
// =============================================================================
|
|
909
|
-
// VERIFIER AGENT (v5.0)
|
|
910
|
-
// =============================================================================
|
|
911
|
-
|
|
912
|
-
describe('VerifierAgent', async () => {
|
|
913
|
-
const { VerifierAgent } = await import('../agents/verifier-agent.js');
|
|
914
|
-
const verifier = new VerifierAgent();
|
|
915
|
-
|
|
916
|
-
it('confirms finding with user input and no sanitization', async () => {
|
|
917
|
-
const code = 'app.post("/api", (req, res) => {\n const name = req.body.name;\n db.query(`SELECT * FROM users WHERE name = ${name}`);\n res.send("ok");\n});';
|
|
918
|
-
const { dir, file } = writeTempFile(code);
|
|
919
|
-
try {
|
|
920
|
-
const findings = [{
|
|
921
|
-
file, line: 3, severity: 'critical', confidence: 'high',
|
|
922
|
-
rule: 'SQL_INJECTION', matched: '`SELECT * FROM users',
|
|
923
|
-
}];
|
|
924
|
-
const verified = verifier.verify(findings);
|
|
925
|
-
assert.strictEqual(verified[0].verified, true, 'Should verify finding with user input');
|
|
926
|
-
assert.strictEqual(verified[0].confidence, 'high', 'Should keep high confidence');
|
|
927
|
-
} finally { cleanup(dir); }
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
it('downgrades finding with sanitization upstream', async () => {
|
|
931
|
-
const code = 'app.post("/api", (req, res) => {\n const name = sanitize(req.body.name);\n const validated = validator.escape(name);\n db.query(`SELECT * FROM users WHERE name = ${validated}`);\n res.send("ok");\n});';
|
|
932
|
-
const { dir, file } = writeTempFile(code);
|
|
933
|
-
try {
|
|
934
|
-
const findings = [{
|
|
935
|
-
file, line: 4, severity: 'critical', confidence: 'high',
|
|
936
|
-
rule: 'SQL_INJECTION', matched: '`SELECT * FROM users',
|
|
937
|
-
}];
|
|
938
|
-
const verified = verifier.verify(findings);
|
|
939
|
-
assert.strictEqual(verified[0].verified, false, 'Should not verify sanitized finding');
|
|
940
|
-
assert.strictEqual(verified[0].confidence, 'medium', 'Should downgrade confidence');
|
|
941
|
-
} finally { cleanup(dir); }
|
|
942
|
-
});
|
|
943
|
-
|
|
944
|
-
it('skips verification for medium/low severity', async () => {
|
|
945
|
-
const findings = [{
|
|
946
|
-
file: '/fake/path.js', line: 1, severity: 'medium', confidence: 'high',
|
|
947
|
-
rule: 'SOME_RULE', matched: 'something',
|
|
948
|
-
}];
|
|
949
|
-
const verified = verifier.verify(findings);
|
|
950
|
-
assert.strictEqual(verified[0].verified, null, 'Should skip medium severity');
|
|
951
|
-
});
|
|
952
|
-
});
|
|
953
|
-
|
|
954
|
-
// =============================================================================
|
|
955
|
-
// DEEP ANALYZER
|
|
956
|
-
// =============================================================================
|
|
957
|
-
|
|
958
|
-
describe('DeepAnalyzer', async () => {
|
|
959
|
-
const { DeepAnalyzer } = await import('../agents/deep-analyzer.js');
|
|
960
|
-
|
|
961
|
-
it('returns findings unchanged when no provider is set', async () => {
|
|
962
|
-
const analyzer = new DeepAnalyzer({ provider: null });
|
|
963
|
-
const findings = [
|
|
964
|
-
{ file: '/test.js', line: 1, severity: 'critical', rule: 'SQL_INJECTION', confidence: 'high' },
|
|
965
|
-
];
|
|
966
|
-
const result = await analyzer.analyze(findings);
|
|
967
|
-
assert.strictEqual(result.length, 1);
|
|
968
|
-
assert.strictEqual(result[0].deepAnalysis, undefined, 'No deep analysis without provider');
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
it('only selects critical/high findings for analysis', async () => {
|
|
972
|
-
// Mock provider that records what it receives
|
|
973
|
-
let receivedPrompt = '';
|
|
974
|
-
const mockProvider = {
|
|
975
|
-
name: 'MockLLM',
|
|
976
|
-
async complete(sys, prompt) {
|
|
977
|
-
receivedPrompt = prompt;
|
|
978
|
-
return '[]';
|
|
979
|
-
},
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
const analyzer = new DeepAnalyzer({ provider: mockProvider, budgetCents: 100 });
|
|
983
|
-
const findings = [
|
|
984
|
-
{ file: '/a.js', line: 1, severity: 'critical', rule: 'RULE_A', confidence: 'high', title: 'A', description: 'A' },
|
|
985
|
-
{ file: '/b.js', line: 2, severity: 'medium', rule: 'RULE_B', confidence: 'high', title: 'B', description: 'B' },
|
|
986
|
-
{ file: '/c.js', line: 3, severity: 'low', rule: 'RULE_C', confidence: 'high', title: 'C', description: 'C' },
|
|
987
|
-
{ file: '/d.js', line: 4, severity: 'high', rule: 'RULE_D', confidence: 'high', title: 'D', description: 'D' },
|
|
988
|
-
];
|
|
989
|
-
await analyzer.analyze(findings);
|
|
990
|
-
// Only critical and high should be in the prompt
|
|
991
|
-
assert.ok(receivedPrompt.includes('RULE_A'), 'Should include critical finding');
|
|
992
|
-
assert.ok(receivedPrompt.includes('RULE_D'), 'Should include high finding');
|
|
993
|
-
assert.ok(!receivedPrompt.includes('RULE_B'), 'Should exclude medium finding');
|
|
994
|
-
assert.ok(!receivedPrompt.includes('RULE_C'), 'Should exclude low finding');
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
it('attaches deep analysis from LLM response', async () => {
|
|
998
|
-
const mockProvider = {
|
|
999
|
-
name: 'MockLLM',
|
|
1000
|
-
async complete() {
|
|
1001
|
-
return JSON.stringify([{
|
|
1002
|
-
findingId: 'test.js:5:XSS_DANGEROUS',
|
|
1003
|
-
tainted: true,
|
|
1004
|
-
sanitized: false,
|
|
1005
|
-
exploitability: 'confirmed',
|
|
1006
|
-
reasoning: 'User input flows directly to innerHTML without sanitization.',
|
|
1007
|
-
}]);
|
|
1008
|
-
},
|
|
1009
|
-
};
|
|
1010
|
-
|
|
1011
|
-
const analyzer = new DeepAnalyzer({ provider: mockProvider, budgetCents: 100 });
|
|
1012
|
-
const findings = [{
|
|
1013
|
-
file: '/some/path/test.js', line: 5, severity: 'critical',
|
|
1014
|
-
rule: 'XSS_DANGEROUS', confidence: 'medium', title: 'XSS', description: 'XSS via innerHTML',
|
|
1015
|
-
}];
|
|
1016
|
-
|
|
1017
|
-
const result = await analyzer.analyze(findings);
|
|
1018
|
-
assert.ok(result[0].deepAnalysis, 'Should have deepAnalysis attached');
|
|
1019
|
-
assert.strictEqual(result[0].deepAnalysis.tainted, true);
|
|
1020
|
-
assert.strictEqual(result[0].deepAnalysis.sanitized, false);
|
|
1021
|
-
assert.strictEqual(result[0].deepAnalysis.exploitability, 'confirmed');
|
|
1022
|
-
assert.strictEqual(result[0].confidence, 'high', 'Confirmed finding should have high confidence');
|
|
1023
|
-
});
|
|
1024
|
-
|
|
1025
|
-
it('downgrades confidence for false_positive analysis', async () => {
|
|
1026
|
-
const mockProvider = {
|
|
1027
|
-
name: 'MockLLM',
|
|
1028
|
-
async complete() {
|
|
1029
|
-
return JSON.stringify([{
|
|
1030
|
-
findingId: 'app.js:10:EVAL_CALL',
|
|
1031
|
-
tainted: false,
|
|
1032
|
-
sanitized: false,
|
|
1033
|
-
exploitability: 'false_positive',
|
|
1034
|
-
reasoning: 'Static string passed to eval, no user input path.',
|
|
1035
|
-
}]);
|
|
1036
|
-
},
|
|
1037
|
-
};
|
|
1038
|
-
|
|
1039
|
-
const analyzer = new DeepAnalyzer({ provider: mockProvider, budgetCents: 100 });
|
|
1040
|
-
const findings = [{
|
|
1041
|
-
file: '/code/app.js', line: 10, severity: 'high',
|
|
1042
|
-
rule: 'EVAL_CALL', confidence: 'high', title: 'Eval', description: 'eval() usage',
|
|
1043
|
-
}];
|
|
1044
|
-
|
|
1045
|
-
const result = await analyzer.analyze(findings);
|
|
1046
|
-
assert.strictEqual(result[0].confidence, 'low', 'False positive should downgrade to low confidence');
|
|
1047
|
-
assert.strictEqual(result[0].deepAnalysis.exploitability, 'false_positive');
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
it('respects budget limit', async () => {
|
|
1051
|
-
let callCount = 0;
|
|
1052
|
-
const mockProvider = {
|
|
1053
|
-
name: 'MockLLM',
|
|
1054
|
-
async complete() {
|
|
1055
|
-
callCount++;
|
|
1056
|
-
// Return a very long response to burn budget
|
|
1057
|
-
return '[]';
|
|
1058
|
-
},
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
const analyzer = new DeepAnalyzer({ provider: mockProvider, budgetCents: 0 });
|
|
1062
|
-
const findings = Array.from({ length: 10 }, (_, i) => ({
|
|
1063
|
-
file: `/f${i}.js`, line: 1, severity: 'critical', rule: `RULE_${i}`,
|
|
1064
|
-
confidence: 'high', title: `Rule ${i}`, description: `Desc ${i}`,
|
|
1065
|
-
}));
|
|
1066
|
-
|
|
1067
|
-
await analyzer.analyze(findings);
|
|
1068
|
-
// With 0 budget, should not make any calls (budget check happens before first batch)
|
|
1069
|
-
assert.strictEqual(callCount, 0, 'Should not call LLM when budget is 0');
|
|
1070
|
-
});
|
|
1071
|
-
|
|
1072
|
-
it('handles LLM errors gracefully', async () => {
|
|
1073
|
-
const mockProvider = {
|
|
1074
|
-
name: 'MockLLM',
|
|
1075
|
-
async complete() {
|
|
1076
|
-
throw new Error('API rate limit exceeded');
|
|
1077
|
-
},
|
|
1078
|
-
};
|
|
1079
|
-
|
|
1080
|
-
const analyzer = new DeepAnalyzer({ provider: mockProvider, budgetCents: 100 });
|
|
1081
|
-
const findings = [{
|
|
1082
|
-
file: '/err.js', line: 1, severity: 'critical', rule: 'SOME_RULE',
|
|
1083
|
-
confidence: 'high', title: 'Rule', description: 'Desc',
|
|
1084
|
-
}];
|
|
1085
|
-
|
|
1086
|
-
// Should not throw
|
|
1087
|
-
const result = await analyzer.analyze(findings);
|
|
1088
|
-
assert.strictEqual(result.length, 1);
|
|
1089
|
-
assert.strictEqual(result[0].deepAnalysis, undefined, 'Failed analysis should not attach result');
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
it('parses malformed LLM response without crashing', async () => {
|
|
1093
|
-
const mockProvider = {
|
|
1094
|
-
name: 'MockLLM',
|
|
1095
|
-
async complete() {
|
|
1096
|
-
return 'This is not valid JSON at all';
|
|
1097
|
-
},
|
|
1098
|
-
};
|
|
1099
|
-
|
|
1100
|
-
const analyzer = new DeepAnalyzer({ provider: mockProvider, budgetCents: 100 });
|
|
1101
|
-
const findings = [{
|
|
1102
|
-
file: '/bad.js', line: 1, severity: 'high', rule: 'BAD_RULE',
|
|
1103
|
-
confidence: 'high', title: 'Bad', description: 'Bad response test',
|
|
1104
|
-
}];
|
|
1105
|
-
|
|
1106
|
-
const result = await analyzer.analyze(findings);
|
|
1107
|
-
assert.strictEqual(result.length, 1);
|
|
1108
|
-
assert.strictEqual(result[0].deepAnalysis, undefined, 'Malformed response should not attach result');
|
|
1109
|
-
});
|
|
1110
|
-
|
|
1111
|
-
it('static create() returns null when no provider available', () => {
|
|
1112
|
-
// Remove all API key env vars temporarily
|
|
1113
|
-
const saved = {};
|
|
1114
|
-
for (const key of ['ANTHROPIC_API_KEY', 'OPENAI_API_KEY', 'GOOGLE_API_KEY', 'GEMINI_API_KEY']) {
|
|
1115
|
-
saved[key] = process.env[key];
|
|
1116
|
-
delete process.env[key];
|
|
1117
|
-
}
|
|
1118
|
-
|
|
1119
|
-
try {
|
|
1120
|
-
const analyzer = DeepAnalyzer.create('/nonexistent/path');
|
|
1121
|
-
assert.strictEqual(analyzer, null, 'Should return null when no provider is available');
|
|
1122
|
-
} finally {
|
|
1123
|
-
// Restore
|
|
1124
|
-
for (const [key, val] of Object.entries(saved)) {
|
|
1125
|
-
if (val !== undefined) process.env[key] = val;
|
|
1126
|
-
}
|
|
1127
|
-
}
|
|
1128
|
-
});
|
|
1129
|
-
});
|
|
1130
|
-
|
|
1131
|
-
// =============================================================================
|
|
1132
|
-
// CROSS-AGENT AWARENESS & FRAMEWORK-AWARE shouldRun
|
|
1133
|
-
// =============================================================================
|
|
1134
|
-
|
|
1135
|
-
describe('Framework-aware shouldRun', async () => {
|
|
1136
|
-
const { MobileScanner } = await import('../agents/mobile-scanner.js');
|
|
1137
|
-
const { SupabaseRLSAgent } = await import('../agents/supabase-rls-agent.js');
|
|
1138
|
-
const { InjectionTester } = await import('../agents/injection-tester.js');
|
|
1139
|
-
|
|
1140
|
-
it('MobileScanner skips when no mobile framework detected', () => {
|
|
1141
|
-
const agent = new MobileScanner();
|
|
1142
|
-
const recon = { frameworks: ['express', 'nextjs'], databases: ['postgres'] };
|
|
1143
|
-
assert.strictEqual(agent.shouldRun(recon), false, 'Should skip for non-mobile projects');
|
|
1144
|
-
});
|
|
1145
|
-
|
|
1146
|
-
it('MobileScanner runs when react-native detected', () => {
|
|
1147
|
-
const agent = new MobileScanner();
|
|
1148
|
-
const recon = { frameworks: ['react-native'] };
|
|
1149
|
-
assert.strictEqual(agent.shouldRun(recon), true, 'Should run for React Native projects');
|
|
1150
|
-
});
|
|
1151
|
-
|
|
1152
|
-
it('SupabaseRLSAgent skips when no Supabase detected', () => {
|
|
1153
|
-
const agent = new SupabaseRLSAgent();
|
|
1154
|
-
const recon = { frameworks: ['express'], databases: ['postgres'], authPatterns: ['jwt'] };
|
|
1155
|
-
assert.strictEqual(agent.shouldRun(recon), false, 'Should skip for non-Supabase projects');
|
|
1156
|
-
});
|
|
1157
|
-
|
|
1158
|
-
it('SupabaseRLSAgent runs when Supabase detected', () => {
|
|
1159
|
-
const agent = new SupabaseRLSAgent();
|
|
1160
|
-
const recon = { databases: ['supabase'], authPatterns: ['supabase-auth'] };
|
|
1161
|
-
assert.strictEqual(agent.shouldRun(recon), true, 'Should run for Supabase projects');
|
|
1162
|
-
});
|
|
1163
|
-
|
|
1164
|
-
it('InjectionTester always runs (default shouldRun)', () => {
|
|
1165
|
-
const agent = new InjectionTester();
|
|
1166
|
-
const recon = { frameworks: ['express'] };
|
|
1167
|
-
assert.strictEqual(agent.shouldRun(recon), true, 'Universal agents always run');
|
|
1168
|
-
});
|
|
1169
|
-
});
|
|
1170
|
-
|
|
1171
|
-
// =============================================================================
|
|
1172
|
-
// SECRETS VERIFIER
|
|
1173
|
-
// =============================================================================
|
|
1174
|
-
|
|
1175
|
-
describe('SecretsVerifier', async () => {
|
|
1176
|
-
const { SecretsVerifier } = await import('../utils/secrets-verifier.js');
|
|
1177
|
-
|
|
1178
|
-
it('skips non-secret findings', async () => {
|
|
1179
|
-
const verifier = new SecretsVerifier();
|
|
1180
|
-
const findings = [
|
|
1181
|
-
{ file: '/a.js', line: 1, severity: 'high', category: 'injection', rule: 'SQL_INJECTION', matched: 'SELECT *' },
|
|
1182
|
-
];
|
|
1183
|
-
const results = await verifier.verify(findings);
|
|
1184
|
-
assert.strictEqual(results.length, 0, 'Should skip non-secret findings');
|
|
1185
|
-
});
|
|
1186
|
-
|
|
1187
|
-
it('extracts secret value from quoted match', () => {
|
|
1188
|
-
const verifier = new SecretsVerifier();
|
|
1189
|
-
const secret = verifier._extractSecret('API_KEY="sk_live_abc123def456"');
|
|
1190
|
-
assert.strictEqual(secret, 'sk_live_abc123def456');
|
|
1191
|
-
});
|
|
1192
|
-
|
|
1193
|
-
it('extracts secret value from assignment', () => {
|
|
1194
|
-
const verifier = new SecretsVerifier();
|
|
1195
|
-
const secret = verifier._extractSecret('token=ghp_abcdefghijklmnop');
|
|
1196
|
-
assert.strictEqual(secret, 'ghp_abcdefghijklmnop');
|
|
1197
|
-
});
|
|
1198
|
-
|
|
1199
|
-
it('returns null for short/empty matches', () => {
|
|
1200
|
-
const verifier = new SecretsVerifier();
|
|
1201
|
-
assert.strictEqual(verifier._extractSecret(''), null);
|
|
1202
|
-
assert.strictEqual(verifier._extractSecret(null), null);
|
|
1203
|
-
});
|
|
1204
|
-
|
|
1205
|
-
it('finds probe for known rule names', () => {
|
|
1206
|
-
const verifier = new SecretsVerifier();
|
|
1207
|
-
assert.ok(verifier._findProbe('GITHUB_TOKEN'), 'Should find GitHub probe');
|
|
1208
|
-
assert.ok(verifier._findProbe('OPENAI_API_KEY'), 'Should find OpenAI probe');
|
|
1209
|
-
assert.ok(verifier._findProbe('STRIPE_LIVE_KEY'), 'Should find Stripe probe');
|
|
1210
|
-
assert.strictEqual(verifier._findProbe('UNKNOWN_PATTERN_XYZ'), null, 'Should return null for unknown');
|
|
1211
|
-
});
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
// =============================================================================
|
|
1215
|
-
// SBOM GENERATOR (CRA Enhancement)
|
|
1216
|
-
// =============================================================================
|
|
1217
|
-
|
|
1218
|
-
describe('SBOMGenerator CRA', async () => {
|
|
1219
|
-
const { SBOMGenerator } = await import('../agents/sbom-generator.js');
|
|
1220
|
-
|
|
1221
|
-
it('generates SBOM with CRA-required fields', () => {
|
|
1222
|
-
const sbom = new SBOMGenerator();
|
|
1223
|
-
const bom = sbom.generate(process.cwd());
|
|
1224
|
-
|
|
1225
|
-
// CRA fields
|
|
1226
|
-
assert.ok(bom.metadata.supplier, 'Should have supplier field');
|
|
1227
|
-
assert.ok(bom.metadata.lifecycles, 'Should have lifecycles field');
|
|
1228
|
-
assert.strictEqual(bom.metadata.lifecycles[0].phase, 'build');
|
|
1229
|
-
assert.ok(Array.isArray(bom.vulnerabilities), 'Should have vulnerabilities array');
|
|
1230
|
-
});
|
|
1231
|
-
|
|
1232
|
-
it('attachVulnerabilities adds CVEs to SBOM', () => {
|
|
1233
|
-
const sbom = new SBOMGenerator();
|
|
1234
|
-
const bom = sbom.generate(process.cwd());
|
|
1235
|
-
|
|
1236
|
-
const vulns = [
|
|
1237
|
-
{ id: 'CVE-2024-1234', package: 'lodash@4.17.20', severity: 'high', description: 'Prototype pollution' },
|
|
1238
|
-
];
|
|
1239
|
-
sbom.attachVulnerabilities(bom, vulns);
|
|
1240
|
-
|
|
1241
|
-
assert.strictEqual(bom.vulnerabilities.length, 1);
|
|
1242
|
-
assert.strictEqual(bom.vulnerabilities[0].id, 'CVE-2024-1234');
|
|
1243
|
-
assert.strictEqual(bom.vulnerabilities[0].ratings[0].severity, 'high');
|
|
1244
|
-
});
|
|
1245
|
-
|
|
1246
|
-
it('detects licenses from node_modules', () => {
|
|
1247
|
-
const sbom = new SBOMGenerator();
|
|
1248
|
-
const licenses = sbom._detectLicenses(process.cwd());
|
|
1249
|
-
// Our project uses chalk, commander, etc. — should find some licenses
|
|
1250
|
-
if (Object.keys(licenses).length > 0) {
|
|
1251
|
-
const firstLicense = Object.values(licenses)[0];
|
|
1252
|
-
assert.ok(typeof firstLicense === 'string', 'License should be a string');
|
|
1253
|
-
}
|
|
1254
|
-
});
|
|
1255
|
-
});
|
|
1256
|
-
|
|
1257
|
-
// =============================================================================
|
|
1258
|
-
// ORCHESTRATOR — CROSS-AGENT SHARED FINDINGS
|
|
1259
|
-
// =============================================================================
|
|
1260
|
-
|
|
1261
|
-
describe('Orchestrator cross-agent awareness', async () => {
|
|
1262
|
-
const { Orchestrator } = await import('../agents/orchestrator.js');
|
|
1263
|
-
|
|
1264
|
-
it('passes sharedFindings in context to agents', async () => {
|
|
1265
|
-
const orchestrator = new Orchestrator();
|
|
1266
|
-
let receivedSharedFindings = null;
|
|
1267
|
-
|
|
1268
|
-
// Mock agent that captures context
|
|
1269
|
-
const mockAgent = {
|
|
1270
|
-
name: 'MockAgent',
|
|
1271
|
-
category: 'test',
|
|
1272
|
-
shouldRun: () => true,
|
|
1273
|
-
async analyze(context) {
|
|
1274
|
-
receivedSharedFindings = context.sharedFindings;
|
|
1275
|
-
return [];
|
|
1276
|
-
},
|
|
1277
|
-
};
|
|
1278
|
-
|
|
1279
|
-
orchestrator.register(mockAgent);
|
|
1280
|
-
await orchestrator.runAll(process.cwd(), { quiet: true, skipVerifier: true });
|
|
1281
|
-
|
|
1282
|
-
assert.ok(Array.isArray(receivedSharedFindings), 'sharedFindings should be an array in context');
|
|
1283
|
-
});
|
|
1284
|
-
|
|
1285
|
-
it('skips agents where shouldRun returns false', async () => {
|
|
1286
|
-
const orchestrator = new Orchestrator();
|
|
1287
|
-
let ran = false;
|
|
1288
|
-
|
|
1289
|
-
const skipAgent = {
|
|
1290
|
-
name: 'SkipMe',
|
|
1291
|
-
category: 'test',
|
|
1292
|
-
shouldRun: () => false,
|
|
1293
|
-
async analyze() { ran = true; return []; },
|
|
1294
|
-
};
|
|
1295
|
-
|
|
1296
|
-
orchestrator.register(skipAgent);
|
|
1297
|
-
await orchestrator.runAll(process.cwd(), { quiet: true, skipVerifier: true });
|
|
1298
|
-
|
|
1299
|
-
assert.strictEqual(ran, false, 'Agent with shouldRun=false should not execute');
|
|
1300
|
-
});
|
|
1301
|
-
});
|