@lateos/npm-scan 0.13.1 → 0.15.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 +15 -6
- package/README.zh.md +2 -2
- package/backend/license.js +4 -0
- package/backend/lockfile.js +259 -31
- package/cli/cli.js +33 -30
- 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/npm-lock.json +69 -0
- package/test/fixtures/lockfiles/pnpm-lock.yaml +118 -0
- package/test/fixtures/lockfiles/yarn.lock +104 -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
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/@lateos/npm-scan)
|
|
4
4
|
[](LICENSING.md)
|
|
5
5
|
[](package.json)
|
|
6
|
-
[](https://github.com/lateos-ai/npm-scan)
|
|
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)
|
|
@@ -71,6 +71,7 @@ Attackers have moved past simple typosquatting. They now ship **obfuscated prein
|
|
|
71
71
|
| 🛡️ | **Zero telemetry** | No data leaves your machine. No cloud. No callbacks. |
|
|
72
72
|
| 💾 | **Local scan history** | SQLite-backed persistence, zero external dependencies |
|
|
73
73
|
| 🪝 | **Pre-commit hook** | Block threats before commit — one-liner install, scans `package-lock.json` changes |
|
|
74
|
+
| 📎 | **Yarn + pnpm support** | `scan-lockfile` parses `yarn.lock` and `pnpm-lock.yaml` alongside `package-lock.json` |
|
|
74
75
|
|
|
75
76
|
---
|
|
76
77
|
|
|
@@ -102,7 +103,7 @@ npx @lateos/npm-scan scan commander
|
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
105
|
# Pull and run a single scan — no Node.js or npm required
|
|
105
|
-
docker run --rm
|
|
106
|
+
docker run --rm lateos/npm-scan:cli scan lodash
|
|
106
107
|
|
|
107
108
|
# Full pipeline with persistent storage and Compose
|
|
108
109
|
docker compose --profile pipeline up -d
|
|
@@ -190,12 +191,16 @@ npm-scan scan --file path/to/malicious-package.tgz
|
|
|
190
191
|
### Scan a lockfile
|
|
191
192
|
|
|
192
193
|
```bash
|
|
193
|
-
# Scan the current project's dependencies
|
|
194
|
+
# Scan the current project's dependencies (auto-detects npm/yarn/pnpm)
|
|
194
195
|
npm-scan scan-lockfile
|
|
195
196
|
|
|
196
197
|
# Scan a specific lockfile
|
|
197
198
|
npm-scan scan-lockfile -f ./path/to/package-lock.json
|
|
198
199
|
|
|
200
|
+
# Scan yarn.lock or pnpm-lock.yaml
|
|
201
|
+
npm-scan scan-lockfile -f ./yarn.lock --yarn
|
|
202
|
+
npm-scan scan-lockfile -f ./pnpm-lock.yaml --pnpm
|
|
203
|
+
|
|
199
204
|
# Fail CI/CD on high or critical findings (exit code 1)
|
|
200
205
|
npm-scan scan-lockfile --fail-on high
|
|
201
206
|
|
|
@@ -211,7 +216,7 @@ npm-scan scan-lockfile --watch
|
|
|
211
216
|
# Watch with faster debounce (500ms) — great for dev workflows
|
|
212
217
|
npm-scan scan-lockfile --watch --debounce 500
|
|
213
218
|
|
|
214
|
-
# Watch monorepo (all
|
|
219
|
+
# Watch monorepo (all lockfiles — npm/yarn/pnpm — in workspace)
|
|
215
220
|
npm-scan scan-lockfile --watch --monorepo
|
|
216
221
|
|
|
217
222
|
# Output only risk score (0-10) for dashboards/thresholds
|
|
@@ -686,8 +691,12 @@ node --test test/detectors-corpus.test.js
|
|
|
686
691
|
- `test/detectors-corpus.test.js` — 33 malicious + 50 clean tarball integration (offline)
|
|
687
692
|
- `test/fetch.test.js` — tarball extraction, temp directory cleanup
|
|
688
693
|
- `test/policy-edge-cases.test.js` — edge cases in suppress, override, load validation
|
|
694
|
+
- `test/policy.test.js` — policy YAML/JSON load, apply, suppress, severity override tests
|
|
689
695
|
- `test/report-snapshots.test.js` — HTML/text/CRA/PDF format assertions
|
|
696
|
+
- `test/report.test.js` — SARIF, CSV, STIG, risk score format tests
|
|
697
|
+
- `test/lockfile.test.js` — npm/yarn/pnpm parser, auto-detect, ATK-007/011 lockfile tests
|
|
690
698
|
- `test/cli.test.js` — commander integration tests (help, version, scan, report, error handling)
|
|
699
|
+
- `test/cli-lockfile.test.js` — scan-lockfile CLI options, yarn/pnpm/monorepo/watch tests
|
|
691
700
|
|
|
692
701
|
### Need help?
|
|
693
702
|
|
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/license.js
CHANGED
|
@@ -78,6 +78,10 @@ export function validateLicense(key, feature = '*') {
|
|
|
78
78
|
|
|
79
79
|
export function isFeatureEnabled(feature, licenseKey = process.env.NPM_SCAN_LICENSE_KEY) {
|
|
80
80
|
try {
|
|
81
|
+
if (!licenseKey) {
|
|
82
|
+
const unlocked = feature === 'scan' || ALLOWED_UNLOCKED.includes(feature);
|
|
83
|
+
if (unlocked) return true;
|
|
84
|
+
}
|
|
81
85
|
validateLicense(licenseKey, feature);
|
|
82
86
|
return true;
|
|
83
87
|
} catch {
|
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.match(/^\s*resolved\s+(.+)/)) {
|
|
130
|
+
const rMatch = bodyTrim.match(/^\s*resolved\s+(.+)/);
|
|
131
|
+
if (rMatch) {
|
|
132
|
+
resolved = rMatch[1].trim().replace(/^['"]|['"]$/g, '');
|
|
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) {
|
|
@@ -96,7 +314,7 @@ export function analyzeDependencyGraph(lockfileData) {
|
|
|
96
314
|
}
|
|
97
315
|
|
|
98
316
|
if (pkg.dependencies && typeof pkg.dependencies === 'object' && Object.keys(pkg.dependencies).length > 5) {
|
|
99
|
-
const transitiveCount = Object.keys(pkg.dependencies).filter(k => k.includes('
|
|
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;
|
|
@@ -242,7 +245,7 @@ program
|
|
|
242
245
|
|
|
243
246
|
if (!silent) console.log(`\x1b[32m✔\x1b[0m Scanning lockfile: ${lockfile}`);
|
|
244
247
|
|
|
245
|
-
const lockfileData = parseLockfile(lockfile);
|
|
248
|
+
const lockfileData = parseLockfile(lockfile, { autoDetect: !options.yarn && !options.pnpm });
|
|
246
249
|
const results = generateLockfileReport(lockfileData);
|
|
247
250
|
|
|
248
251
|
if (!silent) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lateos/npm-scan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "test-project",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"packages": {
|
|
6
|
+
"": {
|
|
7
|
+
"name": "test-project",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"lodash": "^4.17.21",
|
|
11
|
+
"axios": "^1.6.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@babel/core": "^7.23.0"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"node_modules/lodash": {
|
|
18
|
+
"name": "lodash",
|
|
19
|
+
"version": "4.17.21",
|
|
20
|
+
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
|
21
|
+
"integrity": "sha512-v2kDEeDAnj4p1hhL6Ogrgu4BSWwg8cD2fRIouDAiqwu+iNl1IvyMex9jG9j8OpNp1zntnv/headququbit",
|
|
22
|
+
"dependencies": {}
|
|
23
|
+
},
|
|
24
|
+
"node_modules/axios": {
|
|
25
|
+
"name": "axios",
|
|
26
|
+
"version": "1.6.8",
|
|
27
|
+
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz",
|
|
28
|
+
"integrity": "sha512-j2xvyqwsdd456789abcdef",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"form-data": "4.0.0",
|
|
31
|
+
"proxy-from-env": "1.1.0"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"node_modules/axios/node_modules/form-data": {
|
|
35
|
+
"version": "4.0.0",
|
|
36
|
+
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
|
37
|
+
"integrity": "sha512-444567890123456"
|
|
38
|
+
},
|
|
39
|
+
"node_modules/@babel/core": {
|
|
40
|
+
"name": "@babel/core",
|
|
41
|
+
"version": "7.23.9",
|
|
42
|
+
"resolved": "https://registry.yarnpkg.com/@babel/core/-/core-7.23.9.tgz",
|
|
43
|
+
"integrity": "sha512-5q+M1iEJCOrGJs9NxzG3p3z7w2cJK/QuoRoI2pOJhtcNQjl9y7w6w4At5ZQHZdwqd+5N5G1lULu7I6pXVBw==",
|
|
44
|
+
"dev": true,
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@babel/generator": "^7.23.6",
|
|
47
|
+
"@babel/parser": "^7.23.9"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"node_modules/reakt": {
|
|
51
|
+
"name": "reakt",
|
|
52
|
+
"version": "18.2.0",
|
|
53
|
+
"resolved": "https://registry.yarnpkg.com/reakt/-/reakt-18.2.0.tgz",
|
|
54
|
+
"integrity": "sha-abcdabcd1234defghi",
|
|
55
|
+
"optional": true,
|
|
56
|
+
"dependencies": {}
|
|
57
|
+
},
|
|
58
|
+
"node_modules/express": {
|
|
59
|
+
"name": "express",
|
|
60
|
+
"version": "4.18.2",
|
|
61
|
+
"resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz",
|
|
62
|
+
"integrity": "sha512-abcdabcd1234abcdefghi",
|
|
63
|
+
"dependencies": {
|
|
64
|
+
"accepts": "~1.3.8",
|
|
65
|
+
"body-parser": "1.20.2"
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -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,104 @@
|
|
|
1
|
+
lodash@^4.17.21:
|
|
2
|
+
version "4.17.21"
|
|
3
|
+
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz"
|
|
4
|
+
integrity sha512-Vythumb
|
|
5
|
+
dependencies: {}
|
|
6
|
+
dev false
|
|
7
|
+
optional true
|
|
8
|
+
|
|
9
|
+
axios@^1.6.0:
|
|
10
|
+
version "1.6.8"
|
|
11
|
+
resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.8.tgz"
|
|
12
|
+
integrity sha-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
|
+
rimraf "^3.0.2"
|
|
34
|
+
dev true
|
|
35
|
+
optional false
|
|
36
|
+
|
|
37
|
+
"@babel/generator@^7.23.6", "@babel/generator@^7.23.9":
|
|
38
|
+
version "7.23.6"
|
|
39
|
+
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz"
|
|
40
|
+
integrity sha512-56bfx9G1AJAFDl5QuK6t7MTCW3CBi7J8j+GxJJPvZ7L1f4P2FG8f9dBiH8Hg4U5Gcb6Bi4Y8DQ8x0j8b1QE8w==
|
|
41
|
+
dependencies:
|
|
42
|
+
"@babel/types" "^7.23.6"
|
|
43
|
+
"@jridgewell/gen-mapping" "^0.3.2"
|
|
44
|
+
"@jridgewell/trace-mapping" "^0.3.17"
|
|
45
|
+
jsesc "^2.5.1"
|
|
46
|
+
dev false
|
|
47
|
+
optional false
|
|
48
|
+
|
|
49
|
+
reakt@^18.2.0:
|
|
50
|
+
version "18.2.0"
|
|
51
|
+
resolved "https://registry.yarnpkg.com/reakt/-/reakt-18.2.0.tgz"
|
|
52
|
+
integrity sha512-abcdabcd1234defghi
|
|
53
|
+
dependencies: []
|
|
54
|
+
dev false
|
|
55
|
+
optional true
|
|
56
|
+
|
|
57
|
+
express@npm:expres@^4.18.2:
|
|
58
|
+
version "4.18.2"
|
|
59
|
+
resolved "https://registry.npmjs.org/expres-4.18.2.tgz"
|
|
60
|
+
integrity sha512-abcdabcd1234abcdefghi
|
|
61
|
+
dependencies:
|
|
62
|
+
accepts "~1.3.8"
|
|
63
|
+
array-flatten "1.1.1"
|
|
64
|
+
body-parser "1.20.2"
|
|
65
|
+
content-disposition "0.5.4"
|
|
66
|
+
content-type "~1.0.5"
|
|
67
|
+
cookie "0.5.0"
|
|
68
|
+
cookie-signature "1.0.6"
|
|
69
|
+
debug "2.6.9"
|
|
70
|
+
depd "2.0.0"
|
|
71
|
+
encodeurl "~1.0.2"
|
|
72
|
+
escape-html "~1.0.3"
|
|
73
|
+
etag "~1.8.1"
|
|
74
|
+
finalhandler "1.2.0"
|
|
75
|
+
fresh "0.5.2"
|
|
76
|
+
http-errors "2.0.0"
|
|
77
|
+
merge-descriptors "1.0.1"
|
|
78
|
+
methods "~1.1.2"
|
|
79
|
+
on-finished "2.4.1"
|
|
80
|
+
parseurl "~1.3.3"
|
|
81
|
+
path-to-regexp "0.1.7"
|
|
82
|
+
proxy-addr "~2.0.7"
|
|
83
|
+
qs "6.11.0"
|
|
84
|
+
range-parser "~1.2.1"
|
|
85
|
+
safe-buffer "5.2.1"
|
|
86
|
+
send "0.18.0"
|
|
87
|
+
serve-static "1.15.0"
|
|
88
|
+
setprototypeof "1.2.0"
|
|
89
|
+
statuses "2.0.1"
|
|
90
|
+
type-is "~1.6.18"
|
|
91
|
+
utils-merge "1.0.1"
|
|
92
|
+
vary "~1.1.2"
|
|
93
|
+
dev false
|
|
94
|
+
optional false
|
|
95
|
+
|
|
96
|
+
"my-scope-plugin@npm:my-scope-plugin@^1.0.0":
|
|
97
|
+
version "1.0.0"
|
|
98
|
+
resolved "https://registry.npmjs.org/my-scope-plugin-1.0.0.tgz"
|
|
99
|
+
integrity sha512-abcdefghijk123456789abcdef
|
|
100
|
+
dependencies:
|
|
101
|
+
lodash "^4.17.21"
|
|
102
|
+
axios "^1.6.0"
|
|
103
|
+
dev false
|
|
104
|
+
optional false
|