@lateos/npm-scan 1.0.0 → 1.1.1

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 (129) hide show
  1. package/README.de.md +3 -98
  2. package/README.fr.md +3 -98
  3. package/README.ja.md +3 -98
  4. package/README.md +2 -122
  5. package/README.zh.md +3 -98
  6. package/backend/cra.js +113 -21
  7. package/backend/db.js +18 -10
  8. package/backend/detectors/atk-001-lifecycle.js +5 -5
  9. package/backend/detectors/atk-002-obfusc.js +126 -47
  10. package/backend/detectors/atk-003-creds.js +8 -4
  11. package/backend/detectors/atk-004-persist.js +3 -3
  12. package/backend/detectors/atk-005-exfil.js +8 -4
  13. package/backend/detectors/atk-006-depconf.js +3 -3
  14. package/backend/detectors/atk-007-typosquat.js +64 -10
  15. package/backend/detectors/atk-008-tarball-tamper.js +6 -6
  16. package/backend/detectors/atk-009-dormant-trigger.js +9 -5
  17. package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
  18. package/backend/detectors/atk-011-transitive-prop.js +14 -13
  19. package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
  20. package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
  21. package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
  22. package/backend/detectors/axios-poisoning/index.js +77 -60
  23. package/backend/detectors/config/thresholds.js +48 -3
  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 +181 -34
  34. package/backend/detectors/lib/ast-patterns.js +4 -1
  35. package/backend/detectors/lib/entropy-analyzer.js +12 -4
  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 +109 -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 +45 -17
  73. package/backend/detectors/tier1-self-propagation.js +115 -0
  74. package/backend/detectors/tier1-slsa-attestation.js +1 -1
  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 +77 -41
  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 +78 -19
  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 +14 -8
  104. package/backend/scripts/analyze-validation.js +27 -21
  105. package/backend/scripts/detect-false-positives.js +20 -10
  106. package/backend/scripts/fetch-top-packages.js +197 -49
  107. package/backend/scripts/validate-d10-d13.js +103 -0
  108. package/backend/scripts/validate-detectors.js +26 -17
  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 +13 -12
  115. package/backend/tests-d6-version-anomaly.test.js +17 -8
  116. package/backend/tests-d6.test.js +24 -14
  117. package/backend/tests-d6c.test.js +27 -14
  118. package/backend/tests-d7-obfuscation.test.js +9 -12
  119. package/backend/tests.test.js +182 -83
  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 +12 -3
package/cli/cli.js CHANGED
@@ -5,7 +5,7 @@ import { watch } from 'fs';
5
5
  import { statSync } from 'fs';
6
6
  import { execSync } from 'child_process';
7
7
  import { glob } from 'glob';
8
- import { isFeatureEnabled, generateKey } from '../backend/license.js';
8
+ import { isFeatureEnabled, generateKey as _generateKey } from '../backend/license.js';
9
9
 
