@lateos/npm-scan 0.13.0 → 0.14.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.de.md +2 -2
- package/README.fr.md +2 -2
- package/README.ja.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/backend/lockfile.js +260 -32
- package/cli/cli.js +63 -53
- package/deploy/helm/npm-scan/values.byoc.yaml +1 -1
- package/deploy/helm/npm-scan/values.yaml +1 -1
- package/package.json +4 -2
- package/test/fixtures/lockfiles/pnpm-lock.yaml +118 -0
- package/test/fixtures/lockfiles/yarn.lock +103 -0
package/README.de.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](package.json)
|
|
12
12
|
[](https://github.com/lateos-ai/npm-scan)
|
|
13
13
|
[](https://github.com/lateos-ai/npm-scan)
|
|
14
|
-
[](https://hub.docker.com/r/lateos/npm-scan)
|
|
15
15
|
[](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
|
|
16
16
|
|
|
17
17
|
**Moderne Lieferkettensicherheit für das npm-Ökosystem.**
|
|
@@ -100,7 +100,7 @@ npx @lateos/npm-scan scan commander
|
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
# Einmaligen Scan pullen und ausführen — kein Node.js oder npm erforderlich
|
|
103
|
-
docker run --rm
|
|
103
|
+
docker run --rm lateos/npm-scan:cli scan lodash
|
|
104
104
|
|
|
105
105
|
# Vollständige Pipeline mit persistentem Speicher und Compose
|
|
106
106
|
docker compose --profile pipeline up -d
|
package/README.fr.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](package.json)
|
|
12
12
|
[](https://github.com/lateos-ai/npm-scan)
|
|
13
13
|
[](https://github.com/lateos-ai/npm-scan)
|
|
14
|
-
[](https://hub.docker.com/r/lateos/npm-scan)
|
|
15
15
|
[](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
|
|
16
16
|
|
|
17
17
|
**Sécurité moderne de la chaîne d'approvisionnement pour l'écosystème npm.**
|
|
@@ -100,7 +100,7 @@ npx @lateos/npm-scan scan commander
|
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
# Tirez et exécutez un scan unique — pas de Node.js ni npm requis
|
|
103
|
-
docker run --rm
|
|
103
|
+
docker run --rm lateos/npm-scan:cli scan lodash
|
|
104
104
|
|
|
105
105
|
# Pipeline complet avec stockage persistant et Compose
|
|
106
106
|
docker compose --profile pipeline up -d
|
package/README.ja.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](package.json)
|
|
12
12
|
[](https://github.com/lateos-ai/npm-scan)
|
|
13
13
|
[](https://github.com/lateos-ai/npm-scan)
|
|
14
|
-
[](https://hub.docker.com/r/lateos/npm-scan)
|
|
15
15
|
[](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
|
|
16
16
|
|
|
17
17
|
**npmエコシステムのためのモダンなサプライチェーンセキュリティ。**
|
|
@@ -100,7 +100,7 @@ npx @lateos/npm-scan scan commander
|
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
# 単一スキャンをプルして実行 — Node.jsやnpmは不要
|
|
103
|
-
docker run --rm
|
|
103
|
+
docker run --rm lateos/npm-scan:cli scan lodash
|
|
104
104
|
|
|
105
105
|
# 永続ストレージとComposeを使用した完全パイプライン
|
|
106
106
|
docker compose --profile pipeline up -d
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](package.json)
|
|
6
6
|
[](https://github.com/lateos-ai/npm-scan)
|
|
7
7
|
[](https://github.com/lateos-ai/npm-scan)
|
|
8
|
-
[](https://hub.docker.com/r/lateos/npm-scan)
|
|
9
9
|
[](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
|
|
10
10
|
|
|
11
11
|
[](https://github.com/lateos-ai/npm-scan/blob/main/README.zh.md)
|
|
@@ -102,7 +102,7 @@ npx @lateos/npm-scan scan commander
|
|
|
102
102
|
|
|
103
103
|
```bash
|
|
104
104
|
# Pull and run a single scan — no Node.js or npm required
|
|
105
|
-
docker run --rm
|
|
105
|
+
docker run --rm lateos/npm-scan:cli scan lodash
|
|
106
106
|
|
|
107
107
|
# Full pipeline with persistent storage and Compose
|
|
108
108
|
docker compose --profile pipeline up -d
|
package/README.zh.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
[](package.json)
|
|
12
12
|
[](https://github.com/lateos-ai/npm-scan)
|
|
13
13
|
[](https://github.com/lateos-ai/npm-scan)
|
|
14
|
-
[](https://hub.docker.com/r/lateos/npm-scan)
|
|
15
15
|
[](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
|
|
16
16
|
|
|
17
17
|
**适用于 npm 生态系统的现代供应链安全工具。**
|
|
@@ -100,7 +100,7 @@ npx @lateos/npm-scan scan commander
|
|
|
100
100
|
|
|
101
101
|
```bash
|
|
102
102
|
# 拉取并运行单次扫描 — 无需 Node.js 或 npm
|
|
103
|
-
docker run --rm
|
|
103
|
+
docker run --rm lateos/npm-scan:cli scan lodash
|
|
104
104
|
|
|
105
105
|
# 使用持久化存储和 Compose 的完整流水线
|
|
106
106
|
docker compose --profile pipeline up -d
|
package/backend/lockfile.js
CHANGED
|
@@ -1,47 +1,265 @@
|
|
|
1
1
|
import { readFileSync } from 'fs';
|
|
2
2
|
import { resolve, dirname } from 'path';
|
|
3
|
-
import
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
4
|
|
|
5
|
-
export function parseLockfile(filePath) {
|
|
5
|
+
export function parseLockfile(filePath, options = {}) {
|
|
6
|
+
const { autoDetect = false } = options;
|
|
6
7
|
try {
|
|
7
8
|
const content = readFileSync(filePath, 'utf8');
|
|
8
|
-
const
|
|
9
|
-
const packages = [];
|
|
9
|
+
const ext = filePath.split('.').pop().toLowerCase();
|
|
10
10
|
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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.startsWith('resolved ')) {
|
|
130
|
+
const rMatch = bodyTrim.match(/^resolved ['"]([^'"]+)['"]/);
|
|
131
|
+
if (rMatch) {
|
|
132
|
+
resolved = rMatch[1];
|
|
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) {
|
|
15
158
|
packages.push({
|
|
16
159
|
name,
|
|
17
|
-
version:
|
|
18
|
-
resolved
|
|
19
|
-
integrity
|
|
20
|
-
path:
|
|
21
|
-
peerDeps:
|
|
22
|
-
dev
|
|
23
|
-
optional
|
|
24
|
-
scripts:
|
|
25
|
-
dependencies
|
|
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
|
|
26
170
|
});
|
|
27
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];
|
|
28
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
|
+
}
|
|
29
209
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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}`;
|
|
40
227
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
+
}
|
|
44
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
|
+
};
|
|
45
263
|
}
|
|
46
264
|
|
|
47
265
|
export function checkMaliciousPatterns(pkg) {
|
|
@@ -95,8 +313,8 @@ export function analyzeDependencyGraph(lockfileData) {
|
|
|
95
313
|
}
|
|
96
314
|
}
|
|
97
315
|
|
|
98
|
-
if (pkg.dependencies && Object.keys(pkg.dependencies).length > 5) {
|
|
99
|
-
const transitiveCount =
|
|
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;
|
|
100
318
|
if (transitiveCount > 3) {
|
|
101
319
|
findings.push({
|
|
102
320
|
id: 'ATK-011',
|
|
@@ -107,6 +325,16 @@ export function analyzeDependencyGraph(lockfileData) {
|
|
|
107
325
|
});
|
|
108
326
|
}
|
|
109
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
|
+
}
|
|
110
338
|
}
|
|
111
339
|
|
|
112
340
|
return findings;
|
package/cli/cli.js
CHANGED
|
@@ -163,7 +163,7 @@ program
|
|
|
163
163
|
|
|
164
164
|
program
|
|
165
165
|
.command('scan-lockfile')
|
|
166
|
-
.description('Scan package
|
|
166
|
+
.description('Scan package lockfile (npm/yarn/pnpm)')
|
|
167
167
|
.option('-f, --file <path>', 'lockfile path', 'package-lock.json')
|
|
168
168
|
.option('--fail-on <level>', 'Exit with code 1 if findings >= level (low|medium|high|critical)', 'none')
|
|
169
169
|
.option('--csv [file]', 'Output CSV format to file or stdout')
|
|
@@ -171,43 +171,46 @@ program
|
|
|
171
171
|
.option('--watch', 'Watch for changes and re-scan automatically')
|
|
172
172
|
.option('--debounce <ms>', 'Debounce delay in ms before rescanning (default: 1000)', '1000')
|
|
173
173
|
.option('--silent', 'Suppress stdout output (useful for piping)')
|
|
174
|
-
.option('--monorepo', 'Scan all
|
|
174
|
+
.option('--monorepo', 'Scan all lockfiles in workspace (auto-detect type)')
|
|
175
|
+
.option('--yarn', 'Force yarn.lock format')
|
|
176
|
+
.option('--pnpm', 'Force pnpm-lock.yaml format')
|
|
175
177
|
.action(async (options) => {
|
|
176
178
|
const silent = options.silent;
|
|
177
179
|
const debounce = parseInt(options.debounce, 10) || 1000;
|
|
178
180
|
const isWatch = options.watch;
|
|
179
181
|
const isMonorepo = options.monorepo;
|
|
180
182
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
183
|
+
if (isWatch) {
|
|
184
|
+
if (isMonorepo) {
|
|
185
|
+
const lockfiles = await glob('**/{package-lock.json,yarn.lock,pnpm-lock.yaml}', { ignore: 'node_modules/**' });
|
|
184
186
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
if (!silent) {
|
|
188
|
+
console.log(`\x1b[32m✔\x1b[0m npm-scan watch mode (monorepo) — ${lockfiles.length} lockfiles`);
|
|
189
|
+
console.log(` Debounce: ${debounce}ms | Press Ctrl+C to stop\n`);
|
|
190
|
+
}
|
|
189
191
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
192
|
+
let timers = {};
|
|
193
|
+
for (const lf of lockfiles) {
|
|
194
|
+
if (!silent) console.log(` Watching: ${lf}`);
|
|
195
|
+
const watcher = watch(lf, (eventType) => {
|
|
196
|
+
if (eventType !== 'change') return;
|
|
197
|
+
clearTimeout(timers[lf]);
|
|
198
|
+
timers[lf] = setTimeout(() => {
|
|
199
|
+
if (!silent) {
|
|
200
|
+
console.log(`\n\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${lf} changed — scanning...`);
|
|
201
|
+
}
|
|
202
|
+
const lockType = lf.includes('yarn') ? '--yarn' : lf.includes('pnpm') ? '--pnpm' : '';
|
|
203
|
+
try {
|
|
204
|
+
execSync(`node cli/cli.js scan-lockfile -f "${lf}" --fail-on ${options.failOn || 'high'} --silent ${lockType}`, { stdio: silent ? 'ignore' : 'inherit' });
|
|
205
|
+
} catch (e) {}
|
|
206
|
+
}, debounce);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
206
209
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
210
|
+
process.on('SIGINT', () => {
|
|
211
|
+
if (!silent) console.log('\n\x1b[33m✖\x1b[0m Stopped.');
|
|
212
|
+
process.exit(0);
|
|
213
|
+
});
|
|
211
214
|
} else {
|
|
212
215
|
const lockfile = options.file;
|
|
213
216
|
let lastSize = 0;
|
|
@@ -237,37 +240,44 @@ program
|
|
|
237
240
|
}
|
|
238
241
|
} else {
|
|
239
242
|
const lockfile = options.file;
|
|
240
|
-
|
|
243
|
+
try {
|
|
244
|
+
const { parseLockfile, generateLockfileReport } = await import('../backend/lockfile.js');
|
|
241
245
|
|
|
242
|
-
|
|
246
|
+
if (!silent) console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
|
|
243
247
|
|
|
244
|
-
|
|
245
|
-
|
|
248
|
+
const lockfileData = parseLockfile(lockfile, { autoDetect: !options.yarn && !options.pnpm });
|
|
249
|
+
const results = generateLockfileReport(lockfileData);
|
|
246
250
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
251
|
+
if (!silent) {
|
|
252
|
+
console.log(` Total deps: ${results.totalDependencies}`);
|
|
253
|
+
console.log(` Lockfile version: ${results.lockfileVersion}`);
|
|
254
|
+
if (results.findings.length > 0) {
|
|
255
|
+
console.log(`\n\x1b[31m🔴\x1b[0m ${results.findings.length} finding(s) found:\n`);
|
|
256
|
+
for (const f of results.findings) {
|
|
257
|
+
const color = f.severity === 'critical' ? '\x1b[31m' : f.severity === 'high' ? '\x1b[91m' : f.severity === 'medium' ? '\x1b[33m' : '\x1b[32m';
|
|
258
|
+
console.log(` ${color}${f.severity.toUpperCase().padEnd(8)}\x1b[0m ${f.id}: ${f.title}`);
|
|
259
|
+
console.log(` ${f.description}`);
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
console.log(`\n\x1b[32m✔\x1b[0m No threats found.`);
|
|
256
263
|
}
|
|
257
|
-
|
|
258
|
-
console.log(`\n\x1b[32m✔\x1b[0m No threats found.`);
|
|
264
|
+
console.log(`\n\x1b[36mRisk Score: ${results.riskScore}/10\x1b[0m`);
|
|
259
265
|
}
|
|
260
|
-
console.log(`\n\x1b[36mRisk Score: ${results.riskScore}/10\x1b[0m`);
|
|
261
|
-
}
|
|
262
266
|
|
|
263
|
-
|
|
267
|
+
console.log(JSON.stringify(results, null, 2));
|
|
264
268
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
269
|
+
if (results.findings.length > 0) {
|
|
270
|
+
const failOn = options.failOn || 'none';
|
|
271
|
+
if (failOn !== 'none') {
|
|
272
|
+
const weights = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
|
|
273
|
+
const maxWeight = Math.max(...results.findings.map(f => weights[f.severity] || 0));
|
|
274
|
+
const failThreshold = weights[failOn] || 0;
|
|
275
|
+
if (maxWeight >= failThreshold) process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (e) {
|
|
279
|
+
console.error(`Error: ${e.message}`);
|
|
280
|
+
process.exit(1);
|
|
271
281
|
}
|
|
272
282
|
}
|
|
273
283
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
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": {
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
"corpus": "node tests/corpus/run.js"
|
|
35
35
|
},
|
|
36
36
|
"lint-staged": {
|
|
37
|
-
"**/package{,-lock}.json": "node cli/cli.js scan-lockfile --fail-on high"
|
|
37
|
+
"**/package{,-lock}.json": "node cli/cli.js scan-lockfile --fail-on high",
|
|
38
|
+
"**/yarn.lock": "node cli/cli.js scan-lockfile --fail-on high --yarn",
|
|
39
|
+
"**/pnpm-lock.yaml": "node cli/cli.js scan-lockfile --fail-on high --pnpm"
|
|
38
40
|
},
|
|
39
41
|
"publishConfig": {
|
|
40
42
|
"access": "public"
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
lockfileVersion: "6.0"
|
|
2
|
+
|
|
3
|
+
importers:
|
|
4
|
+
.:
|
|
5
|
+
dependencies:
|
|
6
|
+
lodash: "^4.17.21"
|
|
7
|
+
axios: "^1.6.0"
|
|
8
|
+
devDependencies:
|
|
9
|
+
"@babel/core": "^7.23.0"
|
|
10
|
+
optionalDependencies:
|
|
11
|
+
chalk: "^5.3.0"
|
|
12
|
+
peerDependencies:
|
|
13
|
+
react: ">=16"
|
|
14
|
+
|
|
15
|
+
packages:
|
|
16
|
+
"/lodash@4.17.21":
|
|
17
|
+
resolution:
|
|
18
|
+
url: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
|
19
|
+
sha512: X2xvyqwsdd456789abcdefghijk
|
|
20
|
+
dev: false
|
|
21
|
+
optional: false
|
|
22
|
+
dependencies: {}
|
|
23
|
+
|
|
24
|
+
"/axios@1.6.8":
|
|
25
|
+
resolution:
|
|
26
|
+
url: "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz"
|
|
27
|
+
sha512: j2xvyqwsdd456789abcdef
|
|
28
|
+
dev: false
|
|
29
|
+
optional: false
|
|
30
|
+
dependencies:
|
|
31
|
+
form-data: "4.0.0"
|
|
32
|
+
proxy-from-env: "1.1.0"
|
|
33
|
+
|
|
34
|
+
"/reakt@18.2.0":
|
|
35
|
+
resolution:
|
|
36
|
+
url: "https://registry.yarnpkg.com/reakt/-/reakt-18.2.0.tgz"
|
|
37
|
+
sha512: abcdefghijk123456789
|
|
38
|
+
dev: false
|
|
39
|
+
optional: true
|
|
40
|
+
dependencies: []
|
|
41
|
+
|
|
42
|
+
"/@babel/core@7.23.9":
|
|
43
|
+
resolution:
|
|
44
|
+
url: "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz"
|
|
45
|
+
sha512: k2yVyqwsdd456789abcdefghij
|
|
46
|
+
dev: true
|
|
47
|
+
optional: false
|
|
48
|
+
dependencies:
|
|
49
|
+
"@babel/generator": "7.23.6"
|
|
50
|
+
"@babel/parser": "7.23.9"
|
|
51
|
+
"@babel/traverse": "7.23.9"
|
|
52
|
+
"@babel/types": "7.23.9"
|
|
53
|
+
convert-source-map: "2.0.0"
|
|
54
|
+
debug: "4.1.0"
|
|
55
|
+
gensync: "1.0.0-beta.2"
|
|
56
|
+
json5: "2.2.3"
|
|
57
|
+
semver: "6.3.1"
|
|
58
|
+
|
|
59
|
+
"/@babel/generator@7.23.6":
|
|
60
|
+
resolution:
|
|
61
|
+
url: "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz"
|
|
62
|
+
sha512: abcdefghijk12345678abcdef
|
|
63
|
+
dev: false
|
|
64
|
+
optional: false
|
|
65
|
+
dependencies:
|
|
66
|
+
"@babel/types": "7.23.6"
|
|
67
|
+
"@jridgewell/gen-mapping": "0.3.2"
|
|
68
|
+
"@jridgewell/trace-mapping": "0.3.17"
|
|
69
|
+
jsesc: "2.5.1"
|
|
70
|
+
|
|
71
|
+
"/expres@4.18.2":
|
|
72
|
+
resolution:
|
|
73
|
+
url: "https://registry.npmjs.org/expres-4.18.2.tgz"
|
|
74
|
+
sha512: abcdefghijk12345678
|
|
75
|
+
dev: false
|
|
76
|
+
optional: false
|
|
77
|
+
dependencies:
|
|
78
|
+
accepts: "1.3.8"
|
|
79
|
+
array-flatten: "1.1.1"
|
|
80
|
+
body-parser: "1.20.2"
|
|
81
|
+
content-disposition: "0.5.4"
|
|
82
|
+
content-type: "1.0.5"
|
|
83
|
+
cookie: "0.5.0"
|
|
84
|
+
cookie-signature: "1.0.6"
|
|
85
|
+
debug: "2.6.9"
|
|
86
|
+
depd: "2.0.0"
|
|
87
|
+
encodeurl: "1.0.2"
|
|
88
|
+
escape-html: "1.0.3"
|
|
89
|
+
etag: "1.8.1"
|
|
90
|
+
finalhandler: "1.2.0"
|
|
91
|
+
fresh: "0.5.2"
|
|
92
|
+
http-errors: "2.0.0"
|
|
93
|
+
merge-descriptors: "1.0.1"
|
|
94
|
+
methods: "1.1.2"
|
|
95
|
+
on-finished: "2.4.1"
|
|
96
|
+
parseurl: "1.3.3"
|
|
97
|
+
path-to-regexp: "0.1.7"
|
|
98
|
+
proxy-addr: "2.0.7"
|
|
99
|
+
qs: "6.11.0"
|
|
100
|
+
range-parser: "1.2.1"
|
|
101
|
+
safe-buffer: "5.2.1"
|
|
102
|
+
send: "0.18.0"
|
|
103
|
+
serve-static: "1.15.0"
|
|
104
|
+
setprototypeof: "1.2.0"
|
|
105
|
+
statuses: "2.0.1"
|
|
106
|
+
type-is: "1.6.18"
|
|
107
|
+
utils-merge: "1.0.1"
|
|
108
|
+
vary: "1.1.2"
|
|
109
|
+
|
|
110
|
+
"/my-scope-plugin@1.0.0":
|
|
111
|
+
resolution:
|
|
112
|
+
url: "https://registry.npmjs.org/my-scope-plugin/-/my-scope-plugin-1.0.0.tgz"
|
|
113
|
+
sha512: defghijk123456789abcdef
|
|
114
|
+
dev: false
|
|
115
|
+
optional: false
|
|
116
|
+
dependencies:
|
|
117
|
+
lodash: "4.17.21"
|
|
118
|
+
axios: "1.6.8"
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
lodash@^4.17.21:
|
|
2
|
+
version "4.17.21"
|
|
3
|
+
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
|
4
|
+
integrity sha512-Vythumb
|
|
5
|
+
dependencies: {}
|
|
6
|
+
dev false
|
|
7
|
+
optional false
|
|
8
|
+
|
|
9
|
+
axios@^1.6.0:
|
|
10
|
+
version "1.6.8"
|
|
11
|
+
resolved "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz"
|
|
12
|
+
integrity sha512-j2xvyqwsdd456789abcdef
|
|
13
|
+
dependencies:
|
|
14
|
+
form-data "4.0.0"
|
|
15
|
+
proxy-from-env "1.1.0"
|
|
16
|
+
dev false
|
|
17
|
+
optional false
|
|
18
|
+
|
|
19
|
+
"@babel/core@^7.23.0", "@babel/core@^7.23.9":
|
|
20
|
+
version "7.23.9"
|
|
21
|
+
resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz"
|
|
22
|
+
integrity sha512-5q+M1iEJCOrGJs9NxzG3p3z7w2cJK/QuoRoI2pOJhtcNQjl9y7w6w4At5ZQHZdwqd+5N5G1lULu7I6pXVBw==
|
|
23
|
+
dependencies:
|
|
24
|
+
"@babel/generator" "^7.23.6"
|
|
25
|
+
"@babel/parser" "^7.23.9"
|
|
26
|
+
"@babel/traverse" "^7.23.9"
|
|
27
|
+
"@babel/types" "^7.23.9"
|
|
28
|
+
convert-source-map "^2.0.0"
|
|
29
|
+
debug "^4.1.0"
|
|
30
|
+
gensync "^1.0.0-beta.2"
|
|
31
|
+
json5 "^2.2.3"
|
|
32
|
+
semver "^6.3.1"
|
|
33
|
+
dev true
|
|
34
|
+
optional false
|
|
35
|
+
|
|
36
|
+
"@babel/generator@^7.23.6", "@babel/generator@^7.23.9":
|
|
37
|
+
version "7.23.6"
|
|
38
|
+
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz"
|
|
39
|
+
integrity sha512-56bfx9G1AJAFDl5QuK6t7MTCW3CBi7J8j+GxJJPvZ7L1f4P2FG8f9dBiH8Hg4U5Gcb6Bi4Y8DQ8x0j8b1QE8w==
|
|
40
|
+
dependencies:
|
|
41
|
+
"@babel/types" "^7.23.6"
|
|
42
|
+
"@jridgewell/gen-mapping" "^0.3.2"
|
|
43
|
+
"@jridgewell/trace-mapping" "^0.3.17"
|
|
44
|
+
jsesc "^2.5.1"
|
|
45
|
+
dev false
|
|
46
|
+
optional false
|
|
47
|
+
|
|
48
|
+
reakt@^18.2.0, reakt@^18.2.0::version=18.2.0:
|
|
49
|
+
version "18.2.0"
|
|
50
|
+
resolved "https://registry.yarnpkg.com/reakt/-/reakt-18.2.0.tgz"
|
|
51
|
+
integrity sha512-abcdabcd1234defghi
|
|
52
|
+
dependencies: []
|
|
53
|
+
dev false
|
|
54
|
+
optional true
|
|
55
|
+
|
|
56
|
+
"express@npm:expres@^4.18.2", expres@^4.18.2::version=4.18.2:
|
|
57
|
+
version "4.18.2"
|
|
58
|
+
resolved "https://registry.npmjs.org/expres-4.18.2.tgz"
|
|
59
|
+
integrity sha512-abcdabcd1234abcdefghi
|
|
60
|
+
dependencies:
|
|
61
|
+
accepts "~1.3.8"
|
|
62
|
+
array-flatten "1.1.1"
|
|
63
|
+
body-parser "1.20.2"
|
|
64
|
+
content-disposition "0.5.4"
|
|
65
|
+
content-type "~1.0.5"
|
|
66
|
+
cookie "0.5.0"
|
|
67
|
+
cookie-signature "1.0.6"
|
|
68
|
+
debug "2.6.9"
|
|
69
|
+
depd "2.0.0"
|
|
70
|
+
encodeurl "~1.0.2"
|
|
71
|
+
escape-html "~1.0.3"
|
|
72
|
+
etag "~1.8.1"
|
|
73
|
+
finalhandler "1.2.0"
|
|
74
|
+
fresh "0.5.2"
|
|
75
|
+
http-errors "2.0.0"
|
|
76
|
+
merge-descriptors "1.0.1"
|
|
77
|
+
methods "~1.1.2"
|
|
78
|
+
on-finished "2.4.1"
|
|
79
|
+
parseurl "~1.3.3"
|
|
80
|
+
path-to-regexp "0.1.7"
|
|
81
|
+
proxy-addr "~2.0.7"
|
|
82
|
+
qs "6.11.0"
|
|
83
|
+
range-parser "~1.2.1"
|
|
84
|
+
safe-buffer "5.2.1"
|
|
85
|
+
send "0.18.0"
|
|
86
|
+
serve-static "1.15.0"
|
|
87
|
+
setprototypeof "1.2.0"
|
|
88
|
+
statuses "2.0.1"
|
|
89
|
+
type-is "~1.6.18"
|
|
90
|
+
utils-merge "1.0.1"
|
|
91
|
+
vary "~1.1.2"
|
|
92
|
+
dev false
|
|
93
|
+
optional false
|
|
94
|
+
|
|
95
|
+
"my-scope-plugin@npm:my-scope-plugin@^1.0.0", my-scope-plugin@^1.0.0::version=1.0.0:
|
|
96
|
+
version "1.0.0"
|
|
97
|
+
resolved "https://registry.npmjs.org/my-scope-plugin-1.0.0.tgz"
|
|
98
|
+
integrity sha512-abcdefghijk123456789abcdef
|
|
99
|
+
dependencies:
|
|
100
|
+
lodash "^4.17.21"
|
|
101
|
+
axios "^1.6.0"
|
|
102
|
+
dev false
|
|
103
|
+
optional false
|