@lateos/npm-scan 0.12.3 → 0.13.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.
- package/backend/lockfile.js +152 -0
- package/cli/cli.js +40 -1
- package/package.json +1 -1
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
export function parseLockfile(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
const content = readFileSync(filePath, 'utf8');
|
|
8
|
+
const lockfile = JSON.parse(content);
|
|
9
|
+
const packages = [];
|
|
10
|
+
|
|
11
|
+
if (lockfile.packages) {
|
|
12
|
+
for (const [key, pkg] of Object.entries(lockfile.packages)) {
|
|
13
|
+
if (key === '') continue;
|
|
14
|
+
const name = pkg.name || key.replace(/^node_modules\//, '').replace(/^[^/]+\//, '');
|
|
15
|
+
packages.push({
|
|
16
|
+
name,
|
|
17
|
+
version: pkg.version || 'unknown',
|
|
18
|
+
resolved: pkg.resolved || '',
|
|
19
|
+
integrity: pkg.integrity || '',
|
|
20
|
+
path: key,
|
|
21
|
+
peerDeps: pkg.peerDependencies || {},
|
|
22
|
+
dev: pkg.dev || false,
|
|
23
|
+
optional: pkg.optional || false,
|
|
24
|
+
scripts: pkg.scripts || {},
|
|
25
|
+
dependencies: pkg.dependencies || {}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rootDeps = lockfile.packages?.['node_modules/'] || {};
|
|
31
|
+
return {
|
|
32
|
+
version: lockfile.lockfileVersion,
|
|
33
|
+
packages,
|
|
34
|
+
root: {
|
|
35
|
+
name: rootDeps.name || 'unknown',
|
|
36
|
+
version: rootDeps.version || 'unknown',
|
|
37
|
+
dependencies: rootDeps.dependencies || {},
|
|
38
|
+
devDependencies: rootDeps.devDependencies || {},
|
|
39
|
+
peerDependencies: rootDeps.peerDependencies || {}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new Error(`Failed to parse lockfile: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkMaliciousPatterns(pkg) {
|
|
48
|
+
const findings = [];
|
|
49
|
+
const name = pkg.name?.toLowerCase() || '';
|
|
50
|
+
|
|
51
|
+
const typosquatPatterns = [
|
|
52
|
+
/^(lodash|lodahs|lodash-js|lodashexe)$/,
|
|
53
|
+
/^(axios|axio|ax10s|ax1os)$/,
|
|
54
|
+
/^(react|reakt|reackt|r3act)$/,
|
|
55
|
+
/^(express|expres|expresjs|exress)$/,
|
|
56
|
+
/^(vue|vu3|vujs|vuejs)$/,
|
|
57
|
+
/^(webpack|webpak|webpackjs)$/,
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const pattern of typosquatPatterns) {
|
|
61
|
+
if (pattern.test(name)) {
|
|
62
|
+
findings.push({
|
|
63
|
+
id: 'ATK-007',
|
|
64
|
+
severity: 'high',
|
|
65
|
+
title: 'Typosquat detected',
|
|
66
|
+
description: `Package name "${pkg.name}" is similar to popular packages`,
|
|
67
|
+
evidence: `similar to ${pattern.source}`
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return findings;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function analyzeDependencyGraph(lockfileData) {
|
|
76
|
+
const findings = [];
|
|
77
|
+
const pkgMap = new Map();
|
|
78
|
+
|
|
79
|
+
for (const pkg of lockfileData.packages) {
|
|
80
|
+
pkgMap.set(pkg.name, pkg);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
for (const pkg of lockfileData.packages) {
|
|
84
|
+
if (pkg.peerDeps && Object.keys(pkg.peerDeps).length > 0) {
|
|
85
|
+
for (const [peerName, peerVersion] of Object.entries(pkg.peerDeps)) {
|
|
86
|
+
if (peerName.includes('plugin') || peerName.includes('hook') || peerName.includes('ext')) {
|
|
87
|
+
findings.push({
|
|
88
|
+
id: 'ATK-011',
|
|
89
|
+
severity: 'high',
|
|
90
|
+
title: 'Transitive propagation (worm)',
|
|
91
|
+
description: `Package "${pkg.name}" depends on peer "${peerName}@${peerVersion}" - potential worm propagation chain`,
|
|
92
|
+
evidence: `peer dep chain: ${pkg.name} -> ${peerName}`
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (pkg.dependencies && typeof pkg.dependencies === 'object' && Object.keys(pkg.dependencies).length > 5) {
|
|
99
|
+
const transitiveCount = Object.keys(pkg.dependencies).filter(k => k.includes('scope')).length;
|
|
100
|
+
if (transitiveCount > 3) {
|
|
101
|
+
findings.push({
|
|
102
|
+
id: 'ATK-011',
|
|
103
|
+
severity: 'medium',
|
|
104
|
+
title: 'Transitive propagation (worm)',
|
|
105
|
+
description: `Package "${pkg.name}" has excessive transitive dependencies (${transitiveCount} scoped)`,
|
|
106
|
+
evidence: `heavy transitive dep chain: ${pkg.name}`
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function generateLockfileReport(lockfileData) {
|
|
116
|
+
const total = lockfileData.packages.length;
|
|
117
|
+
const dev = lockfileData.packages.filter(p => p.dev).length;
|
|
118
|
+
const optional = lockfileData.packages.filter(p => p.optional).length;
|
|
119
|
+
|
|
120
|
+
const findings = [];
|
|
121
|
+
|
|
122
|
+
for (const pkg of lockfileData.packages) {
|
|
123
|
+
const maliciousFindings = checkMaliciousPatterns(pkg);
|
|
124
|
+
findings.push(...maliciousFindings);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
findings.push(...analyzeDependencyGraph(lockfileData));
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
scanId: Date.now(),
|
|
131
|
+
package: lockfileData.root.name,
|
|
132
|
+
version: lockfileData.root.version,
|
|
133
|
+
totalDependencies: total,
|
|
134
|
+
devDependencies: dev,
|
|
135
|
+
optionalDependencies: optional,
|
|
136
|
+
lockfileVersion: lockfileData.version,
|
|
137
|
+
findings,
|
|
138
|
+
riskScore: calculateRiskScore(findings)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function calculateRiskScore(findings) {
|
|
143
|
+
if (!findings.length) return '0.0';
|
|
144
|
+
const weights = { critical: 10, high: 7, medium: 4, low: 2, info: 0.5 };
|
|
145
|
+
const maxSeverity = findings.reduce((max, f) => {
|
|
146
|
+
const w = weights[f.severity] || 0;
|
|
147
|
+
return Math.max(max, w);
|
|
148
|
+
}, 0);
|
|
149
|
+
const countBonus = Math.min(findings.length * 0.3, 3);
|
|
150
|
+
const score = Math.min(maxSeverity + countBonus, 10);
|
|
151
|
+
return score.toFixed(1);
|
|
152
|
+
}
|
package/cli/cli.js
CHANGED
|
@@ -236,7 +236,46 @@ program
|
|
|
236
236
|
});
|
|
237
237
|
}
|
|
238
238
|
} else {
|
|
239
|
-
|
|
239
|
+
const lockfile = options.file;
|
|
240
|
+
try {
|
|
241
|
+
const { parseLockfile, generateLockfileReport } = await import('../backend/lockfile.js');
|
|
242
|
+
|
|
243
|
+
if (!silent) console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
|
|
244
|
+
|
|
245
|
+
const lockfileData = parseLockfile(lockfile);
|
|
246
|
+
const results = generateLockfileReport(lockfileData);
|
|
247
|
+
|
|
248
|
+
if (!silent) {
|
|
249
|
+
console.log(` Total deps: ${results.totalDependencies}`);
|
|
250
|
+
console.log(` Lockfile version: ${results.lockfileVersion}`);
|
|
251
|
+
if (results.findings.length > 0) {
|
|
252
|
+
console.log(`\n\x1b[31m🔴\x1b[0m ${results.findings.length} finding(s) found:\n`);
|
|
253
|
+
for (const f of results.findings) {
|
|
254
|
+
const color = f.severity === 'critical' ? '\x1b[31m' : f.severity === 'high' ? '\x1b[91m' : f.severity === 'medium' ? '\x1b[33m' : '\x1b[32m';
|
|
255
|
+
console.log(` ${color}${f.severity.toUpperCase().padEnd(8)}\x1b[0m ${f.id}: ${f.title}`);
|
|
256
|
+
console.log(` ${f.description}`);
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
console.log(`\n\x1b[32m✔\x1b[0m No threats found.`);
|
|
260
|
+
}
|
|
261
|
+
console.log(`\n\x1b[36mRisk Score: ${results.riskScore}/10\x1b[0m`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
console.log(JSON.stringify(results, null, 2));
|
|
265
|
+
|
|
266
|
+
if (results.findings.length > 0) {
|
|
267
|
+
const failOn = options.failOn || 'none';
|
|
268
|
+
if (failOn !== 'none') {
|
|
269
|
+
const weights = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
270
|
+
const maxWeight = Math.max(...results.findings.map(f => weights[f.severity] || 0));
|
|
271
|
+
const failThreshold = weights[failOn] || 0;
|
|
272
|
+
if (maxWeight >= failThreshold) process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch (e) {
|
|
276
|
+
console.error(`Error: ${e.message}`);
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
240
279
|
}
|
|
241
280
|
});
|
|
242
281
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"description": "Modern npm supply chain security scanner — detects obfuscated payloads, credential stealers, conditional triggers, sandbox evasion, and worm-like propagation. 11 attack types, SBOM, NIST/EU CRA compliance reporting.",
|
|
5
5
|
"main": "backend/index.js",
|
|
6
6
|
"bin": {
|