ship-safe 6.1.1 → 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 -641
- 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 +568 -568
- 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 -980
- 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 -569
- package/cli/commands/score.js +449 -449
- 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 +230 -230
- package/cli/utils/patterns.js +1121 -1121
- package/cli/utils/pdf-generator.js +94 -94
- package/package.json +69 -69
- package/configs/supabase/rls-templates.sql +0 -242
|
@@ -1,507 +1,650 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SupplyChainAudit Agent
|
|
3
|
-
* =======================
|
|
4
|
-
*
|
|
5
|
-
* Comprehensive supply chain security analysis.
|
|
6
|
-
* Goes beyond npm audit: checks for dependency confusion,
|
|
7
|
-
* typosquatting, malicious install scripts, lockfile integrity,
|
|
8
|
-
* EPSS scoring, and KEV flagging.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import fs from 'fs';
|
|
12
|
-
import path from 'path';
|
|
13
|
-
import { BaseAgent, createFinding } from './base-agent.js';
|
|
14
|
-
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
//
|
|
373
|
-
const
|
|
374
|
-
if (
|
|
375
|
-
findings.push(createFinding({
|
|
376
|
-
file,
|
|
377
|
-
line: 1,
|
|
378
|
-
severity: '
|
|
379
|
-
category: 'supply-chain',
|
|
380
|
-
rule: '
|
|
381
|
-
title: '
|
|
382
|
-
description: `File contains ${
|
|
383
|
-
matched: `${
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
1
|
+
/**
|
|
2
|
+
* SupplyChainAudit Agent
|
|
3
|
+
* =======================
|
|
4
|
+
*
|
|
5
|
+
* Comprehensive supply chain security analysis.
|
|
6
|
+
* Goes beyond npm audit: checks for dependency confusion,
|
|
7
|
+
* typosquatting, malicious install scripts, lockfile integrity,
|
|
8
|
+
* EPSS scoring, and KEV flagging.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { BaseAgent, createFinding } from './base-agent.js';
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// KNOWN-COMPROMISED PACKAGE IOC LIST
|
|
17
|
+
// Source: TeamPCP/CanisterWorm campaign (March 2026) + prior incidents
|
|
18
|
+
// Format: { name, badVersions: [exact], note }
|
|
19
|
+
// =============================================================================
|
|
20
|
+
const COMPROMISED_PACKAGES = [
|
|
21
|
+
{
|
|
22
|
+
name: 'litellm',
|
|
23
|
+
badVersions: ['1.82.7', '1.82.8'],
|
|
24
|
+
note: 'TeamPCP supply chain attack (Mar 24 2026). Multi-stage credential stealer targeting SSH keys, cloud tokens, and AI API keys.',
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'axios',
|
|
28
|
+
badVersions: ['1.8.2'],
|
|
29
|
+
note: 'TeamPCP/CanisterWorm campaign (Mar 31 2026). Malicious publish delivered a Remote Access Trojan with persistence.',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'telnyx',
|
|
33
|
+
badVersions: ['2.1.5'],
|
|
34
|
+
note: 'TeamPCP campaign (Mar 27 2026). Compromised PyPI release exfiltrated credentials.',
|
|
35
|
+
},
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Packages that have no reason to depend on ICP/Internet Computer blockchain
|
|
39
|
+
// but CanisterWorm injected @dfinity/agent as its decentralized C2 mechanism
|
|
40
|
+
const ICP_BLOCKCHAIN_PACKAGES = [
|
|
41
|
+
'@dfinity/agent',
|
|
42
|
+
'@dfinity/candid',
|
|
43
|
+
'@dfinity/principal',
|
|
44
|
+
'ic-agent',
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
// Common packages that are often typosquatted
|
|
48
|
+
const POPULAR_PACKAGES = [
|
|
49
|
+
'lodash', 'express', 'react', 'axios', 'moment', 'request',
|
|
50
|
+
'chalk', 'commander', 'debug', 'uuid', 'dotenv', 'cors',
|
|
51
|
+
'body-parser', 'jsonwebtoken', 'bcrypt', 'mongoose', 'sequelize',
|
|
52
|
+
'webpack', 'babel', 'eslint', 'prettier', 'typescript',
|
|
53
|
+
'next', 'nuxt', 'svelte', 'vue', 'angular',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// Well-known packages that happen to be close to other popular names
|
|
57
|
+
// (not typosquats — verified legitimate packages)
|
|
58
|
+
const KNOWN_SAFE = new Set([
|
|
59
|
+
'ora', 'got', 'ink', 'yup', 'joi', 'ava', 'tap', 'npm', 'nwb',
|
|
60
|
+
'pug', 'koa', 'hap', 'ejs', 'csv', 'ws', 'pg', 'ms',
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// Known malicious package name patterns
|
|
64
|
+
const SUSPICIOUS_NAME_PATTERNS = [
|
|
65
|
+
/^@[^/]+\/[^/]+-[0-9]+$/, // @scope/package-123 (random suffix)
|
|
66
|
+
/^[a-z]+-[a-z]+-[a-z]+-[a-z]+$/, // overly-generic multi-word names
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export class SupplyChainAudit extends BaseAgent {
|
|
70
|
+
constructor() {
|
|
71
|
+
super('SupplyChainAudit', 'Comprehensive supply chain security audit', 'supply-chain');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async analyze(context) {
|
|
75
|
+
const { rootPath } = context;
|
|
76
|
+
const findings = [];
|
|
77
|
+
|
|
78
|
+
// ── 1. Check package.json ─────────────────────────────────────────────────
|
|
79
|
+
const pkgPath = path.join(rootPath, 'package.json');
|
|
80
|
+
if (fs.existsSync(pkgPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
83
|
+
const allDeps = {
|
|
84
|
+
...(pkg.dependencies || {}),
|
|
85
|
+
...(pkg.devDependencies || {}),
|
|
86
|
+
...(pkg.optionalDependencies || {}),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// ── Typosquatting detection ───────────────────────────────────────────
|
|
90
|
+
for (const depName of Object.keys(allDeps)) {
|
|
91
|
+
if (KNOWN_SAFE.has(depName)) continue;
|
|
92
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
93
|
+
const distance = this.levenshtein(depName, popular);
|
|
94
|
+
if (distance > 0 && distance <= 2 && depName !== popular) {
|
|
95
|
+
findings.push(createFinding({
|
|
96
|
+
file: pkgPath,
|
|
97
|
+
line: 0,
|
|
98
|
+
severity: 'high',
|
|
99
|
+
category: 'supply-chain',
|
|
100
|
+
rule: 'TYPOSQUAT_SUSPECT',
|
|
101
|
+
title: `Possible Typosquat: "${depName}" (similar to "${popular}")`,
|
|
102
|
+
description: `Package "${depName}" is ${distance} character(s) away from popular package "${popular}". This could be a typosquatting attempt.`,
|
|
103
|
+
matched: depName,
|
|
104
|
+
fix: `Verify this is the intended package. Did you mean "${popular}"?`,
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Deprecated/suspicious version pins ───────────────────────────────
|
|
111
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
112
|
+
if (typeof version === 'string' && version.startsWith('git+')) {
|
|
113
|
+
findings.push(createFinding({
|
|
114
|
+
file: pkgPath,
|
|
115
|
+
line: 0,
|
|
116
|
+
severity: 'high',
|
|
117
|
+
category: 'supply-chain',
|
|
118
|
+
rule: 'GIT_DEPENDENCY',
|
|
119
|
+
title: `Git Dependency: ${name}`,
|
|
120
|
+
description: `"${name}" is installed from a git URL. Git dependencies bypass registry integrity checks.`,
|
|
121
|
+
matched: `${name}: ${version}`,
|
|
122
|
+
fix: 'Pin to a specific commit hash or use a published npm package version',
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (typeof version === 'string' && version.startsWith('http')) {
|
|
127
|
+
findings.push(createFinding({
|
|
128
|
+
file: pkgPath,
|
|
129
|
+
line: 0,
|
|
130
|
+
severity: 'critical',
|
|
131
|
+
category: 'supply-chain',
|
|
132
|
+
rule: 'URL_DEPENDENCY',
|
|
133
|
+
title: `URL Dependency: ${name}`,
|
|
134
|
+
description: `"${name}" is installed from a URL. This bypasses npm registry and integrity checks.`,
|
|
135
|
+
matched: `${name}: ${version}`,
|
|
136
|
+
fix: 'Publish the package to npm or use a private registry',
|
|
137
|
+
}));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof version === 'string' && version === '*') {
|
|
141
|
+
findings.push(createFinding({
|
|
142
|
+
file: pkgPath,
|
|
143
|
+
line: 0,
|
|
144
|
+
severity: 'high',
|
|
145
|
+
category: 'supply-chain',
|
|
146
|
+
rule: 'WILDCARD_VERSION',
|
|
147
|
+
title: `Wildcard Version: ${name}`,
|
|
148
|
+
description: `"${name}" uses "*" version which accepts any version including malicious updates.`,
|
|
149
|
+
matched: `${name}: *`,
|
|
150
|
+
fix: 'Pin to a specific version or use a caret range: "^x.y.z"',
|
|
151
|
+
}));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Install scripts ──────────────────────────────────────────────────
|
|
156
|
+
if (pkg.scripts) {
|
|
157
|
+
const dangerousScripts = ['preinstall', 'postinstall', 'preuninstall', 'postuninstall'];
|
|
158
|
+
for (const script of dangerousScripts) {
|
|
159
|
+
if (pkg.scripts[script]) {
|
|
160
|
+
const cmd = pkg.scripts[script];
|
|
161
|
+
const suspicious = /curl|wget|bash|sh\s|powershell|eval|base64|nc\s|ncat/i.test(cmd);
|
|
162
|
+
if (suspicious) {
|
|
163
|
+
findings.push(createFinding({
|
|
164
|
+
file: pkgPath,
|
|
165
|
+
line: 0,
|
|
166
|
+
severity: 'critical',
|
|
167
|
+
category: 'supply-chain',
|
|
168
|
+
rule: 'SUSPICIOUS_INSTALL_SCRIPT',
|
|
169
|
+
title: `Suspicious ${script} Script`,
|
|
170
|
+
description: `The ${script} script contains potentially dangerous commands: ${cmd.slice(0, 100)}`,
|
|
171
|
+
matched: cmd,
|
|
172
|
+
fix: 'Review and remove suspicious install scripts',
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
} catch { /* skip parse errors */ }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── 2. Dependency confusion detection ─────────────────────────────────────
|
|
183
|
+
if (fs.existsSync(pkgPath)) {
|
|
184
|
+
try {
|
|
185
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
186
|
+
const allDeps = {
|
|
187
|
+
...(pkg.dependencies || {}),
|
|
188
|
+
...(pkg.devDependencies || {}),
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Check for scoped packages without registry pinning
|
|
192
|
+
const scopedPkgs = Object.keys(allDeps).filter(n => n.startsWith('@'));
|
|
193
|
+
if (scopedPkgs.length > 0) {
|
|
194
|
+
const npmrcPath = path.join(rootPath, '.npmrc');
|
|
195
|
+
const yarnrcPath = path.join(rootPath, '.yarnrc');
|
|
196
|
+
const yarnrcYmlPath = path.join(rootPath, '.yarnrc.yml');
|
|
197
|
+
const hasRegistryConfig = [npmrcPath, yarnrcPath, yarnrcYmlPath].some(p => {
|
|
198
|
+
if (!fs.existsSync(p)) return false;
|
|
199
|
+
const content = this.readFile(p) || '';
|
|
200
|
+
// Check if any scope is pinned to a registry
|
|
201
|
+
return /@[^:]+:registry/i.test(content) || /npmRegistryServer/i.test(content);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Extract unique scopes
|
|
205
|
+
const scopes = [...new Set(scopedPkgs.map(n => n.split('/')[0]))];
|
|
206
|
+
// Check if this looks like an internal scope (not well-known public ones)
|
|
207
|
+
const publicScopes = new Set([
|
|
208
|
+
'@types', '@babel', '@eslint', '@jest', '@testing-library',
|
|
209
|
+
'@react-native', '@angular', '@vue', '@nuxt', '@next',
|
|
210
|
+
'@emotion', '@mui', '@radix-ui', '@tanstack', '@trpc',
|
|
211
|
+
'@prisma', '@supabase', '@aws-sdk', '@azure', '@google-cloud',
|
|
212
|
+
'@octokit', '@sentry', '@stripe', '@anthropic-ai', '@openai',
|
|
213
|
+
]);
|
|
214
|
+
const internalScopes = scopes.filter(s => !publicScopes.has(s));
|
|
215
|
+
|
|
216
|
+
if (internalScopes.length > 0 && !hasRegistryConfig) {
|
|
217
|
+
findings.push(createFinding({
|
|
218
|
+
file: pkgPath,
|
|
219
|
+
line: 0,
|
|
220
|
+
severity: 'high',
|
|
221
|
+
category: 'supply-chain',
|
|
222
|
+
rule: 'DEPCONF_NO_SCOPE_REGISTRY',
|
|
223
|
+
title: `Scoped Packages Without Registry Pin: ${internalScopes.join(', ')}`,
|
|
224
|
+
description: `Scoped packages (${internalScopes.join(', ')}) found without a .npmrc pinning the scope to a private registry. An attacker could claim the scope on the public npm registry.`,
|
|
225
|
+
matched: internalScopes.join(', '),
|
|
226
|
+
confidence: 'medium',
|
|
227
|
+
fix: 'Add to .npmrc: @yourscope:registry=https://your-private-registry.com/',
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Check for suspicious install scripts in dependencies
|
|
233
|
+
const nodeModulesPath = path.join(rootPath, 'node_modules');
|
|
234
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
235
|
+
for (const depName of Object.keys(allDeps).slice(0, 50)) {
|
|
236
|
+
const depPkgPath = path.join(nodeModulesPath, depName, 'package.json');
|
|
237
|
+
if (!fs.existsSync(depPkgPath)) continue;
|
|
238
|
+
try {
|
|
239
|
+
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
|
|
240
|
+
const scripts = depPkg.scripts || {};
|
|
241
|
+
for (const hook of ['preinstall', 'install', 'postinstall']) {
|
|
242
|
+
const cmd = scripts[hook];
|
|
243
|
+
if (!cmd) continue;
|
|
244
|
+
if (/(?:curl|wget|powershell|base64\s|eval\s|nc\s|ncat|\.sh\b)/i.test(cmd)) {
|
|
245
|
+
findings.push(createFinding({
|
|
246
|
+
file: depPkgPath,
|
|
247
|
+
line: 0,
|
|
248
|
+
severity: 'critical',
|
|
249
|
+
category: 'supply-chain',
|
|
250
|
+
rule: 'DEPCONF_SUSPICIOUS_INSTALL_SCRIPT',
|
|
251
|
+
title: `Suspicious ${hook} in ${depName}`,
|
|
252
|
+
description: `Dependency "${depName}" has a suspicious ${hook} script: ${cmd.slice(0, 120)}`,
|
|
253
|
+
matched: cmd.slice(0, 200),
|
|
254
|
+
fix: 'Review the script. If untrusted, remove the dependency or use npm with --ignore-scripts',
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
} catch { /* skip */ }
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} catch { /* skip */ }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ── 3. Check lockfile integrity ── ───────────────────────────────────────────
|
|
265
|
+
const lockFiles = [
|
|
266
|
+
{ file: 'package-lock.json', manager: 'npm' },
|
|
267
|
+
{ file: 'yarn.lock', manager: 'yarn' },
|
|
268
|
+
{ file: 'pnpm-lock.yaml', manager: 'pnpm' },
|
|
269
|
+
{ file: 'bun.lockb', manager: 'bun' },
|
|
270
|
+
];
|
|
271
|
+
|
|
272
|
+
const hasPackageJson = fs.existsSync(pkgPath);
|
|
273
|
+
let hasLockfile = false;
|
|
274
|
+
|
|
275
|
+
for (const { file, manager } of lockFiles) {
|
|
276
|
+
if (fs.existsSync(path.join(rootPath, file))) {
|
|
277
|
+
hasLockfile = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (hasPackageJson && !hasLockfile) {
|
|
282
|
+
findings.push(createFinding({
|
|
283
|
+
file: pkgPath,
|
|
284
|
+
line: 0,
|
|
285
|
+
severity: 'high',
|
|
286
|
+
category: 'supply-chain',
|
|
287
|
+
rule: 'MISSING_LOCKFILE',
|
|
288
|
+
title: 'No Lock File Found',
|
|
289
|
+
description: 'No package-lock.json, yarn.lock, or pnpm-lock.yaml found. Without a lockfile, installs are non-deterministic and vulnerable to dependency confusion.',
|
|
290
|
+
matched: 'package.json without lockfile',
|
|
291
|
+
fix: 'Run npm install, yarn install, or pnpm install to generate a lockfile, then commit it',
|
|
292
|
+
}));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── 4. Check .npmrc for security settings ─────────────────────────────────
|
|
296
|
+
const npmrcPath = path.join(rootPath, '.npmrc');
|
|
297
|
+
if (fs.existsSync(npmrcPath)) {
|
|
298
|
+
const content = this.readFile(npmrcPath) || '';
|
|
299
|
+
if (content.includes('ignore-scripts=true')) {
|
|
300
|
+
// Good — scripts are disabled
|
|
301
|
+
}
|
|
302
|
+
if (content.includes('registry=') && !content.includes('registry=https://registry.npmjs.org')) {
|
|
303
|
+
findings.push(createFinding({
|
|
304
|
+
file: npmrcPath,
|
|
305
|
+
line: 0,
|
|
306
|
+
severity: 'medium',
|
|
307
|
+
category: 'supply-chain',
|
|
308
|
+
rule: 'CUSTOM_REGISTRY',
|
|
309
|
+
title: 'Custom NPM Registry Configured',
|
|
310
|
+
description: 'A custom npm registry is configured. Verify it is trusted and uses HTTPS.',
|
|
311
|
+
matched: content.match(/registry=.*/)?.[0] || '',
|
|
312
|
+
confidence: 'medium',
|
|
313
|
+
fix: 'Verify the registry URL is trusted and uses HTTPS',
|
|
314
|
+
}));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── 5. Package behavioral signals (Socket-style) ─────────────────────────
|
|
319
|
+
if (fs.existsSync(pkgPath)) {
|
|
320
|
+
try {
|
|
321
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
322
|
+
const allDeps = {
|
|
323
|
+
...(pkg.dependencies || {}),
|
|
324
|
+
...(pkg.devDependencies || {}),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Scan node_modules for behavioral red flags
|
|
328
|
+
const nodeModulesPath = path.join(rootPath, 'node_modules');
|
|
329
|
+
if (fs.existsSync(nodeModulesPath)) {
|
|
330
|
+
for (const depName of Object.keys(allDeps).slice(0, 50)) {
|
|
331
|
+
const depDir = path.join(nodeModulesPath, depName);
|
|
332
|
+
const depPkgPath = path.join(depDir, 'package.json');
|
|
333
|
+
if (!fs.existsSync(depPkgPath)) continue;
|
|
334
|
+
try {
|
|
335
|
+
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
|
|
336
|
+
|
|
337
|
+
// Check for postinstall scripts with network/eval calls
|
|
338
|
+
const scripts = depPkg.scripts || {};
|
|
339
|
+
for (const hook of ['preinstall', 'install', 'postinstall']) {
|
|
340
|
+
const cmd = scripts[hook];
|
|
341
|
+
if (!cmd) continue;
|
|
342
|
+
if (/node\s+-e|node\s+--eval/.test(cmd)) {
|
|
343
|
+
findings.push(createFinding({
|
|
344
|
+
file: depPkgPath,
|
|
345
|
+
line: 0,
|
|
346
|
+
severity: 'high',
|
|
347
|
+
category: 'supply-chain',
|
|
348
|
+
rule: 'BEHAVIORAL_INLINE_EVAL',
|
|
349
|
+
title: `Inline Code Execution in ${hook}: ${depName}`,
|
|
350
|
+
description: `Dependency "${depName}" runs inline Node.js code during ${hook}. This is a common pattern in malicious packages.`,
|
|
351
|
+
matched: cmd.slice(0, 200),
|
|
352
|
+
fix: 'Review the inline code. Consider using --ignore-scripts or removing the dependency.',
|
|
353
|
+
}));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} catch { /* skip */ }
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Detect obfuscated code patterns in dependencies
|
|
361
|
+
const codeFiles = (context.files || []).filter(f =>
|
|
362
|
+
f.includes('node_modules') &&
|
|
363
|
+
!f.includes('node_modules/.cache') &&
|
|
364
|
+
path.extname(f).toLowerCase() === '.js' &&
|
|
365
|
+
!path.basename(f).endsWith('.min.js')
|
|
366
|
+
).slice(0, 30); // Sample up to 30 files
|
|
367
|
+
|
|
368
|
+
for (const file of codeFiles) {
|
|
369
|
+
const content = this.readFile(file);
|
|
370
|
+
if (!content || content.length < 100) continue;
|
|
371
|
+
|
|
372
|
+
// Excessive hex encoding
|
|
373
|
+
const hexMatches = (content.match(/\\x[0-9a-fA-F]{2}/g) || []).length;
|
|
374
|
+
if (hexMatches > 20) {
|
|
375
|
+
findings.push(createFinding({
|
|
376
|
+
file,
|
|
377
|
+
line: 1,
|
|
378
|
+
severity: 'high',
|
|
379
|
+
category: 'supply-chain',
|
|
380
|
+
rule: 'BEHAVIORAL_HEX_OBFUSCATION',
|
|
381
|
+
title: 'Obfuscated Code: Excessive Hex Encoding',
|
|
382
|
+
description: `File contains ${hexMatches} hex-encoded sequences. Common in malicious packages trying to hide payload.`,
|
|
383
|
+
matched: `${hexMatches} hex sequences detected`,
|
|
384
|
+
fix: 'Inspect the deobfuscated code. Consider removing this dependency.',
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Excessive String.fromCharCode
|
|
389
|
+
const charCodeMatches = (content.match(/String\.fromCharCode/g) || []).length;
|
|
390
|
+
if (charCodeMatches > 5) {
|
|
391
|
+
findings.push(createFinding({
|
|
392
|
+
file,
|
|
393
|
+
line: 1,
|
|
394
|
+
severity: 'high',
|
|
395
|
+
category: 'supply-chain',
|
|
396
|
+
rule: 'BEHAVIORAL_CHARCODE_OBFUSCATION',
|
|
397
|
+
title: 'Obfuscated Code: Excessive String.fromCharCode',
|
|
398
|
+
description: `File contains ${charCodeMatches} String.fromCharCode calls. Common obfuscation technique in malicious packages.`,
|
|
399
|
+
matched: `${charCodeMatches} String.fromCharCode calls`,
|
|
400
|
+
fix: 'Inspect the deobfuscated code. Consider removing this dependency.',
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Base64 decode chains
|
|
405
|
+
const base64Matches = (content.match(/Buffer\.from\s*\([^,]+,\s*['"]base64['"]\)/g) || []).length;
|
|
406
|
+
if (base64Matches > 3) {
|
|
407
|
+
findings.push(createFinding({
|
|
408
|
+
file,
|
|
409
|
+
line: 1,
|
|
410
|
+
severity: 'medium',
|
|
411
|
+
category: 'supply-chain',
|
|
412
|
+
rule: 'BEHAVIORAL_BASE64_DECODE',
|
|
413
|
+
title: 'Suspicious: Multiple Base64 Decode Operations',
|
|
414
|
+
description: `File contains ${base64Matches} base64 decode operations. May indicate hidden payload.`,
|
|
415
|
+
matched: `${base64Matches} base64 decode operations`,
|
|
416
|
+
confidence: 'medium',
|
|
417
|
+
fix: 'Review what data is being decoded. Legitimate use is possible but warrants inspection.',
|
|
418
|
+
}));
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Detect unused dependencies (in package.json but never imported)
|
|
423
|
+
const projectFiles = (context.files || []).filter(f =>
|
|
424
|
+
!f.includes('node_modules') &&
|
|
425
|
+
['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(path.extname(f).toLowerCase())
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
if (projectFiles.length > 0 && projectFiles.length < 500) {
|
|
429
|
+
const allImports = new Set();
|
|
430
|
+
for (const file of projectFiles) {
|
|
431
|
+
const content = this.readFile(file);
|
|
432
|
+
if (!content) continue;
|
|
433
|
+
// Capture import/require module names
|
|
434
|
+
const importMatches = content.matchAll(/(?:from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/g);
|
|
435
|
+
for (const m of importMatches) {
|
|
436
|
+
const mod = (m[1] || m[2] || '').split('/')[0]; // Get package name (not subpath)
|
|
437
|
+
if (mod && !mod.startsWith('.')) allImports.add(mod);
|
|
438
|
+
// Handle scoped packages
|
|
439
|
+
const fullMod = m[1] || m[2] || '';
|
|
440
|
+
if (fullMod.startsWith('@')) {
|
|
441
|
+
const scopedPkg = fullMod.split('/').slice(0, 2).join('/');
|
|
442
|
+
allImports.add(scopedPkg);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const prodDeps = Object.keys(pkg.dependencies || {});
|
|
448
|
+
for (const dep of prodDeps) {
|
|
449
|
+
if (!allImports.has(dep) && !dep.startsWith('@types/')) {
|
|
450
|
+
findings.push(createFinding({
|
|
451
|
+
file: pkgPath,
|
|
452
|
+
line: 0,
|
|
453
|
+
severity: 'low',
|
|
454
|
+
category: 'supply-chain',
|
|
455
|
+
rule: 'UNUSED_DEPENDENCY',
|
|
456
|
+
title: `Unused Dependency: ${dep}`,
|
|
457
|
+
description: `"${dep}" is in dependencies but never imported in project code. Unused dependencies increase attack surface.`,
|
|
458
|
+
matched: dep,
|
|
459
|
+
confidence: 'low',
|
|
460
|
+
fix: `Remove if unused: npm uninstall ${dep}`,
|
|
461
|
+
}));
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
} catch { /* skip */ }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// ── 6. npm token scope in .npmrc ──────────────────────────────────────────
|
|
470
|
+
if (fs.existsSync(npmrcPath)) {
|
|
471
|
+
const content = this.readFile(npmrcPath) || '';
|
|
472
|
+
// Detect auth tokens stored in .npmrc — a prerequisite for worm spread
|
|
473
|
+
const tokenLines = content.split('\n').filter(l => /_authToken\s*=/.test(l));
|
|
474
|
+
for (const line of tokenLines) {
|
|
475
|
+
const isScopedToPackage = /\/\/registry\.npmjs\.org\/.+:_authToken/.test(line);
|
|
476
|
+
// Flag tokens that appear to be publish-level (not scoped to a single package)
|
|
477
|
+
findings.push(createFinding({
|
|
478
|
+
file: npmrcPath,
|
|
479
|
+
line: 0,
|
|
480
|
+
severity: 'high',
|
|
481
|
+
category: 'supply-chain',
|
|
482
|
+
rule: 'NPMRC_AUTH_TOKEN_EXPOSED',
|
|
483
|
+
title: 'npm Auth Token in .npmrc',
|
|
484
|
+
description: `An npm auth token is stored in .npmrc. ${isScopedToPackage ? 'Verify it is scoped to only the packages it needs to publish.' : 'If this token has publish rights, a compromised install script or dependency can steal it and spread a worm (CanisterWorm attack pattern).'} Commit this file only if absolutely necessary; prefer CI secret injection.`,
|
|
485
|
+
matched: line.replace(/=.+/, '=***'),
|
|
486
|
+
fix: 'Use npm token create --cidr-whitelist or scope tokens per-package. Never commit .npmrc with auth tokens.',
|
|
487
|
+
}));
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ── 7. Check Python requirements ──────────────────────────────────────────
|
|
492
|
+
const reqPath = path.join(rootPath, 'requirements.txt');
|
|
493
|
+
if (fs.existsSync(reqPath)) {
|
|
494
|
+
const content = this.readFile(reqPath) || '';
|
|
495
|
+
const lines = content.split('\n');
|
|
496
|
+
for (let i = 0; i < lines.length; i++) {
|
|
497
|
+
const line = lines[i].trim();
|
|
498
|
+
if (!line || line.startsWith('#')) continue;
|
|
499
|
+
|
|
500
|
+
// Unpinned versions
|
|
501
|
+
if (!line.includes('==') && !line.includes('>=') && !line.includes('~=') && !line.includes('@')) {
|
|
502
|
+
findings.push(createFinding({
|
|
503
|
+
file: reqPath,
|
|
504
|
+
line: i + 1,
|
|
505
|
+
severity: 'medium',
|
|
506
|
+
category: 'supply-chain',
|
|
507
|
+
rule: 'UNPINNED_PYTHON_DEP',
|
|
508
|
+
title: `Unpinned Python Dependency: ${line}`,
|
|
509
|
+
description: 'Python dependency without version pin. Pin to a specific version for reproducible builds.',
|
|
510
|
+
matched: line,
|
|
511
|
+
fix: `Pin version: ${line}==x.y.z`,
|
|
512
|
+
}));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Git/URL dependencies
|
|
516
|
+
if (line.includes('git+') || line.startsWith('http')) {
|
|
517
|
+
findings.push(createFinding({
|
|
518
|
+
file: reqPath,
|
|
519
|
+
line: i + 1,
|
|
520
|
+
severity: 'high',
|
|
521
|
+
category: 'supply-chain',
|
|
522
|
+
rule: 'GIT_PYTHON_DEP',
|
|
523
|
+
title: `Git/URL Python Dependency: ${line.slice(0, 60)}`,
|
|
524
|
+
description: 'Installing from git/URL bypasses PyPI integrity checks.',
|
|
525
|
+
matched: line,
|
|
526
|
+
fix: 'Publish to PyPI or pin to a specific commit hash',
|
|
527
|
+
}));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── 8. Compromised package IOC matching (npm + PyPI) ──────────────────────
|
|
533
|
+
const iocSources = [];
|
|
534
|
+
|
|
535
|
+
// npm packages
|
|
536
|
+
if (fs.existsSync(pkgPath)) {
|
|
537
|
+
try {
|
|
538
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
539
|
+
const allDeps = {
|
|
540
|
+
...(pkg.dependencies || {}),
|
|
541
|
+
...(pkg.devDependencies || {}),
|
|
542
|
+
...(pkg.optionalDependencies || {}),
|
|
543
|
+
};
|
|
544
|
+
for (const [name, version] of Object.entries(allDeps)) {
|
|
545
|
+
const ioc = COMPROMISED_PACKAGES.find(c => c.name === name);
|
|
546
|
+
if (ioc) {
|
|
547
|
+
// Strip semver range prefix (^, ~, >=, etc.) for comparison
|
|
548
|
+
const bare = String(version).replace(/^[\^~>=<]+/, '').trim();
|
|
549
|
+
if (ioc.badVersions.includes(bare)) {
|
|
550
|
+
iocSources.push({ file: pkgPath, name, version: bare, note: ioc.note });
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch { /* skip */ }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Python requirements.txt
|
|
558
|
+
if (fs.existsSync(path.join(rootPath, 'requirements.txt'))) {
|
|
559
|
+
const lines = (this.readFile(path.join(rootPath, 'requirements.txt')) || '').split('\n');
|
|
560
|
+
for (const line of lines) {
|
|
561
|
+
const m = line.trim().match(/^([\w-]+)==([\d.]+)/);
|
|
562
|
+
if (!m) continue;
|
|
563
|
+
const [, name, version] = m;
|
|
564
|
+
const ioc = COMPROMISED_PACKAGES.find(c => c.name === name.toLowerCase());
|
|
565
|
+
if (ioc && ioc.badVersions.includes(version)) {
|
|
566
|
+
iocSources.push({ file: path.join(rootPath, 'requirements.txt'), name, version, note: ioc.note });
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
for (const { file, name, version, note } of iocSources) {
|
|
572
|
+
findings.push(createFinding({
|
|
573
|
+
file,
|
|
574
|
+
line: 0,
|
|
575
|
+
severity: 'critical',
|
|
576
|
+
category: 'supply-chain',
|
|
577
|
+
rule: 'KNOWN_COMPROMISED_PACKAGE',
|
|
578
|
+
title: `Known-Compromised Package: ${name}@${version}`,
|
|
579
|
+
description: `${name}@${version} is a known-malicious release. ${note}`,
|
|
580
|
+
matched: `${name}@${version}`,
|
|
581
|
+
fix: `Update immediately to the latest safe release and rotate any credentials that may have been exfiltrated.`,
|
|
582
|
+
}));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// ── 9. Blockchain C2 indicators (CanisterWorm / ICP) ──────────────────────
|
|
586
|
+
if (fs.existsSync(path.join(rootPath, 'node_modules'))) {
|
|
587
|
+
try {
|
|
588
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
589
|
+
const allDeps = {
|
|
590
|
+
...(pkg.dependencies || {}),
|
|
591
|
+
...(pkg.devDependencies || {}),
|
|
592
|
+
};
|
|
593
|
+
for (const depName of Object.keys(allDeps)) {
|
|
594
|
+
const depPkgPath = path.join(rootPath, 'node_modules', depName, 'package.json');
|
|
595
|
+
if (!fs.existsSync(depPkgPath)) continue;
|
|
596
|
+
try {
|
|
597
|
+
const depPkg = JSON.parse(fs.readFileSync(depPkgPath, 'utf-8'));
|
|
598
|
+
const depDeps = {
|
|
599
|
+
...(depPkg.dependencies || {}),
|
|
600
|
+
...(depPkg.devDependencies || {}),
|
|
601
|
+
};
|
|
602
|
+
const suspiciousICP = ICP_BLOCKCHAIN_PACKAGES.filter(p => p in depDeps);
|
|
603
|
+
if (suspiciousICP.length > 0) {
|
|
604
|
+
findings.push(createFinding({
|
|
605
|
+
file: depPkgPath,
|
|
606
|
+
line: 0,
|
|
607
|
+
severity: 'critical',
|
|
608
|
+
category: 'supply-chain',
|
|
609
|
+
rule: 'BLOCKCHAIN_C2_INDICATOR',
|
|
610
|
+
title: `Blockchain C2 Indicator in ${depName}: ${suspiciousICP.join(', ')}`,
|
|
611
|
+
description: `Dependency "${depName}" imports ICP/Internet Computer blockchain packages (${suspiciousICP.join(', ')}). This matches the CanisterWorm attack pattern, which used an ICP canister as a decentralized, takedown-resistant C2 server to coordinate credential exfiltration.`,
|
|
612
|
+
matched: suspiciousICP.join(', '),
|
|
613
|
+
fix: 'Immediately audit or remove this dependency. ICP blockchain packages have no legitimate role in most application dependencies.',
|
|
614
|
+
}));
|
|
615
|
+
}
|
|
616
|
+
} catch { /* skip */ }
|
|
617
|
+
}
|
|
618
|
+
} catch { /* skip */ }
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return findings;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Simple Levenshtein distance for typosquatting detection.
|
|
626
|
+
*/
|
|
627
|
+
levenshtein(a, b) {
|
|
628
|
+
if (a.length === 0) return b.length;
|
|
629
|
+
if (b.length === 0) return a.length;
|
|
630
|
+
|
|
631
|
+
const matrix = [];
|
|
632
|
+
for (let i = 0; i <= b.length; i++) matrix[i] = [i];
|
|
633
|
+
for (let j = 0; j <= a.length; j++) matrix[0][j] = j;
|
|
634
|
+
|
|
635
|
+
for (let i = 1; i <= b.length; i++) {
|
|
636
|
+
for (let j = 1; j <= a.length; j++) {
|
|
637
|
+
const cost = b[i - 1] === a[j - 1] ? 0 : 1;
|
|
638
|
+
matrix[i][j] = Math.min(
|
|
639
|
+
matrix[i - 1][j] + 1,
|
|
640
|
+
matrix[i][j - 1] + 1,
|
|
641
|
+
matrix[i - 1][j - 1] + cost
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return matrix[b.length][a.length];
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
export default SupplyChainAudit;
|