@lateos/npm-scan 0.18.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/CHANGELOG.md +265 -233
  2. package/LICENSING.md +19 -19
  3. package/README.de.md +708 -708
  4. package/README.fr.md +707 -707
  5. package/README.ja.md +704 -704
  6. package/README.md +861 -826
  7. package/README.zh.md +708 -708
  8. package/VALIDATION.md +92 -0
  9. package/backend/cra.js +68 -68
  10. package/backend/db/pg-schema.sql +155 -0
  11. package/backend/db/schema.sql +32 -32
  12. package/backend/db.js +88 -88
  13. package/backend/detectors/atk-001-lifecycle.js +17 -17
  14. package/backend/detectors/atk-002-obfusc.js +261 -261
  15. package/backend/detectors/atk-003-creds.js +13 -13
  16. package/backend/detectors/atk-004-persist.js +13 -13
  17. package/backend/detectors/atk-005-exfil.js +13 -13
  18. package/backend/detectors/atk-006-depconf.js +14 -14
  19. package/backend/detectors/atk-007-typosquat.js +34 -34
  20. package/backend/detectors/atk-008-tarball-tamper.js +91 -91
  21. package/backend/detectors/atk-009-dormant-trigger.js +62 -62
  22. package/backend/detectors/atk-010-sandbox-evasion.js +50 -50
  23. package/backend/detectors/atk-011-transitive-prop.js +76 -76
  24. package/backend/detectors/config/thresholds.js +66 -0
  25. package/backend/detectors/config/whitelist.json +74 -0
  26. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +99 -99
  27. package/backend/detectors/cve-2026-48710-badhost/findings.js +105 -105
  28. package/backend/detectors/cve-2026-48710-badhost/index.js +15 -15
  29. package/backend/detectors/cve-2026-48710-badhost/manifest.js +305 -305
  30. package/backend/detectors/cve-2026-48710-badhost/transitive.js +189 -189
  31. package/backend/detectors/hf-impersonation/index.js +396 -396
  32. package/backend/detectors/hf-impersonation/jaro-winkler.js +44 -44
  33. package/backend/detectors/hf-impersonation/known-orgs.js +5 -5
  34. package/backend/detectors/hf-impersonation/simhash.js +46 -46
  35. package/backend/detectors/index.js +87 -81
  36. package/backend/detectors/lib/ast-patterns.js +21 -0
  37. package/backend/detectors/lib/entropy-analyzer.js +24 -0
  38. package/backend/detectors/megalodon/d1-workflow-scan.js +147 -147
  39. package/backend/detectors/megalodon/d2-credential-harvest.js +61 -61
  40. package/backend/detectors/megalodon/d3-publish-velocity.js +67 -67
  41. package/backend/detectors/megalodon/d4-publisher-drift.js +124 -124
  42. package/backend/detectors/megalodon/d5-bot-commit-identity.js +3 -3
  43. package/backend/detectors/megalodon/d6-date-anachronism.js +3 -3
  44. package/backend/detectors/megalodon/index.js +80 -80
  45. package/backend/detectors/megalodon/types.js +9 -9
  46. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +42 -42
  47. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +116 -116
  48. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +72 -72
  49. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +45 -45
  50. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +95 -95
  51. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +38 -38
  52. package/backend/detectors/mini-shai-hulud/index.js +118 -118
  53. package/backend/detectors/mini-shai-hulud/iocs.json +79 -79
  54. package/backend/detectors/tier1-binary-embed.js +34 -5
  55. package/backend/detectors/tier1-obfuscation-heuristics.js +156 -0
  56. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  57. package/backend/detectors/tier1-version-anomaly.js +187 -0
  58. package/backend/detectors.test.js +88 -0
  59. package/backend/fetch.js +175 -175
  60. package/backend/index.js +4 -4
  61. package/backend/license.js +89 -89
  62. package/backend/lockfile.js +379 -379
  63. package/backend/pdf.js +245 -245
  64. package/backend/policy.js +193 -193
  65. package/backend/report.js +254 -254
  66. package/backend/sbom.js +66 -66
  67. package/backend/scripts/analyze-false-positives.js +146 -0
  68. package/backend/scripts/analyze-validation.js +151 -0
  69. package/backend/scripts/detect-false-positives.js +93 -0
  70. package/backend/scripts/fetch-top-packages.js +129 -0
  71. package/backend/scripts/validate-detectors.js +142 -0
  72. package/backend/siem/cef.js +32 -32
  73. package/backend/siem/ecs.js +40 -40
  74. package/backend/siem/index.js +18 -18
  75. package/backend/siem/qradar.js +56 -56
  76. package/backend/siem/sentinel.js +27 -27
  77. package/backend/tests-d5-enhanced.test.js +46 -0
  78. package/backend/tests-d6-version-anomaly.test.js +58 -0
  79. package/backend/tests-d6.test.js +116 -0
  80. package/backend/tests-d6c.test.js +106 -0
  81. package/backend/tests-d7-obfuscation.test.js +91 -0
  82. package/backend/tests.test.js +898 -0
  83. package/backend/vsix-scan/detectors/activation-event-risk.js +116 -116
  84. package/backend/vsix-scan/detectors/burst-publish.js +52 -52
  85. package/backend/vsix-scan/detectors/exfil-pattern.js +88 -88
  86. package/backend/vsix-scan/detectors/known-ioc.js +105 -105
  87. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +69 -69
  88. package/backend/vsix-scan/detectors/publisher-anomaly.js +70 -70
  89. package/backend/vsix-scan/index.js +183 -183
  90. package/backend/vsix-scan/marketplace-client.js +145 -145
  91. package/backend/vsix-scan/vsix-iocs.json +31 -31
  92. package/cli/cli.js +458 -458
  93. package/package.json +74 -57
  94. package/.dockerignore +0 -20
  95. package/.husky/pre-commit +0 -1
  96. package/SECURITY.md +0 -73
  97. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  98. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  99. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  100. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  101. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  102. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  103. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  104. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  105. package/deploy/helm/npm-scan/values.yaml +0 -103
  106. package/scripts/download-corpus.js +0 -30
  107. package/scripts/gen-mal-corpus.js +0 -35
  108. package/scripts/generate-campaign-fixtures.js +0 -170
  109. package/src/config/top-5000.json +0 -87
  110. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  111. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  112. package/test/fixtures/lockfiles/yarn.lock +0 -104
  113. package/test/fixtures/mock-data.js +0 -69
