@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 CHANGED
@@ -11,7 +11,7 @@
11
11
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
12
  [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
- [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Flateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://github.com/lateos-ai/npm-scan/pkgs/container/npm-scan)
14
+ [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![Sigstore](https://img.shields.io/static/v1?label=Sigstore&message=Provenance&color=green&style=flat-square&logo=sigstore)](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 ghcr.io/lateos/npm-scan:cli scan lodash
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
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
12
  [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
- [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Flateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://github.com/lateos-ai/npm-scan/pkgs/container/npm-scan)
14
+ [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![Sigstore](https://img.shields.io/static/v1?label=Sigstore&message=Provenance&color=green&style=flat-square&logo=sigstore)](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 ghcr.io/lateos/npm-scan:cli scan lodash
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
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
12
  [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
- [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Flateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://github.com/lateos-ai/npm-scan/pkgs/container/npm-scan)
14
+ [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![Sigstore](https://img.shields.io/static/v1?label=Sigstore&message=Provenance&color=green&style=flat-square&logo=sigstore)](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 ghcr.io/lateos/npm-scan:cli scan lodash
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
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
6
6
  [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
7
7
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
8
- [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Flateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://github.com/lateos-ai/npm-scan/pkgs/container/npm-scan)
8
+ [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
9
9
  [![Sigstore](https://img.shields.io/static/v1?label=Sigstore&message=Provenance&color=green&style=flat-square&logo=sigstore)](https://github.com/lateos-ai/npm-scan/actions/workflows/publish.yml)
10
10
 
11
11
  [![中文](https://img.shields.io/badge/lang-zh--CN-red?style=flat-square)](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 ghcr.io/lateos/npm-scan:cli scan lodash
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
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen?style=flat-square)](package.json)
12
12
  [![Tests](https://img.shields.io/badge/tests-222%20passing-brightgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
13
13
  [![Coverage](https://img.shields.io/badge/coverage-85%25-yellowgreen?style=flat-square)](https://github.com/lateos-ai/npm-scan)
14
- [![Docker](https://img.shields.io/badge/docker-ghcr.io%2Flateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://github.com/lateos-ai/npm-scan/pkgs/container/npm-scan)
14
+ [![Docker](https://img.shields.io/badge/docker-lateos%2Fnpm--scan-2496ED?style=flat-square&logo=docker)](https://hub.docker.com/r/lateos/npm-scan)
15
15
  [![Sigstore](https://img.shields.io/static/v1?label=Sigstore&message=Provenance&color=green&style=flat-square&logo=sigstore)](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 ghcr.io/lateos/npm-scan:cli scan lodash
103
+ docker run --rm lateos/npm-scan:cli scan lodash
104
104
 
105
105
  # 使用持久化存储和 Compose 的完整流水线
106
106
  docker compose --profile pipeline up -d
@@ -1,47 +1,265 @@
1
1
  import { readFileSync } from 'fs';
2
2
  import { resolve, dirname } from 'path';
3
- import { fileURLToPath } from 'url';
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 lockfile = JSON.parse(content);
9
- const packages = [];
9
+ const ext = filePath.split('.').pop().toLowerCase();
10
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(/^[^/]+\//, '');
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: 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 || {}
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
- 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 || {}
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
- } catch (e) {
43
- throw new Error(`Failed to parse lockfile: ${e.message}`);
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 = [...pkg.dependencies].filter(([k]) => k.includes('scope')).length;
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-lock.json')
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 package-lock.json files in workspace')
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
- if (isWatch) {
182
- if (isMonorepo) {
183
- const lockfiles = await glob('**/package-lock.json', { ignore: 'node_modules/**' });
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
- if (!silent) {
186
- console.log(`\x1b[32m✔\x1b[0m npm-scan watch mode (monorepo) — ${lockfiles.length} lockfiles`);
187
- console.log(` Debounce: ${debounce}ms | Press Ctrl+C to stop\n`);
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
- let timers = {};
191
- for (const lf of lockfiles) {
192
- if (!silent) console.log(` Watching: ${lf}`);
193
- const watcher = watch(lf, (eventType) => {
194
- if (eventType !== 'change') return;
195
- clearTimeout(timers[lf]);
196
- timers[lf] = setTimeout(() => {
197
- if (!silent) {
198
- console.log(`\n\x1b[90m[${new Date().toLocaleTimeString()}]\x1b[0m ${lf} changed — scanning...`);
199
- }
200
- try {
201
- execSync(`node cli/cli.js scan-lockfile -f "${lf}" --fail-on ${options.failOn || 'high'} --silent`, { stdio: silent ? 'ignore' : 'inherit' });
202
- } catch (e) {}
203
- }, debounce);
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
- process.on('SIGINT', () => {
208
- if (!silent) console.log('\n\x1b[33m✖\x1b[0m Stopped.');
209
- process.exit(0);
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
- const { parseLockfile, generateLockfileReport } = await import('../backend/lockfile.js');
243
+ try {
244
+ const { parseLockfile, generateLockfileReport } = await import('../backend/lockfile.js');
241
245
 
242
- if (!silent) console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
246
+ if (!silent) console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
243
247
 
244
- const lockfileData = parseLockfile(lockfile);
245
- const results = generateLockfileReport(lockfileData);
248
+ const lockfileData = parseLockfile(lockfile, { autoDetect: !options.yarn && !options.pnpm });
249
+ const results = generateLockfileReport(lockfileData);
246
250
 
247
- if (!silent) {
248
- console.log(` Total deps: ${results.totalDependencies}`);
249
- console.log(` Lockfile version: ${results.lockfileVersion}`);
250
- if (results.findings.length > 0) {
251
- console.log(`\n\x1b[31m🔴\x1b[0m ${results.findings.length} finding(s) found:\n`);
252
- for (const f of results.findings) {
253
- const color = f.severity === 'critical' ? '\x1b[31m' : f.severity === 'high' ? '\x1b[91m' : f.severity === 'medium' ? '\x1b[33m' : '\x1b[32m';
254
- console.log(` ${color}${f.severity.toUpperCase().padEnd(8)}\x1b[0m ${f.id}: ${f.title}`);
255
- console.log(` ${f.description}`);
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
- } else {
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
- console.log(JSON.stringify(results, null, 2));
267
+ console.log(JSON.stringify(results, null, 2));
264
268
 
265
- if (results.findings.length > 0) {
266
- const failOn = options.failOn || 'none';
267
- const weights = { critical: 5, high: 4, medium: 3, low: 2, info: 1 };
268
- const maxWeight = Math.max(...results.findings.map(f => weights[f.severity] || 0));
269
- const failThreshold = weights[failOn] || 0;
270
- if (maxWeight >= failThreshold) process.exit(1);
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
  });
@@ -2,7 +2,7 @@
2
2
  # Deploy to your VPC: helm install -f values.byoc.yaml npm-scan ./
3
3
 
4
4
  image:
5
- repository: ghcr.io/lateos/npm-scan
5
+ repository: lateos/npm-scan
6
6
  tag: "1.0.0"
7
7
 
8
8
  premium:
@@ -2,7 +2,7 @@
2
2
  # Override per environment: helm install -f values-prod.yaml
3
3
 
4
4
  image:
5
- repository: ghcr.io/lateos/npm-scan
5
+ repository: lateos/npm-scan
6
6
  tag: latest
7
7
  pullPolicy: Always
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lateos/npm-scan",
3
- "version": "0.13.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