10
10
  function requirePremium(feature, licenseKey) {
11
11
  if (!isFeatureEnabled(feature, licenseKey)) {
@@ -29,7 +29,11 @@ program
29
29
  .option('-l, --license-key <key>', 'Premium license')
30
30
  .option('--sbom [format]', 'Generate SBOM (json/xml/spdx)')
31
31
  .option('-p, --policy <path>', 'Policy file (YAML/JSON)')
32
- .option('--fail-on <level>', 'Exit with code 1 if findings >= level (low|medium|high|critical)', 'none')
32
+ .option(
33
+ '--fail-on <level>',
34
+ 'Exit with code 1 if findings >= level (low|medium|high|critical)',
35
+ 'none'
36
+ )
33
37
  .option('--sarif [file]', 'Output SARIF v2.1 format to file or stdout')
34
38
  .option('--csv [file]', 'Output CSV format to file or stdout')
35
39
  .option('--score-only', 'Output only the risk score (0-10)')
@@ -48,7 +52,7 @@ program
48
52
  const fetchOptions = {
49
53
  cacheDir: options.cacheDir,
50
54
  cacheTTL: parseInt(options.cacheTtl || '604800'),
51
- cacheMaxSize: parseInt(options.cacheSize || '1000000000')
55
+ cacheMaxSize: parseInt(options.cacheSize || '1000000000'),
52
56
  };
53
57
 
54
58
  if (!target && !options.file && !options.vsix) {
@@ -61,28 +65,41 @@ program
61
65
  const vsixFindings = await vsixScan(options.vsix);
62
66
  const { saveScan } = await import('../backend/db.js');
63
67
  const scanId = await saveScan(options.vsix, 'latest', vsixFindings);
64
- const vsixOutput = JSON.stringify({ scanId, findings: vsixFindings, blocked: false, riskScore: 0, vsix: true }, null, 2);
68
+ const vsixOutput = JSON.stringify(
69
+ { scanId, findings: vsixFindings, blocked: false, riskScore: 0, vsix: true },
70
+ null,
71
+ 2
72
+ );
65
73
  console.log(vsixOutput);
66
74
  return;
67
75
  }
68
76
 
69
77
  const policy = options.policy
70
- ? await import('../backend/policy.js').then(m => m.loadPolicy(options.policy))
78
+ ? await import('../backend/policy.js').then((m) => m.loadPolicy(options.policy))
71
79
  : null;
72
80
 
73
81
  if (policy) {
74
82
  const { isAllowed } = await import('../backend/policy.js');
75
83
  if (target && isAllowed(target, policy)) {
76
- console.log(JSON.stringify({ scanId: null, findings: [], skipped: true, reason: `Package '${target}' is in policy allowlist` }));
84
+ console.log(
85
+ JSON.stringify({
86
+ scanId: null,
87
+ findings: [],
88
+ skipped: true,
89
+ reason: `Package '${target}' is in policy allowlist`,
90
+ })
91
+ );
77
92
  return;
78
93
  }
79
94
  }
80
95
 
81
96
  const { pkgJson, jsFiles, allFiles, tmpDir, meta } = options.file
82
- ? await import('../backend/fetch.js').then(m => m.scanLocalTarball(options.file))
83
- : await import('../backend/fetch.js').then(m => m.fetchPackage(target, fetchOptions));
97
+ ? await import('../backend/fetch.js').then((m) => m.scanLocalTarball(options.file))
98
+ : await import('../backend/fetch.js').then((m) => m.fetchPackage(target, fetchOptions));
84
99
  const pkgName = target || pkgJson.name || 'unknown';
85
- const findings = await import('../backend/detectors/index.js').then(m => m.runAll(pkgJson, jsFiles, meta, allFiles));
100
+ const findings = await import('../backend/detectors/index.js').then((m) =>
101
+ m.runAll(pkgJson, jsFiles, meta, allFiles)
102
+ );
86
103
  let vsixFindings = [];
87
104
  if (options.vsix) {
88
105
  const { vsixScan } = await import('../backend/vsix-scan/index.js');
@@ -107,13 +124,17 @@ program
107
124
 
108
125
  if (options.scoreOnly) {
109
126
  console.log(riskScore);
110
- import('../backend/fetch.js').then(m => m.cleanup(tmpDir));
127
+ import('../backend/fetch.js').then((m) => m.cleanup(tmpDir));
111
128
  return;
112
129
  }
113
130
 
114
131
  if (options.sarif) {
115
132
  const { generateSARIF } = await import('../backend/report.js');
116
- const scan = { package_name: pkgName, version: pkgJson.version || 'latest', findings: outputFindings };
133
+ const scan = {
134
+ package_name: pkgName,
135
+ version: pkgJson.version || 'latest',
136
+ findings: outputFindings,
137
+ };
117
138
  const sarifOutput = generateSARIF(scan);
118
139
  if (options.sarif === true || !options.sarif) {
119
140
  console.log(sarifOutput);
@@ -124,7 +145,11 @@ program
124
145
  }
125
146
  } else if (options.csv) {
126
147
  const { generateCSV } = await import('../backend/report.js');
127
- const scan = { package_name: pkgName, version: pkgJson.version || 'latest', findings: outputFindings };
148
+ const scan = {
149
+ package_name: pkgName,
150
+ version: pkgJson.version || 'latest',
151
+ findings: outputFindings,
152
+ };
128
153
  const csvOutput = generateCSV([scan]);
129
154
  if (options.csv === true || !options.csv) {
130
155
  console.log(csvOutput);
@@ -136,14 +161,20 @@ program
136
161
  } else if (options.sbom) {
137
162
  const { generateSBOM } = await import('../backend/sbom.js');
138
163
  const pkg = { name: pkgName, version: pkgJson.version || 'latest' };
139
- const sbom = generateSBOM(pkg, outputFindings, options.sbom === true ? 'json' : options.sbom);
164
+ const sbom = generateSBOM(
165
+ pkg,
166
+ outputFindings,
167
+ options.sbom === true ? 'json' : options.sbom
168
+ );
140
169
  console.log(sbom);
141
170
  } else {
142
- console.log(JSON.stringify({scanId, findings: outputFindings, blocked, riskScore}, null, 2));
171
+ console.log(
172
+ JSON.stringify({ scanId, findings: outputFindings, blocked, riskScore }, null, 2)
173
+ );
143
174
  }
144
175
 
145
176
  if (options.auditLog) {
146
- const { writeFileSync, appendFileSync } = await import('fs');
177
+ const { writeFileSync: _writeFileSync, appendFileSync } = await import('fs');
147
178
  const entry = {
148
179
  timestamp: new Date().toISOString(),
149
180
  command: `scan ${target || options.file}`,
@@ -151,7 +182,7 @@ program
151
182
  version: pkgJson.version || 'latest',
152
183
  riskScore,
153
184
  findingsCount: outputFindings.length,
154
- exitCode: 0
185
+ exitCode: 0,
155
186
  };
156
187
  appendFileSync(options.auditLog, JSON.stringify(entry) + '\n');
157
188
  }
@@ -164,14 +195,16 @@ program
164
195
  if (options.failOn !== 'none') {
165
196
  const severityLevels = { low: 1, medium: 2, high: 3, critical: 4 };
166
197
  const failLevel = severityLevels[options.failOn] || 0;
167
- const hasBlockingFindings = outputFindings.some(f => (severityLevels[f.severity] || 0) >= failLevel);
198
+ const hasBlockingFindings = outputFindings.some(
199
+ (f) => (severityLevels[f.severity] || 0) >= failLevel
200
+ );
168
201
  if (hasBlockingFindings) {
169
202
  console.error(`Fail: findings with severity >= ${options.failOn} detected`);
170
203
  process.exit(1);
171
204
  }
172
205
  }
173
206
 
174
- import('../backend/fetch.js').then(m => m.cleanup(tmpDir));
207
+ import('../backend/fetch.js').then((m) => m.cleanup(tmpDir));
175
208
  } catch (e) {
176
209
  console.error(e.message);
177
210
  process.exit(1);
@@ -182,7 +215,11 @@ program
182
215
  .command('scan-lockfile')
183
216
  .description('Scan package lockfile (npm/yarn/pnpm)')
184
217
  .option('-f, --file <path>', 'lockfile path', 'package-lock.json')
185
- .option('--fail-on <level>', 'Exit with code 1 if findings >= level (low|medium|high|critical)', 'none')
218
+ .option(
219
+ '--fail-on <level>',
220
+ 'Exit with code 1 if findings >= level (low|medium|high|critical)',
221
+ 'none'
222
+ )
186
223
  .option('--csv [file]', 'Output CSV format to file or stdout')
187
224
  .option('--sarif [file]', 'Output SARIF v2.1 format to file or stdout')
188
225
  .option('--watch', 'Watch for changes and re-scan automatically')
@@ -197,41 +234,62 @@ program
197
234
  const isWatch = options.watch;
198
235
  const isMonorepo = options.monorepo;
199
236
 
200
- if (isWatch) {
201
- if (isMonorepo) {
202
- const lockfiles = await glob('**/{package-lock.json,yarn.lock,pnpm-lock.yaml}', { ignore: 'node_modules/**' });
237
+ if (isWatch) {
238
+ if (isMonorepo) {
239
+ const lockfiles = await glob('**/{package-lock.json,yarn.lock,pnpm-lock.yaml}', {
240
+ ignore: 'node_modules/**',
241
+ });
203
242
 
243
+ if (!silent) {
244
+ console.log(
245
+ `\x1b[32m✔\x1b[0m npm-scan watch mode (monorepo) — ${lockfiles.length} lockfiles`
246
+ );
247
+ console.log(` Debounce: ${debounce}ms | Press Ctrl+C to stop\n`);
248
+ }
249
+
250
+ const timers = {};
251
+ for (const lf of lockfiles) {
204
252
  if (!silent) {
205
- console.log(`\x1b[32m✔\x1b[0m npm-scan watch mode (monorepo) — ${lockfiles.length} lockfiles`);
206
- console.log(` Debounce: ${debounce}ms | Press Ctrl+C to stop\n`);
253
+ console.log(` Watching: ${lf}`);
207
254
  }
255
+ const _watcher = watch(lf, (eventType) => {
256
+ if (eventType !== 'change') {
257
+ return;
258
+ }
259
+ clearTimeout(timers[lf]);
260
+ timers[lf] = setTimeout(() => {
261
+ if (!silent) {
262
+ console.log(
263
+ `\n\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${lf} changed — scanning...`
264
+ );
265
+ }
266
+ const lockType = lf.includes('yarn') ? '--yarn' : lf.includes('pnpm') ? '--pnpm' : '';
267
+ try {
268
+ execSync(
269
+ `node cli/cli.js scan-lockfile -f "${lf}" --fail-on ${options.failOn || 'high'} --silent ${lockType}`,
270
+ { stdio: silent ? 'ignore' : 'inherit' }
271
+ );
272
+ } catch {
273
+ /* ignore */
274
+ }
275
+ }, debounce);
276
+ });
277
+ }
208
278
 
209
- let timers = {};
210
- for (const lf of lockfiles) {
211
- if (!silent) console.log(` Watching: ${lf}`);
212
- const watcher = watch(lf, (eventType) => {
213
- if (eventType !== 'change') return;
214
- clearTimeout(timers[lf]);
215
- timers[lf] = setTimeout(() => {
216
- if (!silent) {
217
- console.log(`\n\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${lf} changed — scanning...`);
218
- }
219
- const lockType = lf.includes('yarn') ? '--yarn' : lf.includes('pnpm') ? '--pnpm' : '';
220
- try {
221
- execSync(`node cli/cli.js scan-lockfile -f "${lf}" --fail-on ${options.failOn || 'high'} --silent ${lockType}`, { stdio: silent ? 'ignore' : 'inherit' });
222
- } catch (e) {}
223
- }, debounce);
224
- });
279
+ process.on('SIGINT', () => {
280
+ if (!silent) {
281
+ console.log('\n\x1b[33m✖\x1b[0m Stopped.');
225
282
  }
226
-
227
- process.on('SIGINT', () => {
228
- if (!silent) console.log('\n\x1b[33m✖\x1b[0m Stopped.');
229
- process.exit(0);
230
- });
283
+ process.exit(0);
284
+ });
231
285
  } else {
232
286
  const lockfile = options.file;
233
287
  let lastSize = 0;
234
- try { lastSize = statSync(lockfile).size; } catch {}
288
+ try {
289
+ lastSize = statSync(lockfile).size;
290
+ } catch {
291
+ /* ignore */
292
+ }
235
293
 
236
294
  if (!silent) {
237
295
  console.log(`\x1b[32m✔\x1b[0m npm-scan watch mode — ${lockfile}`);
@@ -239,19 +297,34 @@ program
239
297
  }
240
298
 
241
299
  const watcher = watch(lockfile, (eventType) => {
242
- if (eventType !== 'change') return;
300
+ if (eventType !== 'change') {
301
+ return;
302
+ }
243
303
  const size = statSync(lockfile).size;
244
- if (size === lastSize) return;
304
+ if (size === lastSize) {
305
+ return;
306
+ }
245
307
  lastSize = size;
246
- if (!silent) console.log(`\n\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${lockfile} changed — rescanning...`);
308
+ if (!silent) {
309
+ console.log(
310
+ `\n\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${lockfile} changed — rescanning...`
311
+ );
312
+ }
247
313
  try {
248
- execSync(`node cli/cli.js scan-lockfile --fail-on ${options.failOn || 'high'} --silent`, { stdio: silent ? 'ignore' : 'inherit' });
249
- } catch (e) {}
314
+ execSync(
315
+ `node cli/cli.js scan-lockfile --fail-on ${options.failOn || 'high'} --silent`,
316
+ { stdio: silent ? 'ignore' : 'inherit' }
317
+ );
318
+ } catch {
319
+ /* ignore */
320
+ }
250
321
  });
251
322
 
252
323
  process.on('SIGINT', () => {
253
324
  watcher.close();
254
- if (!silent) console.log('\n\x1b[33m✖\x1b[0m Stopped.');
325
+ if (!silent) {
326
+ console.log('\n\x1b[33m✖\x1b[0m Stopped.');
327
+ }
255
328
  process.exit(0);
256
329
  });
257
330
  }
@@ -260,9 +333,13 @@ program
260
333
  try {
261
334
  const { parseLockfile, generateLockfileReport } = await import('../backend/lockfile.js');
262
335
 
263
- if (!silent) console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
336
+ if (!silent) {
337
+ console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
338
+ }
264
339
 
265
- const lockfileData = parseLockfile(lockfile, { autoDetect: !options.yarn && !options.pnpm });
340
+ const lockfileData = parseLockfile(lockfile, {
341
+ autoDetect: !options.yarn && !options.pnpm,
342
+ });
266
343
  const results = generateLockfileReport(lockfileData);
267
344
 
268
345
  if (!silent) {
@@ -271,8 +348,17 @@ program
271
348
  if (results.findings.length > 0) {
272
349
  console.log(`\n\x1b[31m🔴\x1b[0m ${results.findings.length} finding(s) found:\n`);
273
350
  for (const f of results.findings) {
274
- const color = f.severity === 'critical' ? '\x1b[31m' : f.severity === 'high' ? '\x1b[91m' : f.severity === 'medium' ? '\x1b[33m' : '\x1b[32m';
275
- console.log(` ${color}${f.severity.toUpperCase().padEnd(8)}\x1b[0m ${f.id}: ${f.title}`);
351
+ const color =
352
+ f.severity === 'critical'
353
+ ? '\x1b[31m'
354
+ : f.severity === 'high'
355
+ ? '\x1b[91m'
356
+ : f.severity === 'medium'
357
+ ? '\x1b[33m'
358
+ : '\x1b[32m';
359
+ console.log(
360
+ ` ${color}${f.severity.toUpperCase().padEnd(8)}\x1b[0m ${f.id}: ${f.title}`
361
+ );
276
362
  console.log(` ${f.description}`);
277
363
  }
278
364
  } else {
@@ -287,9 +373,11 @@ program
287
373
  const failOn = options.failOn || 'none';
288
374
  if (failOn !== 'none') {
289
375
  const weights = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
290
- const maxWeight = Math.max(...results.findings.map(f => weights[f.severity] || 0));
376
+ const maxWeight = Math.max(...results.findings.map((f) => weights[f.severity] || 0));
291
377
  const failThreshold = weights[failOn] || 0;
292
- if (maxWeight >= failThreshold) process.exit(1);
378
+ if (maxWeight >= failThreshold) {
379
+ process.exit(1);
380
+ }
293
381
  }
294
382
  }
295
383
  } catch (e) {
@@ -300,7 +388,7 @@ program
300
388
  });
301
389
 
302
390
  program
303
- .command('report')
391
+ .command('report')
304
392
  .description('Generate report')
305
393
  .option('-i, --id <id>', 'Scan ID')
306
394
  .option('--sbom [format]', 'SBOM format (json/xml/spdx)')
@@ -339,7 +427,7 @@ program
339
427
  const { generatePDF } = await import('../backend/pdf.js');
340
428
  const pdfBytes = await generatePDF(scan ? [scan] : []);
341
429
  const outPath = options.output || `${pkgName}-${options.id}-report.pdf`;
342
- await import('fs').then(m => m.writeFileSync(outPath, pdfBytes));
430
+ await import('fs').then((m) => m.writeFileSync(outPath, pdfBytes));
343
431
  console.log(`PDF report written to ${outPath}`);
344
432
  } else if (options.text) {
345
433
  const { generateText } = await import('../backend/report.js');
@@ -361,7 +449,9 @@ program
361
449
  }
362
450
  } else {
363
451
  const scans = await getRecentScans();
364
- const scansWithFindings = await Promise.all(scans.map(async s => ({ ...s, findings: await getFindings(s.id) })));
452
+ const scansWithFindings = await Promise.all(
453
+ scans.map(async (s) => ({ ...s, findings: await getFindings(s.id) }))
454
+ );
365
455
 
366
456
  if (options.siem) {
367
457
  requirePremium('siem', licenseKey);
@@ -377,7 +467,7 @@ program
377
467
  const pdfBytes = await generatePDF(scansWithFindings);
378
468
  const date = new Date().toISOString().slice(0, 10);
379
469
  const outPath = options.output || `npm-scan-report-${date}.pdf`;
380
- await import('fs').then(m => m.writeFileSync(outPath, pdfBytes));
470
+ await import('fs').then((m) => m.writeFileSync(outPath, pdfBytes));
381
471
  console.log(`PDF report written to ${outPath}`);
382
472
  } else if (options.text) {
383
473
  const { generateText } = await import('../backend/report.js');
@@ -417,7 +507,7 @@ program
417
507
 
418
508
  if (req.url === '/scan' && req.method === 'POST') {
419
509
  let body = '';
420
- req.on('data', chunk => body += chunk);
510
+ req.on('data', (chunk) => (body += chunk));
421
511
  req.on('end', async () => {
422
512
  try {
423
513
  const { package: pkg, options: scanOpts } = JSON.parse(body);
@@ -456,4 +546,4 @@ program
456
546
  });
457
547
  });
458
548
 
459
- program.parse();
549
+ program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lateos/npm-scan",
3
- "version": "1.0.0",
3
+ "version": "1.1.1",
4
4
  "description": "Production-grade npm supply chain vulnerability scanner. Detects 100% of 3 real May 2026 supply chain campaigns (dependency confusion, obfuscation, impersonation) with 0% false positive rate on top 1,000 npm packages.",
5
5
  "main": "backend/index.js",
6
6
  "bin": {
@@ -25,7 +25,11 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "dev": "node cli/cli.js",
28
- "lint": "echo 'Lint stub'",
28
+ "lint": "eslint .",
29
+ "lint:fix": "eslint . --fix",
30
+ "format": "prettier --write .",
31
+ "format:check": "prettier --check .",
32
+ "validate": "npm run lint && npm run format:check && npm test",
29
33
  "test": "node --test",
30
34
  "test:coverage": "node --experimental-test-coverage --test",
31
35
  "test:verbose": "node --test --test-reporter spec",
@@ -68,7 +72,12 @@
68
72
  "tar": "^7.5.15"
69
73
  },
70
74
  "devDependencies": {
75
+ "@eslint/js": "^9.39.4",
76
+ "eslint": "^9.39.4",
77
+ "eslint-config-prettier": "^10.1.8",
78
+ "eslint-plugin-prettier": "^5.5.6",
71
79
  "husky": "^9.1.7",
72
- "lint-staged": "^16.4.0"
80
+ "lint-staged": "^16.4.0",
81
+ "prettier": "^3.8.3"
73
82
  }
74
83
  }