@@ -1,380 +1,380 @@
1
- import { readFileSync } from 'fs';
2
- import { resolve, dirname } from 'path';
3
- import yaml from 'js-yaml';
4
-
5
- export function parseLockfile(filePath, options = {}) {
6
- const { autoDetect = false } = options;
7
- try {
8
- const content = readFileSync(filePath, 'utf8');
9
- const ext = filePath.split('.').pop().toLowerCase();
10
-
11
- if (ext === 'json' || ext === 'jsonc') {
12
- return parseNpmLockfile(content, filePath);
13
- }
14
- if (ext === 'lock' && !autoDetect) {
15
- return parseYarnLockfile(content, filePath);
16
- }
17
- if (ext === 'yaml' || ext === 'yml') {
18
- return parsePnpmLockfile(content, filePath);
19
- }
20
-
21
- if (autoDetect) {
22
- if (content.trimStart().startsWith('{')) {
23
- return parseNpmLockfile(content, filePath);
24
- }
25
- if (content.includes('__metadata')) {
26
- return parsePnpmLockfile(content, filePath);
27
- }
28
- if (content.includes('@npm:') || /^\s*"?[\w@/-]+['"]?\s*,\s*$/m.test(content)) {
29
- return parseYarnLockfile(content, filePath);
30
- }
31
- }
32
-
33
- return parseNpmLockfile(content, filePath);
34
- } catch (e) {
35
- throw new Error(`Failed to parse lockfile: ${e.message}`);
36
- }
37
- }
38
-
39
- function parseNpmLockfile(content, filePath) {
40
- const lockfile = JSON.parse(content);
41
- const packages = [];
42
-
43
- if (lockfile.packages) {
44
- for (const [key, pkg] of Object.entries(lockfile.packages)) {
45
- if (key === '') continue;
46
- const name = pkg.name || key.replace(/^node_modules\//, '').replace(/^[^/]+\//, '');
47
- packages.push({
48
- name,
49
- version: pkg.version || 'unknown',
50
- resolved: pkg.resolved || '',
51
- integrity: pkg.integrity || '',
52
- path: key,
53
- peerDeps: pkg.peerDependencies || {},
54
- dev: pkg.dev || false,
55
- optional: pkg.optional || false,
56
- scripts: pkg.scripts || {},
57
- dependencies: pkg.dependencies || {}
58
- });
59
- }
60
- }
61
-
62
- const rootDeps = lockfile.packages?.['node_modules/'] || {};
63
- return {
64
- version: lockfile.lockfileVersion,
65
- packages,
66
- root: {
67
- name: rootDeps.name || 'unknown',
68
- version: rootDeps.version || 'unknown',
69
- dependencies: rootDeps.dependencies || {},
70
- devDependencies: rootDeps.devDependencies || {},
71
- peerDependencies: rootDeps.peerDependencies || {}
72
- }
73
- };
74
- }
75
-
76
- function parseYarnLockfile(content, filePath) {
77
- const packages = [];
78
- const lines = content.split('\n');
79
- let i = 0;
80
- const n = lines.length;
81
-
82
- const MULTI_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*,\s*"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
83
- const SINGLE_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
84
-
85
- while (i < n) {
86
- let line = lines[i].trimEnd();
87
-
88
- let specs = [];
89
-
90
- const multiMatch = line.match(MULTI_ENTRY_RE);
91
- const singleMatch = line.match(SINGLE_ENTRY_RE);
92
-
93
- if (multiMatch) {
94
- specs = [
95
- { name: multiMatch[1], specVersion: multiMatch[2] },
96
- { name: multiMatch[3], specVersion: multiMatch[4] }
97
- ];
98
- } else if (singleMatch) {
99
- specs = [{ name: singleMatch[1], specVersion: singleMatch[2] }];
100
- }
101
-
102
- if (specs.length > 0) {
103
- let version = '';
104
- let resolved = '';
105
- let integrity = '';
106
- const dependencies = {};
107
- const optionalDependencies = {};
108
- const peerDependencies = {};
109
- let dev = false;
110
- let optional = false;
111
-
112
- i++;
113
- while (i < n) {
114
- const bodyLine = lines[i];
115
- const bodyTrim = bodyLine.trimEnd();
116
-
117
- if (bodyTrim === '' || bodyTrim.startsWith('#')) {
118
- i++;
119
- continue;
120
- }
121
-
122
- if (bodyTrim.endsWith(':') && !bodyLine.startsWith(' ')) {
123
- break;
124
- }
125
-
126
- if (bodyTrim.startsWith('version ')) {
127
- const vMatch = bodyTrim.match(/^version ['"]([^'"]+)['"]/);
128
- if (vMatch) version = vMatch[1];
129
- } else if (bodyTrim.match(/^\s*resolved\s+(.+)/)) {
130
- const rMatch = bodyTrim.match(/^\s*resolved\s+(.+)/);
131
- if (rMatch) {
132
- resolved = rMatch[1].trim().replace(/^['"]|['"]$/g, '');
133
- if (resolved.startsWith('https://registry.yarnpkg.com/')) {
134
- resolved = resolved.replace('https://registry.yarnpkg.com/', 'https://registry.npmjs.org/');
135
- }
136
- }
137
- } else if (bodyTrim.startsWith('integrity ')) {
138
- integrity = bodyTrim.replace('integrity ', '').trim();
139
- } else if (bodyTrim.startsWith('dependencies')) {
140
- const m = bodyTrim.match(/^dependencies\s+(.*)/);
141
- if (m) parseDepList(m[1], dependencies);
142
- } else if (bodyTrim.startsWith('optionalDependencies')) {
143
- const m = bodyTrim.match(/^optionalDependencies\s+(.*)/);
144
- if (m) parseDepList(m[1], optionalDependencies);
145
- } else if (bodyTrim.startsWith('peerDependencies')) {
146
- const m = bodyTrim.match(/^peerDependencies\s+(.*)/);
147
- if (m) parseDepList(m[1], peerDependencies);
148
- } else if (bodyTrim.match(/^\s*dev\s+(true|false)$/)) {
149
- dev = bodyTrim.includes('true');
150
- } else if (bodyTrim.match(/^\s*optional\s+(true|false)$/)) {
151
- optional = bodyTrim.includes('true');
152
- }
153
-
154
- i++;
155
- }
156
-
157
- for (const { name, specVersion } of specs) {
158
- packages.push({
159
- name,
160
- version: version || specVersion,
161
- resolved,
162
- integrity,
163
- path: `node_modules/${name}`,
164
- peerDeps: peerDependencies,
165
- dev,
166
- optional,
167
- scripts: {},
168
- dependencies,
169
- optionalDependencies
170
- });
171
- }
172
- } else {
173
- i++;
174
- }
175
- }
176
-
177
- const rootDeps = {};
178
- const rootDevDeps = {};
179
-
180
- for (const pkg of packages) {
181
- const topDeps = pkg.dev ? rootDevDeps : rootDeps;
182
- for (const depName of Object.keys(pkg.dependencies)) {
183
- topDeps[depName] = pkg.dependencies[depName];
184
- }
185
- }
186
-
187
- return {
188
- version: 2,
189
- packages,
190
- root: {
191
- name: 'root',
192
- version: 'unknown',
193
- dependencies: rootDeps,
194
- devDependencies: rootDevDeps,
195
- peerDependencies: {}
196
- }
197
- };
198
- }
199
-
200
- function parseDepList(str, dest) {
201
- const cleaned = str.replace(/^[[\]]/g, '').trim();
202
- if (!cleaned) return;
203
- const re = /([\w@./-]+)\s+\^?([\w@./-]+)/g;
204
- let m;
205
- while ((m = re.exec(cleaned)) !== null) {
206
- dest[m[1]] = m[2];
207
- }
208
- }
209
-
210
- function parsePnpmLockfile(content, filePath) {
211
- const lockfile = yaml.load(content);
212
- const packages = [];
213
-
214
- if (lockfile.packages) {
215
- for (const [key, pkg] of Object.entries(lockfile.packages)) {
216
- const nameMatch = key.match(/^\/(.+?)@([^@/]+)$/);
217
- if (!nameMatch) continue;
218
- const name = nameMatch[1];
219
- const version = nameMatch[2];
220
-
221
- const resolved = pkg.resolution?.url || '';
222
- let integrity = '';
223
- if (pkg.resolution?.integrity) {
224
- integrity = pkg.resolution.integrity;
225
- } else if (pkg.resolution?.sha512) {
226
- integrity = `sha512-${pkg.resolution.sha512}`;
227
- }
228
-
229
- packages.push({
230
- name,
231
- version,
232
- resolved,
233
- integrity,
234
- path: `node_modules/${name}`,
235
- peerDeps: pkg.peerDependencies || {},
236
- dev: pkg.dev || false,
237
- optional: pkg.optional || false,
238
- scripts: pkg.hasBundledMedia ? { bundled: true } : {},
239
- dependencies: pkg.dependencies || {},
240
- optionalDependencies: pkg.optionalDependencies || {}
241
- });
242
- }
243
- }
244
-
245
- const rootDeps = lockfile.importers?.['.'] || lockfile.root || {};
246
- const rootDepsMap = rootDeps.dependencies || {};
247
- const rootDevDepsMap = rootDeps.devDependencies || {};
248
- const rootPeerDepsMap = rootDeps.peerDependencies || {};
249
-
250
- const version = lockfile.version || (lockfile.lockfileVersion ?? 6);
251
-
252
- return {
253
- version,
254
- packages,
255
- root: {
256
- name: 'root',
257
- version: lockfile.lockfileVersion ? 'unknown' : 'unknown',
258
- dependencies: rootDepsMap,
259
- devDependencies: rootDevDepsMap,
260
- peerDependencies: rootPeerDepsMap
261
- }
262
- };
263
- }
264
-
265
- export function checkMaliciousPatterns(pkg) {
266
- const findings = [];
267
- const name = pkg.name?.toLowerCase() || '';
268
-
269
- const typosquatPatterns = [
270
- /^(lodash|lodahs|lodash-js|lodashexe)$/,
271
- /^(axios|axio|ax10s|ax1os)$/,
272
- /^(react|reakt|reackt|r3act)$/,
273
- /^(express|expres|expresjs|exress)$/,
274
- /^(vue|vu3|vujs|vuejs)$/,
275
- /^(webpack|webpak|webpackjs)$/,
276
- ];
277
-
278
- for (const pattern of typosquatPatterns) {
279
- if (pattern.test(name)) {
280
- findings.push({
281
- id: 'ATK-007',
282
- severity: 'high',
283
- title: 'Typosquat detected',
284
- description: `Package name "${pkg.name}" is similar to popular packages`,
285
- evidence: `similar to ${pattern.source}`
286
- });
287
- }
288
- }
289
-
290
- return findings;
291
- }
292
-
293
- export function analyzeDependencyGraph(lockfileData) {
294
- const findings = [];
295
- const pkgMap = new Map();
296
-
297
- for (const pkg of lockfileData.packages) {
298
- pkgMap.set(pkg.name, pkg);
299
- }
300
-
301
- for (const pkg of lockfileData.packages) {
302
- if (pkg.peerDeps && Object.keys(pkg.peerDeps).length > 0) {
303
- for (const [peerName, peerVersion] of Object.entries(pkg.peerDeps)) {
304
- if (peerName.includes('plugin') || peerName.includes('hook') || peerName.includes('ext')) {
305
- findings.push({
306
- id: 'ATK-011',
307
- severity: 'high',
308
- title: 'Transitive propagation (worm)',
309
- description: `Package "${pkg.name}" depends on peer "${peerName}@${peerVersion}" - potential worm propagation chain`,
310
- evidence: `peer dep chain: ${pkg.name} -> ${peerName}`
311
- });
312
- }
313
- }
314
- }
315
-
316
- if (pkg.dependencies && typeof pkg.dependencies === 'object' && Object.keys(pkg.dependencies).length > 5) {
317
- const transitiveCount = Object.keys(pkg.dependencies).filter(k => k.includes('/')).length;
318
- if (transitiveCount > 3) {
319
- findings.push({
320
- id: 'ATK-011',
321
- severity: 'medium',
322
- title: 'Transitive propagation (worm)',
323
- description: `Package "${pkg.name}" has excessive transitive dependencies (${transitiveCount} scoped)`,
324
- evidence: `heavy transitive dep chain: ${pkg.name}`
325
- });
326
- }
327
- }
328
-
329
- if (pkg.optionalDependencies && Object.keys(pkg.optionalDependencies).length > 10) {
330
- findings.push({
331
- id: 'ATK-011',
332
- severity: 'low',
333
- title: 'Transitive propagation (worm)',
334
- description: `Package "${pkg.name}" has excessive optional dependencies (${Object.keys(pkg.optionalDependencies).length})`,
335
- evidence: `optional dep chain: ${pkg.name} -> [${Object.keys(pkg.optionalDependencies).slice(0, 3).join(', ')}, ...]`
336
- });
337
- }
338
- }
339
-
340
- return findings;
341
- }
342
-
343
- export function generateLockfileReport(lockfileData) {
344
- const total = lockfileData.packages.length;
345
- const dev = lockfileData.packages.filter(p => p.dev).length;
346
- const optional = lockfileData.packages.filter(p => p.optional).length;
347
-
348
- const findings = [];
349
-
350
- for (const pkg of lockfileData.packages) {
351
- const maliciousFindings = checkMaliciousPatterns(pkg);
352
- findings.push(...maliciousFindings);
353
- }
354
-
355
- findings.push(...analyzeDependencyGraph(lockfileData));
356
-
357
- return {
358
- scanId: Date.now(),
359
- package: lockfileData.root.name,
360
- version: lockfileData.root.version,
361
- totalDependencies: total,
362
- devDependencies: dev,
363
- optionalDependencies: optional,
364
- lockfileVersion: lockfileData.version,
365
- findings,
366
- riskScore: calculateRiskScore(findings)
367
- };
368
- }
369
-
370
- function calculateRiskScore(findings) {
371
- if (!findings.length) return '0.0';
372
- const weights = { critical: 10, high: 7, medium: 4, low: 2, info: 0.5 };
373
- const maxSeverity = findings.reduce((max, f) => {
374
- const w = weights[f.severity] || 0;
375
- return Math.max(max, w);
376
- }, 0);
377
- const countBonus = Math.min(findings.length * 0.3, 3);
378
- const score = Math.min(maxSeverity + countBonus, 10);
379
- return score.toFixed(1);
1
+ import { readFileSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import yaml from 'js-yaml';
4
+
5
+ export function parseLockfile(filePath, options = {}) {
6
+ const { autoDetect = false } = options;
7
+ try {
8
+ const content = readFileSync(filePath, 'utf8');
9
+ const ext = filePath.split('.').pop().toLowerCase();
10
+
11
+ if (ext === 'json' || ext === 'jsonc') {
12
+ return parseNpmLockfile(content, filePath);
13
+ }
14
+ if (ext === 'lock' && !autoDetect) {
15
+ return parseYarnLockfile(content, filePath);
16
+ }
17
+ if (ext === 'yaml' || ext === 'yml') {
18
+ return parsePnpmLockfile(content, filePath);
19
+ }
20
+
21
+ if (autoDetect) {
22
+ if (content.trimStart().startsWith('{')) {
23
+ return parseNpmLockfile(content, filePath);
24
+ }
25
+ if (content.includes('__metadata')) {
26
+ return parsePnpmLockfile(content, filePath);
27
+ }
28
+ if (content.includes('@npm:') || /^\s*"?[\w@/-]+['"]?\s*,\s*$/m.test(content)) {
29
+ return parseYarnLockfile(content, filePath);
30
+ }
31
+ }
32
+
33
+ return parseNpmLockfile(content, filePath);
34
+ } catch (e) {
35
+ throw new Error(`Failed to parse lockfile: ${e.message}`);
36
+ }
37
+ }
38
+
39
+ function parseNpmLockfile(content, filePath) {
40
+ const lockfile = JSON.parse(content);
41
+ const packages = [];
42
+
43
+ if (lockfile.packages) {
44
+ for (const [key, pkg] of Object.entries(lockfile.packages)) {
45
+ if (key === '') continue;
46
+ const name = pkg.name || key.replace(/^node_modules\//, '').replace(/^[^/]+\//, '');
47
+ packages.push({
48
+ name,
49
+ version: pkg.version || 'unknown',
50
+ resolved: pkg.resolved || '',
51
+ integrity: pkg.integrity || '',
52
+ path: key,
53
+ peerDeps: pkg.peerDependencies || {},
54
+ dev: pkg.dev || false,
55
+ optional: pkg.optional || false,
56
+ scripts: pkg.scripts || {},
57
+ dependencies: pkg.dependencies || {}
58
+ });
59
+ }
60
+ }
61
+
62
+ const rootDeps = lockfile.packages?.['node_modules/'] || {};
63
+ return {
64
+ version: lockfile.lockfileVersion,
65
+ packages,
66
+ root: {
67
+ name: rootDeps.name || 'unknown',
68
+ version: rootDeps.version || 'unknown',
69
+ dependencies: rootDeps.dependencies || {},
70
+ devDependencies: rootDeps.devDependencies || {},
71
+ peerDependencies: rootDeps.peerDependencies || {}
72
+ }
73
+ };
74
+ }
75
+
76
+ function parseYarnLockfile(content, filePath) {
77
+ const packages = [];
78
+ const lines = content.split('\n');
79
+ let i = 0;
80
+ const n = lines.length;
81
+
82
+ const MULTI_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*,\s*"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
83
+ const SINGLE_ENTRY_RE = /^"?([\w@./-]+)@(\^?[\w.+\-~]+)"?\s*:\s*$/;
84
+
85
+ while (i < n) {
86
+ let line = lines[i].trimEnd();
87
+
88
+ let specs = [];
89
+
90
+ const multiMatch = line.match(MULTI_ENTRY_RE);
91
+ const singleMatch = line.match(SINGLE_ENTRY_RE);
92
+
93
+ if (multiMatch) {
94
+ specs = [
95
+ { name: multiMatch[1], specVersion: multiMatch[2] },
96
+ { name: multiMatch[3], specVersion: multiMatch[4] }
97
+ ];
98
+ } else if (singleMatch) {
99
+ specs = [{ name: singleMatch[1], specVersion: singleMatch[2] }];
100
+ }
101
+
102
+ if (specs.length > 0) {
103
+ let version = '';
104
+ let resolved = '';
105
+ let integrity = '';
106
+ const dependencies = {};
107
+ const optionalDependencies = {};
108
+ const peerDependencies = {};
109
+ let dev = false;
110
+ let optional = false;
111
+
112
+ i++;
113
+ while (i < n) {
114
+ const bodyLine = lines[i];
115
+ const bodyTrim = bodyLine.trimEnd();
116
+
117
+ if (bodyTrim === '' || bodyTrim.startsWith('#')) {
118
+ i++;
119
+ continue;
120
+ }
121
+
122
+ if (bodyTrim.endsWith(':') && !bodyLine.startsWith(' ')) {
123
+ break;
124
+ }
125
+
126
+ if (bodyTrim.startsWith('version ')) {
127
+ const vMatch = bodyTrim.match(/^version ['"]([^'"]+)['"]/);
128
+ if (vMatch) version = vMatch[1];
129
+ } else if (bodyTrim.match(/^\s*resolved\s+(.+)/)) {
130
+ const rMatch = bodyTrim.match(/^\s*resolved\s+(.+)/);
131
+ if (rMatch) {
132
+ resolved = rMatch[1].trim().replace(/^['"]|['"]$/g, '');
133
+ if (resolved.startsWith('https://registry.yarnpkg.com/')) {
134
+ resolved = resolved.replace('https://registry.yarnpkg.com/', 'https://registry.npmjs.org/');
135
+ }
136
+ }
137
+ } else if (bodyTrim.startsWith('integrity ')) {
138
+ integrity = bodyTrim.replace('integrity ', '').trim();
139
+ } else if (bodyTrim.startsWith('dependencies')) {
140
+ const m = bodyTrim.match(/^dependencies\s+(.*)/);
141
+ if (m) parseDepList(m[1], dependencies);
142
+ } else if (bodyTrim.startsWith('optionalDependencies')) {
143
+ const m = bodyTrim.match(/^optionalDependencies\s+(.*)/);
144
+ if (m) parseDepList(m[1], optionalDependencies);
145
+ } else if (bodyTrim.startsWith('peerDependencies')) {
146
+ const m = bodyTrim.match(/^peerDependencies\s+(.*)/);
147
+ if (m) parseDepList(m[1], peerDependencies);
148
+ } else if (bodyTrim.match(/^\s*dev\s+(true|false)$/)) {
149
+ dev = bodyTrim.includes('true');
150
+ } else if (bodyTrim.match(/^\s*optional\s+(true|false)$/)) {
151
+ optional = bodyTrim.includes('true');
152
+ }
153
+
154
+ i++;
155
+ }
156
+
157
+ for (const { name, specVersion } of specs) {
158
+ packages.push({
159
+ name,
160
+ version: version || specVersion,
161
+ resolved,
162
+ integrity,
163
+ path: `node_modules/${name}`,
164
+ peerDeps: peerDependencies,
165
+ dev,
166
+ optional,
167
+ scripts: {},
168
+ dependencies,
169
+ optionalDependencies
170
+ });
171
+ }
172
+ } else {
173
+ i++;
174
+ }
175
+ }
176
+
177
+ const rootDeps = {};
178
+ const rootDevDeps = {};
179
+
180
+ for (const pkg of packages) {
181
+ const topDeps = pkg.dev ? rootDevDeps : rootDeps;
182
+ for (const depName of Object.keys(pkg.dependencies)) {
183
+ topDeps[depName] = pkg.dependencies[depName];
184
+ }
185
+ }
186
+
187
+ return {
188
+ version: 2,
189
+ packages,
190
+ root: {
191
+ name: 'root',
192
+ version: 'unknown',
193
+ dependencies: rootDeps,
194
+ devDependencies: rootDevDeps,
195
+ peerDependencies: {}
196
+ }
197
+ };
198
+ }
199
+
200
+ function parseDepList(str, dest) {
201
+ const cleaned = str.replace(/^[[\]]/g, '').trim();
202
+ if (!cleaned) return;
203
+ const re = /([\w@./-]+)\s+\^?([\w@./-]+)/g;
204
+ let m;
205
+ while ((m = re.exec(cleaned)) !== null) {
206
+ dest[m[1]] = m[2];
207
+ }
208
+ }
209
+
210
+ function parsePnpmLockfile(content, filePath) {
211
+ const lockfile = yaml.load(content);
212
+ const packages = [];
213
+
214
+ if (lockfile.packages) {
215
+ for (const [key, pkg] of Object.entries(lockfile.packages)) {
216
+ const nameMatch = key.match(/^\/(.+?)@([^@/]+)$/);
217
+ if (!nameMatch) continue;
218
+ const name = nameMatch[1];
219
+ const version = nameMatch[2];
220
+
221
+ const resolved = pkg.resolution?.url || '';
222
+ let integrity = '';
223
+ if (pkg.resolution?.integrity) {
224
+ integrity = pkg.resolution.integrity;
225
+ } else if (pkg.resolution?.sha512) {
226
+ integrity = `sha512-${pkg.resolution.sha512}`;
227
+ }
228
+
229
+ packages.push({
230
+ name,
231
+ version,
232
+ resolved,
233
+ integrity,
234
+ path: `node_modules/${name}`,
235
+ peerDeps: pkg.peerDependencies || {},
236
+ dev: pkg.dev || false,
237
+ optional: pkg.optional || false,
238
+ scripts: pkg.hasBundledMedia ? { bundled: true } : {},
239
+ dependencies: pkg.dependencies || {},
240
+ optionalDependencies: pkg.optionalDependencies || {}
241
+ });
242
+ }
243
+ }
244
+
245
+ const rootDeps = lockfile.importers?.['.'] || lockfile.root || {};
246
+ const rootDepsMap = rootDeps.dependencies || {};
247
+ const rootDevDepsMap = rootDeps.devDependencies || {};
248
+ const rootPeerDepsMap = rootDeps.peerDependencies || {};
249
+
250
+ const version = lockfile.version || (lockfile.lockfileVersion ?? 6);
251
+
252
+ return {
253
+ version,
254
+ packages,
255
+ root: {
256
+ name: 'root',
257
+ version: lockfile.lockfileVersion ? 'unknown' : 'unknown',
258
+ dependencies: rootDepsMap,
259
+ devDependencies: rootDevDepsMap,
260
+ peerDependencies: rootPeerDepsMap
261
+ }
262
+ };
263
+ }
264
+
265
+ export function checkMaliciousPatterns(pkg) {
266
+ const findings = [];
267
+ const name = pkg.name?.toLowerCase() || '';
268
+
269
+ const typosquatPatterns = [
270
+ /^(lodash|lodahs|lodash-js|lodashexe)$/,
271
+ /^(axios|axio|ax10s|ax1os)$/,
272
+ /^(react|reakt|reackt|r3act)$/,
273
+ /^(express|expres|expresjs|exress)$/,
274
+ /^(vue|vu3|vujs|vuejs)$/,
275
+ /^(webpack|webpak|webpackjs)$/,
276
+ ];
277
+
278
+ for (const pattern of typosquatPatterns) {
279
+ if (pattern.test(name)) {
280
+ findings.push({
281
+ id: 'ATK-007',
282
+ severity: 'high',
283
+ title: 'Typosquat detected',
284
+ description: `Package name "${pkg.name}" is similar to popular packages`,
285
+ evidence: `similar to ${pattern.source}`
286
+ });
287
+ }
288
+ }
289
+
290
+ return findings;
291
+ }
292
+
293
+ export function analyzeDependencyGraph(lockfileData) {
294
+ const findings = [];
295
+ const pkgMap = new Map();
296
+
297
+ for (const pkg of lockfileData.packages) {
298
+ pkgMap.set(pkg.name, pkg);
299
+ }
300
+
301
+ for (const pkg of lockfileData.packages) {
302
+ if (pkg.peerDeps && Object.keys(pkg.peerDeps).length > 0) {
303
+ for (const [peerName, peerVersion] of Object.entries(pkg.peerDeps)) {
304
+ if (peerName.includes('plugin') || peerName.includes('hook') || peerName.includes('ext')) {
305
+ findings.push({
306
+ id: 'ATK-011',
307
+ severity: 'high',
308
+ title: 'Transitive propagation (worm)',
309
+ description: `Package "${pkg.name}" depends on peer "${peerName}@${peerVersion}" - potential worm propagation chain`,
310
+ evidence: `peer dep chain: ${pkg.name} -> ${peerName}`
311
+ });
312
+ }
313
+ }
314
+ }
315
+
316
+ if (pkg.dependencies && typeof pkg.dependencies === 'object' && Object.keys(pkg.dependencies).length > 5) {
317
+ const transitiveCount = Object.keys(pkg.dependencies).filter(k => k.includes('/')).length;
318
+ if (transitiveCount > 3) {
319
+ findings.push({
320
+ id: 'ATK-011',
321
+ severity: 'medium',
322
+ title: 'Transitive propagation (worm)',
323
+ description: `Package "${pkg.name}" has excessive transitive dependencies (${transitiveCount} scoped)`,
324
+ evidence: `heavy transitive dep chain: ${pkg.name}`
325
+ });
326
+ }
327
+ }
328
+
329
+ if (pkg.optionalDependencies && Object.keys(pkg.optionalDependencies).length > 10) {
330
+ findings.push({
331
+ id: 'ATK-011',
332
+ severity: 'low',
333
+ title: 'Transitive propagation (worm)',
334
+ description: `Package "${pkg.name}" has excessive optional dependencies (${Object.keys(pkg.optionalDependencies).length})`,
335
+ evidence: `optional dep chain: ${pkg.name} -> [${Object.keys(pkg.optionalDependencies).slice(0, 3).join(', ')}, ...]`
336
+ });
337
+ }
338
+ }
339
+
340
+ return findings;
341
+ }
342
+
343
+ export function generateLockfileReport(lockfileData) {
344
+ const total = lockfileData.packages.length;
345
+ const dev = lockfileData.packages.filter(p => p.dev).length;
346
+ const optional = lockfileData.packages.filter(p => p.optional).length;
347
+
348
+ const findings = [];
349
+
350
+ for (const pkg of lockfileData.packages) {
351
+ const maliciousFindings = checkMaliciousPatterns(pkg);
352
+ findings.push(...maliciousFindings);
353
+ }
354
+
355
+ findings.push(...analyzeDependencyGraph(lockfileData));
356
+
357
+ return {
358
+ scanId: Date.now(),
359
+ package: lockfileData.root.name,
360
+ version: lockfileData.root.version,
361
+ totalDependencies: total,
362
+ devDependencies: dev,
363
+ optionalDependencies: optional,
364
+ lockfileVersion: lockfileData.version,
365
+ findings,
366
+ riskScore: calculateRiskScore(findings)
367
+ };
368
+ }
369
+
370
+ function calculateRiskScore(findings) {
371
+ if (!findings.length) return '0.0';
372
+ const weights = { critical: 10, high: 7, medium: 4, low: 2, info: 0.5 };
373
+ const maxSeverity = findings.reduce((max, f) => {
374
+ const w = weights[f.severity] || 0;
375
+ return Math.max(max, w);
376
+ }, 0);
377
+ const countBonus = Math.min(findings.length * 0.3, 3);
378
+ const score = Math.min(maxSeverity + countBonus, 10);
379
+ return score.toFixed(1);
380
380
  }