@lateos/npm-scan 1.0.0 → 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.
- package/README.md +864 -861
- package/backend/cra.js +113 -21
- package/backend/db.js +18 -10
- package/backend/detectors/atk-001-lifecycle.js +5 -5
- package/backend/detectors/atk-002-obfusc.js +126 -47
- package/backend/detectors/atk-003-creds.js +8 -4
- package/backend/detectors/atk-004-persist.js +3 -3
- package/backend/detectors/atk-005-exfil.js +8 -4
- package/backend/detectors/atk-006-depconf.js +3 -3
- package/backend/detectors/atk-007-typosquat.js +64 -10
- package/backend/detectors/atk-008-tarball-tamper.js +6 -6
- package/backend/detectors/atk-009-dormant-trigger.js +9 -5
- package/backend/detectors/atk-010-sandbox-evasion.js +25 -10
- package/backend/detectors/atk-011-transitive-prop.js +14 -13
- package/backend/detectors/axios-poisoning/d1-version-fingerprint.js +4 -4
- package/backend/detectors/axios-poisoning/d2-decoy-dep.js +5 -1
- package/backend/detectors/axios-poisoning/d3-postinstall-rat.js +64 -19
- package/backend/detectors/axios-poisoning/index.js +77 -60
- package/backend/detectors/config/thresholds.js +48 -3
- package/backend/detectors/cve-2026-48710-badhost/codePattern.js +26 -9
- package/backend/detectors/cve-2026-48710-badhost/findings.js +8 -4
- package/backend/detectors/cve-2026-48710-badhost/index.js +1 -1
- package/backend/detectors/cve-2026-48710-badhost/manifest.js +127 -39
- package/backend/detectors/cve-2026-48710-badhost/transitive.js +87 -28
- package/backend/detectors/hf-impersonation/index.js +94 -31
- package/backend/detectors/hf-impersonation/jaro-winkler.js +33 -12
- package/backend/detectors/hf-impersonation/known-orgs.js +15 -3
- package/backend/detectors/hf-impersonation/simhash.js +2 -2
- package/backend/detectors/index.js +181 -34
- package/backend/detectors/lib/ast-patterns.js +4 -1
- package/backend/detectors/lib/entropy-analyzer.js +12 -4
- package/backend/detectors/megalodon/d1-workflow-scan.js +40 -16
- package/backend/detectors/megalodon/d2-credential-harvest.js +12 -5
- package/backend/detectors/megalodon/d3-publish-velocity.js +17 -11
- package/backend/detectors/megalodon/d4-publisher-drift.js +48 -16
- package/backend/detectors/megalodon/d5-bot-commit-identity.js +1 -1
- package/backend/detectors/megalodon/d6-date-anachronism.js +1 -1
- package/backend/detectors/megalodon/index.js +35 -25
- package/backend/detectors/mini-shai-hulud/d1-burst-publish.js +3 -1
- package/backend/detectors/mini-shai-hulud/d2-sibling-compromise.js +22 -10
- package/backend/detectors/mini-shai-hulud/d3-slsa-mismatch.js +30 -10
- package/backend/detectors/mini-shai-hulud/d4-maintainer-anomaly.js +17 -13
- package/backend/detectors/mini-shai-hulud/d5-ioc-check.js +12 -4
- package/backend/detectors/mini-shai-hulud/d6-token-exfil.js +6 -2
- package/backend/detectors/mini-shai-hulud/index.js +63 -26
- package/backend/detectors/msh-supplement/d2-persistence.js +30 -12
- package/backend/detectors/msh-supplement/d3-geo-killswitch.js +20 -8
- package/backend/detectors/msh-supplement/d4-c2-deaddrop.js +19 -5
- package/backend/detectors/msh-supplement/index.js +78 -63
- package/backend/detectors/node-ipc-compromise/d1-version-blocklist.js +4 -2
- package/backend/detectors/node-ipc-compromise/d10-unauthorized-publisher.js +9 -5
- package/backend/detectors/node-ipc-compromise/d11-blast-radius.js +7 -3
- package/backend/detectors/node-ipc-compromise/d2-tarball-hash.js +9 -4
- package/backend/detectors/node-ipc-compromise/d3-cjs-payload-injection.js +7 -5
- package/backend/detectors/node-ipc-compromise/d4-injected-payload-hash.js +4 -2
- package/backend/detectors/node-ipc-compromise/d5-dns-c2-pattern.js +13 -10
- package/backend/detectors/node-ipc-compromise/d7-dns-txt-exfil.js +3 -1
- package/backend/detectors/node-ipc-compromise/d8-runtime-trigger.js +5 -2
- package/backend/detectors/node-ipc-compromise/index.js +21 -15
- package/backend/detectors/tier1-binary-embed.js +109 -41
- package/backend/detectors/tier1-cloud-imds.js +57 -37
- package/backend/detectors/tier1-encrypted-c2.js +198 -0
- package/backend/detectors/tier1-infostealer.js +121 -68
- package/backend/detectors/tier1-lifecycle-hook.js +63 -23
- package/backend/detectors/tier1-maintainer-compromise.js +157 -0
- package/backend/detectors/tier1-metadata-spoof.js +92 -42
- package/backend/detectors/tier1-multistage-postinstall.js +46 -19
- package/backend/detectors/tier1-obfuscation-heuristics.js +45 -17
- package/backend/detectors/tier1-self-propagation.js +115 -0
- package/backend/detectors/tier1-slsa-attestation.js +1 -1
- package/backend/detectors/tier1-transitive-deps.js +182 -0
- package/backend/detectors/tier1-typosquat.js +129 -50
- package/backend/detectors/tier1-version-anomaly.js +77 -41
- package/backend/detectors/tier1-version-confusion.js +79 -59
- package/backend/detectors/trapdoor/d1-campaign-marker.js +3 -1
- package/backend/detectors/trapdoor/d2-payload-fingerprint.js +1 -1
- package/backend/detectors/trapdoor/d3-publisher-blocklist.js +4 -3
- package/backend/detectors/trapdoor/d4-gists-exfil.js +4 -2
- package/backend/detectors/trapdoor/d5-ai-poisoning.js +5 -3
- package/backend/detectors/trapdoor/d6-lure-name.js +12 -7
- package/backend/detectors/trapdoor/d7-crypto-primitives.js +2 -2
- package/backend/detectors/trapdoor/d8-xor-key.js +7 -2
- package/backend/detectors/trapdoor/d9-cred-validation.js +4 -5
- package/backend/detectors/trapdoor/index.js +19 -14
- package/backend/detectors/typosquat-vpmdhaj/d1-maintainer.js +32 -8
- package/backend/detectors/typosquat-vpmdhaj/d2-preinstall-loader.js +5 -3
- package/backend/detectors/typosquat-vpmdhaj/d3-cred-exfil.js +34 -12
- package/backend/detectors/typosquat-vpmdhaj/index.js +78 -59
- package/backend/detectors.test.js +78 -19
- package/backend/fetch.js +37 -29
- package/backend/index.js +1 -1
- package/backend/license.js +20 -4
- package/backend/lockfile.js +60 -36
- package/backend/pdf.js +107 -28
- package/backend/policy.js +183 -56
- package/backend/provenance.js +28 -3
- package/backend/report.js +136 -70
- package/backend/sbom.js +33 -27
- package/backend/scripts/analyze-false-positives.js +14 -8
- package/backend/scripts/analyze-validation.js +27 -21
- package/backend/scripts/detect-false-positives.js +20 -10
- package/backend/scripts/fetch-top-packages.js +197 -49
- package/backend/scripts/validate-d10-d13.js +103 -0
- package/backend/scripts/validate-detectors.js +26 -17
- package/backend/siem/cef.js +23 -21
- package/backend/siem/ecs.js +3 -3
- package/backend/siem/index.js +1 -1
- package/backend/siem/qradar.js +3 -3
- package/backend/siem/sentinel.js +2 -2
- package/backend/tests-d5-enhanced.test.js +13 -12
- package/backend/tests-d6-version-anomaly.test.js +17 -8
- package/backend/tests-d6.test.js +24 -14
- package/backend/tests-d6c.test.js +27 -14
- package/backend/tests-d7-obfuscation.test.js +9 -12
- package/backend/tests.test.js +182 -83
- package/backend/vsix-scan/detectors/activation-event-risk.js +36 -19
- package/backend/vsix-scan/detectors/burst-publish.js +14 -7
- package/backend/vsix-scan/detectors/exfil-pattern.js +7 -3
- package/backend/vsix-scan/detectors/known-ioc.js +23 -8
- package/backend/vsix-scan/detectors/orphan-commit-fetch.js +11 -7
- package/backend/vsix-scan/detectors/publisher-anomaly.js +24 -10
- package/backend/vsix-scan/index.js +97 -41
- package/backend/vsix-scan/marketplace-client.js +29 -13
- package/cli/cli.js +154 -64
- 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(
|
|
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(
|
|
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(
|
|
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 =>
|
|
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 = {
|
|
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 = {
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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(
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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 {
|
|
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')
|
|
300
|
+
if (eventType !== 'change') {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
243
303
|
const size = statSync(lockfile).size;
|
|
244
|
-
if (size === lastSize)
|
|
304
|
+
if (size === lastSize) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
245
307
|
lastSize = size;
|
|
246
|
-
if (!silent)
|
|
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(
|
|
249
|
-
|
|
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)
|
|
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)
|
|
336
|
+
if (!silent) {
|
|
337
|
+
console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
|
|
338
|
+
}
|
|
264
339
|
|
|
265
|
-
const lockfileData = parseLockfile(lockfile, {
|
|
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 =
|
|
275
|
-
|
|
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)
|
|
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(
|
|
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.
|
|
3
|
+
"version": "1.1.0",
|
|
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": "
|
|
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
|
}
|