@knime/utils 1.10.1 → 1.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -0
- package/package.json +8 -2
- package/src/audit/README.md +118 -0
- package/src/audit/bulkAudit.js +671 -0
- package/src/audit/cli.js +117 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @knime/utils
|
|
2
2
|
|
|
3
|
+
## 1.11.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 7b602bf: update dompurify
|
|
8
|
+
|
|
9
|
+
## 1.11.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- ce7f504: Added a tool to provide audit for pnpm using the bulk advisory endpoint
|
|
14
|
+
|
|
3
15
|
## 1.10.1
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -18,6 +18,12 @@ To use it in your project, you can import it as follows:
|
|
|
18
18
|
import { capitalize } from "@knime/utils";
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
## Audit tool
|
|
22
|
+
|
|
23
|
+
This package also ships a CLI tool (`pnpm-bulk-audit`) that audits dependencies
|
|
24
|
+
using the npm bulk advisory API — a drop-in workaround while `pnpm audit` is
|
|
25
|
+
broken. See [src/audit/README.md](src/audit/README.md) for details.
|
|
26
|
+
|
|
21
27
|
# Join the Community!
|
|
22
28
|
|
|
23
29
|
- [KNIME Forum](https://forum.knime.com/)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@knime/utils",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.1",
|
|
4
4
|
"description": "Internal utility functions and constants shared across @knime packages.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"date-fns": "2.30.0",
|
|
16
16
|
"date-fns-tz": "2.0.0",
|
|
17
|
-
"dompurify": "^3.
|
|
17
|
+
"dompurify": "^3.4.7",
|
|
18
18
|
"filesize": "10.0.6"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
@@ -34,10 +34,16 @@
|
|
|
34
34
|
"CHANGELOG.md"
|
|
35
35
|
],
|
|
36
36
|
"sideEffects": false,
|
|
37
|
+
"bin": {
|
|
38
|
+
"pnpm-bulk-audit": "./src/audit/cli.js"
|
|
39
|
+
},
|
|
37
40
|
"exports": {
|
|
38
41
|
".": {
|
|
39
42
|
"import": "./src/index.ts"
|
|
40
43
|
},
|
|
44
|
+
"./audit": {
|
|
45
|
+
"import": "./src/audit/bulkAudit.js"
|
|
46
|
+
},
|
|
41
47
|
"./globals.d.ts": {
|
|
42
48
|
"import": "./src/config/globals.d.ts"
|
|
43
49
|
},
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# pnpm Bulk Audit
|
|
2
|
+
|
|
3
|
+
A workaround for `pnpm audit` failing with HTTP 410 after npm retired the legacy
|
|
4
|
+
audit endpoints. This tool calls the replacement
|
|
5
|
+
[bulk advisory endpoint](https://api-docs.npmjs.com/#tag/Audit) directly.
|
|
6
|
+
|
|
7
|
+
**Background:** In April 2026 npm retired `/-/npm/v1/security/audits` (and the
|
|
8
|
+
`/quick` variant). `pnpm audit` on all 10.x and early 11.x versions is broken
|
|
9
|
+
because it still calls the old endpoints. See
|
|
10
|
+
[pnpm/pnpm#11265](https://github.com/pnpm/pnpm/issues/11265). This tool bridges
|
|
11
|
+
the gap until pnpm ships a fix.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Add `@knime/utils` as a dev dependency (it is already present in the monorepo),
|
|
16
|
+
then call the binary directly:
|
|
17
|
+
|
|
18
|
+
```jsonc
|
|
19
|
+
// package.json
|
|
20
|
+
{
|
|
21
|
+
"scripts": {
|
|
22
|
+
"audit": "pnpm-bulk-audit",
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@knime/utils": "workspace:*",
|
|
26
|
+
},
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Then run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pnpm run audit
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## CLI options
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
Usage: pnpm-bulk-audit [options]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--lockfile <path> Path to pnpm-lock.yaml (default: ./pnpm-lock.yaml)
|
|
43
|
+
--registry <url> npm registry URL (default: https://registry.npmjs.org)
|
|
44
|
+
--severity <level> Minimum severity: low, moderate, high, critical (default: low)
|
|
45
|
+
--prod, -P Only report vulnerabilities in production dependencies
|
|
46
|
+
--json Output raw JSON
|
|
47
|
+
--no-ignore Skip reading ignoreCves/ignoreGhsas from config
|
|
48
|
+
--help, -h Show this help
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Prod vs dev classification
|
|
52
|
+
|
|
53
|
+
Each vulnerability is annotated with `[prod]` or `[dev]` based on whether the
|
|
54
|
+
affected package is reachable from any importer's `dependencies` (as opposed to
|
|
55
|
+
`devDependencies`). The classifier walks the full dependency graph in the
|
|
56
|
+
lockfile, following workspace links transitively.
|
|
57
|
+
|
|
58
|
+
Use `--prod` (or `-P`) to only report vulnerabilities in production
|
|
59
|
+
dependencies.
|
|
60
|
+
|
|
61
|
+
## Ignore lists
|
|
62
|
+
|
|
63
|
+
The tool reads `auditConfig.ignoreCves` and `auditConfig.ignoreGhsas` from two
|
|
64
|
+
places (both are merged):
|
|
65
|
+
|
|
66
|
+
### pnpm-workspace.yaml
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
auditConfig:
|
|
70
|
+
ignoreCves:
|
|
71
|
+
- CVE-2026-4800
|
|
72
|
+
ignoreGhsas:
|
|
73
|
+
- GHSA-78h3-pg4x-j8cv
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### package.json
|
|
77
|
+
|
|
78
|
+
```jsonc
|
|
79
|
+
{
|
|
80
|
+
"pnpm": {
|
|
81
|
+
"auditConfig": {
|
|
82
|
+
"ignoreCves": ["CVE-2026-4800"],
|
|
83
|
+
"ignoreGhsas": ["GHSA-78h3-pg4x-j8cv"],
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
**GHSA IDs** are matched directly from the advisory URL returned by the npm
|
|
90
|
+
registry.
|
|
91
|
+
**CVE IDs** require an extra lookup via the GitHub advisories API
|
|
92
|
+
(`api.github.com/advisories/{ghsa}`) because the npm bulk endpoint does not
|
|
93
|
+
include CVE identifiers in its response.
|
|
94
|
+
|
|
95
|
+
Use `--no-ignore` to skip all ignore-list processing.
|
|
96
|
+
|
|
97
|
+
## Programmatic usage
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
import { audit, formatAuditReport, readAuditConfig } from "@knime/utils/audit";
|
|
101
|
+
|
|
102
|
+
const config = readAuditConfig(".");
|
|
103
|
+
const result = await audit({
|
|
104
|
+
lockfilePath: "./pnpm-lock.yaml",
|
|
105
|
+
minSeverity: "high",
|
|
106
|
+
auditConfig: config,
|
|
107
|
+
});
|
|
108
|
+
console.log(formatAuditReport(result));
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## How it works
|
|
112
|
+
|
|
113
|
+
1. Parses the `packages:` section of `pnpm-lock.yaml` to extract every
|
|
114
|
+
`name@version` pair.
|
|
115
|
+
2. POSTs them as `{ "pkg": ["version", …] }` to the npm bulk advisory endpoint
|
|
116
|
+
(`/-/npm/v1/security/advisories/bulk`).
|
|
117
|
+
3. Filters the response by severity and ignore lists.
|
|
118
|
+
4. Exits with code 1 if any vulnerabilities remain, 0 otherwise.
|
|
@@ -0,0 +1,671 @@
|
|
|
1
|
+
/* eslint-disable no-console, n/no-process-env */
|
|
2
|
+
/**
|
|
3
|
+
* Audits packages from a pnpm-lock.yaml file using the npm bulk advisory API.
|
|
4
|
+
*
|
|
5
|
+
* The legacy npm audit endpoints (/-/npm/v1/security/audits) have been retired
|
|
6
|
+
* (HTTP 410). This module calls the replacement bulk advisory endpoint
|
|
7
|
+
* (/-/npm/v1/security/advisories/bulk) directly.
|
|
8
|
+
*
|
|
9
|
+
* See: https://api-docs.npmjs.com/#tag/Audit
|
|
10
|
+
* See: https://github.com/pnpm/pnpm/issues/11265
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { execSync } from "node:child_process";
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { resolve } from "node:path";
|
|
16
|
+
|
|
17
|
+
const SEVERITY_ORDER = ["low", "moderate", "high", "critical"];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve a GitHub API token from GITHUB_TOKEN env var or `gh auth token`.
|
|
21
|
+
* Returns null (with a warning) if neither is available.
|
|
22
|
+
*/
|
|
23
|
+
function getGitHubToken() {
|
|
24
|
+
if (process.env.GITHUB_TOKEN) {
|
|
25
|
+
return process.env.GITHUB_TOKEN;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
return execSync("gh auth token", { encoding: "utf-8" }).trim();
|
|
29
|
+
} catch {
|
|
30
|
+
// gh CLI not installed or not authenticated
|
|
31
|
+
}
|
|
32
|
+
console.warn(
|
|
33
|
+
"Warning: No GitHub token found (GITHUB_TOKEN env or `gh auth token`).\n" +
|
|
34
|
+
"GHSA→CVE resolution will use unauthenticated requests (60 req/hour limit).\n",
|
|
35
|
+
);
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Extract the GHSA ID from an advisory URL like
|
|
41
|
+
* `https://github.com/advisories/GHSA-r5fr-rjxr-66jc`.
|
|
42
|
+
*/
|
|
43
|
+
function extractGhsaId(url) {
|
|
44
|
+
const match = url.match(/(GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4})/i);
|
|
45
|
+
return match ? match[1] : null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Minimally parse a YAML value that is a string array. Handles lines like:
|
|
50
|
+
* - CVE-2026-4800
|
|
51
|
+
* - CVE-2026-2950 # comment
|
|
52
|
+
*/
|
|
53
|
+
function parseYamlStringArray(lines, startIdx, indent) {
|
|
54
|
+
const result = [];
|
|
55
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
56
|
+
const line = lines[i];
|
|
57
|
+
// Skip blank lines and comment-only lines at the expected indentation
|
|
58
|
+
if (/^\s*$/.test(line) || new RegExp(`^ {${indent}}#`).test(line)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Check if this line is an array item at the expected indentation
|
|
62
|
+
const itemMatch = line.match(new RegExp(`^ {${indent}}-\\s+(.+)`));
|
|
63
|
+
if (!itemMatch) {
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
// Strip inline comments
|
|
67
|
+
const value = itemMatch[1].replace(/#.*$/, "").trim();
|
|
68
|
+
if (value) {
|
|
69
|
+
result.push(value);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Read auditConfig from pnpm-workspace.yaml (top-level `auditConfig` key).
|
|
77
|
+
*/
|
|
78
|
+
function readAuditConfigFromWorkspace(workspaceDir) {
|
|
79
|
+
const config = {};
|
|
80
|
+
const wsPath = resolve(workspaceDir, "pnpm-workspace.yaml");
|
|
81
|
+
if (!existsSync(wsPath)) {
|
|
82
|
+
return config;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const content = readFileSync(wsPath, "utf-8");
|
|
86
|
+
const lines = content.split("\n");
|
|
87
|
+
|
|
88
|
+
let inAuditConfig = false;
|
|
89
|
+
for (let i = 0; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i];
|
|
91
|
+
|
|
92
|
+
if (/^auditConfig:\s*$/.test(line)) {
|
|
93
|
+
inAuditConfig = true;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
// New top-level key ends auditConfig
|
|
97
|
+
if (inAuditConfig && /^\S/.test(line)) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
if (!inAuditConfig) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (/^ {2}ignoreCves:\s*$/.test(line)) {
|
|
105
|
+
// eslint-disable-next-line no-magic-numbers
|
|
106
|
+
config.ignoreCves = parseYamlStringArray(lines, i + 1, 4);
|
|
107
|
+
}
|
|
108
|
+
if (/^ {2}ignoreGhsas:\s*$/.test(line)) {
|
|
109
|
+
// eslint-disable-next-line no-magic-numbers
|
|
110
|
+
config.ignoreGhsas = parseYamlStringArray(lines, i + 1, 4);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return config;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Read auditConfig from package.json (`pnpm.auditConfig` key).
|
|
119
|
+
*/
|
|
120
|
+
function readAuditConfigFromPackageJson(workspaceDir) {
|
|
121
|
+
const config = {};
|
|
122
|
+
const pkgPath = resolve(workspaceDir, "package.json");
|
|
123
|
+
if (!existsSync(pkgPath)) {
|
|
124
|
+
return config;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
129
|
+
const auditConfig = pkg?.pnpm?.auditConfig;
|
|
130
|
+
if (auditConfig) {
|
|
131
|
+
if (Array.isArray(auditConfig.ignoreCves)) {
|
|
132
|
+
config.ignoreCves = auditConfig.ignoreCves;
|
|
133
|
+
}
|
|
134
|
+
if (Array.isArray(auditConfig.ignoreGhsas)) {
|
|
135
|
+
config.ignoreGhsas = auditConfig.ignoreGhsas;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// ignore parse errors
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return config;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Merge auditConfig from pnpm-workspace.yaml and package.json.
|
|
147
|
+
* Both sources are combined (union of ignore lists).
|
|
148
|
+
*/
|
|
149
|
+
export function readAuditConfig(workspaceDir) {
|
|
150
|
+
const fromWorkspace = readAuditConfigFromWorkspace(workspaceDir);
|
|
151
|
+
const fromPkg = readAuditConfigFromPackageJson(workspaceDir);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
ignoreCves: [
|
|
155
|
+
...(fromWorkspace.ignoreCves ?? []),
|
|
156
|
+
...(fromPkg.ignoreCves ?? []),
|
|
157
|
+
],
|
|
158
|
+
ignoreGhsas: [
|
|
159
|
+
...(fromWorkspace.ignoreGhsas ?? []),
|
|
160
|
+
...(fromPkg.ignoreGhsas ?? []),
|
|
161
|
+
],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Resolve GHSA IDs to their associated CVE IDs using the GitHub advisories API.
|
|
167
|
+
* Returns a map from GHSA ID to an array of CVE IDs.
|
|
168
|
+
*/
|
|
169
|
+
async function resolveGhsaToCves(ghsaIds) {
|
|
170
|
+
const map = new Map();
|
|
171
|
+
if (ghsaIds.length === 0) {
|
|
172
|
+
return map;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const token = getGitHubToken();
|
|
176
|
+
const headers = { Accept: "application/vnd.github+json" };
|
|
177
|
+
if (token) {
|
|
178
|
+
headers.Authorization = `Bearer ${token}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fetch in parallel, but limit concurrency to avoid rate limits
|
|
182
|
+
const batchSize = 10;
|
|
183
|
+
for (let i = 0; i < ghsaIds.length; i += batchSize) {
|
|
184
|
+
const batch = ghsaIds.slice(i, i + batchSize);
|
|
185
|
+
const results = await Promise.all(
|
|
186
|
+
batch.map(async (ghsaId) => {
|
|
187
|
+
try {
|
|
188
|
+
const resp = await fetch(
|
|
189
|
+
`https://api.github.com/advisories/${ghsaId}`,
|
|
190
|
+
{ headers },
|
|
191
|
+
);
|
|
192
|
+
if (!resp.ok) {
|
|
193
|
+
console.warn(
|
|
194
|
+
`Warning: GitHub advisory lookup for ${ghsaId} failed (HTTP ${resp.status}). CVE ignore rules may not apply.`,
|
|
195
|
+
);
|
|
196
|
+
return { ghsaId, cves: [] };
|
|
197
|
+
}
|
|
198
|
+
const data = await resp.json();
|
|
199
|
+
const cves = (data.identifiers ?? [])
|
|
200
|
+
.filter((id) => id.type === "CVE")
|
|
201
|
+
.map((id) => id.value);
|
|
202
|
+
return { ghsaId, cves };
|
|
203
|
+
} catch (err) {
|
|
204
|
+
console.warn(
|
|
205
|
+
`Warning: GitHub advisory lookup for ${ghsaId} failed: ${err instanceof Error ? err.message : String(err)}. CVE ignore rules may not apply.`,
|
|
206
|
+
);
|
|
207
|
+
return { ghsaId, cves: [] };
|
|
208
|
+
}
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
for (const { ghsaId, cves } of results) {
|
|
212
|
+
map.set(ghsaId, cves);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return map;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse a snapshot key like `lodash-es@4.17.23` or
|
|
221
|
+
* `vite@7.3.1(@types/node@24.10.1)(terser@5.44.1)` and return the
|
|
222
|
+
* base `name@version` (without peer suffixes).
|
|
223
|
+
*/
|
|
224
|
+
function snapshotKeyToNameVersion(key) {
|
|
225
|
+
// Find the version segment: last `@` before any `(`
|
|
226
|
+
const parenIdx = key.indexOf("(");
|
|
227
|
+
const base = parenIdx >= 0 ? key.slice(0, parenIdx) : key;
|
|
228
|
+
return base;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Classify every package in the lockfile as "prod" or "dev" by walking the
|
|
233
|
+
* dependency graph starting from each importer's `dependencies`.
|
|
234
|
+
*
|
|
235
|
+
* Returns a Set of `name@version` strings that are reachable from production
|
|
236
|
+
* dependency roots. Any package NOT in this set is dev-only.
|
|
237
|
+
*/
|
|
238
|
+
// eslint-disable-next-line complexity
|
|
239
|
+
export function classifyProdPackages(lockfilePath) {
|
|
240
|
+
const content = readFileSync(lockfilePath, "utf-8");
|
|
241
|
+
const lines = content.split("\n");
|
|
242
|
+
|
|
243
|
+
// --- Phase 1: parse all importers into structured data ---
|
|
244
|
+
const importers = new Map();
|
|
245
|
+
|
|
246
|
+
{
|
|
247
|
+
let section = "none";
|
|
248
|
+
let currentImporter = null;
|
|
249
|
+
let currentDepName = null;
|
|
250
|
+
let inDeps = false;
|
|
251
|
+
|
|
252
|
+
for (const line of lines) {
|
|
253
|
+
if (/^importers:\s*$/.test(line)) {
|
|
254
|
+
section = "importers";
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
if (/^(packages|snapshots|catalogs|settings):\s*$/.test(line)) {
|
|
258
|
+
if (section === "importers") {
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
if (section !== "importers") {
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Importer key (2-space indent)
|
|
268
|
+
if (
|
|
269
|
+
/^ {2}\S/.test(line) &&
|
|
270
|
+
!line.match(
|
|
271
|
+
/^ {2}(dependencies|devDependencies|peerDependencies|optionalDependencies):/,
|
|
272
|
+
)
|
|
273
|
+
) {
|
|
274
|
+
currentImporter = line.replace(/^ {2}/, "").replace(/:.*$/, "");
|
|
275
|
+
importers.set(currentImporter, { prodVersions: [], prodLinks: [] });
|
|
276
|
+
inDeps = false;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!currentImporter) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Sub-section detection
|
|
285
|
+
if (/^ {4}dependencies:\s*$/.test(line)) {
|
|
286
|
+
inDeps = true;
|
|
287
|
+
currentDepName = null;
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
if (
|
|
291
|
+
/^ {4}(devDependencies|peerDependencies|optionalDependencies):\s*$/.test(
|
|
292
|
+
line,
|
|
293
|
+
)
|
|
294
|
+
) {
|
|
295
|
+
inDeps = false;
|
|
296
|
+
currentDepName = null;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (inDeps) {
|
|
301
|
+
const versionMatch = line.match(/^ {8}version:\s+'?(.+?)'?\s*$/);
|
|
302
|
+
if (versionMatch) {
|
|
303
|
+
const ver = versionMatch[1];
|
|
304
|
+
const data = importers.get(currentImporter);
|
|
305
|
+
if (ver.startsWith("link:")) {
|
|
306
|
+
// eslint-disable-next-line no-magic-numbers
|
|
307
|
+
data.prodLinks.push(ver.slice(5)); // strip "link:" prefix
|
|
308
|
+
} else if (currentDepName) {
|
|
309
|
+
data.prodVersions.push(`${currentDepName}@${ver}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Package name key: ` package-name:`
|
|
313
|
+
const nameMatch = line.match(/^ {6}'?([^':]+)'?:\s*$/);
|
|
314
|
+
if (nameMatch) {
|
|
315
|
+
currentDepName = nameMatch[1];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Resolve workspace links to importer IDs.
|
|
322
|
+
function resolveLink(fromImporter, linkTarget) {
|
|
323
|
+
if (fromImporter === ".") {
|
|
324
|
+
return linkTarget.replace(/^\.\//, "");
|
|
325
|
+
}
|
|
326
|
+
const parts = fromImporter.split("/");
|
|
327
|
+
for (const seg of linkTarget.split("/")) {
|
|
328
|
+
if (seg === "..") {
|
|
329
|
+
parts.pop();
|
|
330
|
+
} else if (seg !== ".") {
|
|
331
|
+
parts.push(seg);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return parts.join("/");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Collect all prod roots
|
|
338
|
+
const prodRoots = new Set();
|
|
339
|
+
const visitedImporters = new Set();
|
|
340
|
+
|
|
341
|
+
function collectProdFromImporter(importerId) {
|
|
342
|
+
if (visitedImporters.has(importerId)) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
visitedImporters.add(importerId);
|
|
346
|
+
const data = importers.get(importerId);
|
|
347
|
+
if (!data) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
for (const ver of data.prodVersions) {
|
|
352
|
+
prodRoots.add(ver);
|
|
353
|
+
}
|
|
354
|
+
for (const linkTarget of data.prodLinks) {
|
|
355
|
+
const linkedImporter = resolveLink(importerId, linkTarget);
|
|
356
|
+
collectProdFromImporter(linkedImporter);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const importerId of importers.keys()) {
|
|
361
|
+
collectProdFromImporter(importerId);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// --- Phase 2: parse snapshots to build adjacency map ---
|
|
365
|
+
const snapshotDeps = new Map();
|
|
366
|
+
let inSnapshots = false;
|
|
367
|
+
let currentSnapshot = null;
|
|
368
|
+
let inSnapshotDeps = false;
|
|
369
|
+
|
|
370
|
+
for (const line of lines) {
|
|
371
|
+
if (/^snapshots:\s*$/.test(line)) {
|
|
372
|
+
inSnapshots = true;
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
if (inSnapshots && /^\S/.test(line) && !line.startsWith("snapshots:")) {
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
if (!inSnapshots) {
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const snapMatch = line.match(/^ {2}'?([^'\s].*?)'?:\s*(\{\})?$/);
|
|
383
|
+
if (snapMatch && !line.startsWith(" ")) {
|
|
384
|
+
currentSnapshot = snapMatch[1];
|
|
385
|
+
if (!snapshotDeps.has(currentSnapshot)) {
|
|
386
|
+
snapshotDeps.set(currentSnapshot, []);
|
|
387
|
+
}
|
|
388
|
+
inSnapshotDeps = false;
|
|
389
|
+
if (snapMatch[2]) {
|
|
390
|
+
currentSnapshot = null;
|
|
391
|
+
}
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (!currentSnapshot) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (/^ {4}(dependencies|optionalDependencies):\s*$/.test(line)) {
|
|
400
|
+
inSnapshotDeps = true;
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (
|
|
404
|
+
/^ {4}\S/.test(line) &&
|
|
405
|
+
!line.match(/^ {4}(dependencies|optionalDependencies):/)
|
|
406
|
+
) {
|
|
407
|
+
inSnapshotDeps = false;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
if (inSnapshotDeps) {
|
|
412
|
+
const depMatch = line.match(/^ {6}'?([^':]+)'?:\s+'?(.+?)'?\s*$/);
|
|
413
|
+
if (depMatch) {
|
|
414
|
+
const depName = depMatch[1];
|
|
415
|
+
const depVersion = depMatch[2];
|
|
416
|
+
snapshotDeps.get(currentSnapshot).push(`${depName}@${depVersion}`);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// --- Phase 3: BFS from prod roots ---
|
|
422
|
+
const prodPackages = new Set();
|
|
423
|
+
const visited = new Set();
|
|
424
|
+
const queue = [...prodRoots];
|
|
425
|
+
|
|
426
|
+
while (queue.length > 0) {
|
|
427
|
+
const key = queue.pop();
|
|
428
|
+
if (visited.has(key)) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
visited.add(key);
|
|
432
|
+
|
|
433
|
+
const baseKey = snapshotKeyToNameVersion(key);
|
|
434
|
+
prodPackages.add(baseKey);
|
|
435
|
+
|
|
436
|
+
const deps = snapshotDeps.get(key);
|
|
437
|
+
if (deps) {
|
|
438
|
+
queue.push(...deps);
|
|
439
|
+
} else {
|
|
440
|
+
for (const [snapKey, snapDeps] of snapshotDeps) {
|
|
441
|
+
if (
|
|
442
|
+
snapshotKeyToNameVersion(snapKey) === baseKey &&
|
|
443
|
+
!visited.has(snapKey)
|
|
444
|
+
) {
|
|
445
|
+
visited.add(snapKey);
|
|
446
|
+
queue.push(...snapDeps);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return prodPackages;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Minimally parse pnpm-lock.yaml to extract the `packages:` keys.
|
|
457
|
+
*/
|
|
458
|
+
export function extractPackagesFromLockfile(lockfilePath) {
|
|
459
|
+
const content = readFileSync(lockfilePath, "utf-8");
|
|
460
|
+
const lines = content.split("\n");
|
|
461
|
+
|
|
462
|
+
const packages = new Map();
|
|
463
|
+
let inPackagesSection = false;
|
|
464
|
+
|
|
465
|
+
for (const line of lines) {
|
|
466
|
+
if (/^packages:\s*$/.test(line)) {
|
|
467
|
+
inPackagesSection = true;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (
|
|
472
|
+
inPackagesSection &&
|
|
473
|
+
/^\S/.test(line) &&
|
|
474
|
+
!line.startsWith("packages:")
|
|
475
|
+
) {
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!inPackagesSection) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const match = line.match(/^ {2}'?([^':\s][^':]*?)@(\d[^':]*)'?:\s*$/);
|
|
484
|
+
if (!match) {
|
|
485
|
+
const unquotedMatch = line.match(
|
|
486
|
+
/^ {2}([^':\s][^':]*?)@(\d[^':]*?):\s*$/,
|
|
487
|
+
);
|
|
488
|
+
if (unquotedMatch) {
|
|
489
|
+
const name = unquotedMatch[1];
|
|
490
|
+
const version = unquotedMatch[2].replace(/\(.*$/, "");
|
|
491
|
+
if (name && version) {
|
|
492
|
+
// eslint-disable-next-line max-depth
|
|
493
|
+
if (!packages.has(name)) {
|
|
494
|
+
packages.set(name, new Set());
|
|
495
|
+
}
|
|
496
|
+
packages.get(name).add(version);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const name = match[1];
|
|
503
|
+
const version = match[2].replace(/\(.*$/, ""); // strip peer suffixes
|
|
504
|
+
if (name && version) {
|
|
505
|
+
if (!packages.has(name)) {
|
|
506
|
+
packages.set(name, new Set());
|
|
507
|
+
}
|
|
508
|
+
packages.get(name).add(version);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
return packages;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Call the npm bulk advisory endpoint and return the report.
|
|
517
|
+
*/
|
|
518
|
+
export async function fetchBulkAdvisories(
|
|
519
|
+
packages,
|
|
520
|
+
registry = "https://registry.npmjs.org",
|
|
521
|
+
) {
|
|
522
|
+
const body = {};
|
|
523
|
+
for (const [name, versions] of packages) {
|
|
524
|
+
body[name] = [...versions];
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const url = `${registry.replace(/\/$/, "")}/-/npm/v1/security/advisories/bulk`;
|
|
528
|
+
|
|
529
|
+
const response = await fetch(url, {
|
|
530
|
+
method: "POST",
|
|
531
|
+
headers: { "Content-Type": "application/json" },
|
|
532
|
+
body: JSON.stringify(body),
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
throw new Error(
|
|
537
|
+
`Bulk advisory endpoint responded with ${response.status}: ${await response.text()}`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return response.json();
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Run a full audit: parse lockfile, query the API, return structured results.
|
|
546
|
+
* Respects ignore lists from pnpm-workspace.yaml and package.json.
|
|
547
|
+
*/
|
|
548
|
+
export async function audit(opts) {
|
|
549
|
+
const lockfilePath = resolve(opts.lockfilePath);
|
|
550
|
+
const packages = extractPackagesFromLockfile(lockfilePath);
|
|
551
|
+
const advisories = await fetchBulkAdvisories(packages, opts.registry);
|
|
552
|
+
|
|
553
|
+
// Classify prod vs dev
|
|
554
|
+
const prodPackages = classifyProdPackages(lockfilePath);
|
|
555
|
+
|
|
556
|
+
// Collect ignore sets
|
|
557
|
+
const ignoredGhsas = new Set(opts.auditConfig?.ignoreGhsas ?? []);
|
|
558
|
+
const ignoredCves = new Set(opts.auditConfig?.ignoreCves ?? []);
|
|
559
|
+
const needCveResolution = ignoredCves.size > 0;
|
|
560
|
+
|
|
561
|
+
// If we have CVEs to ignore, resolve GHSA→CVE mappings
|
|
562
|
+
let ghsaToCves = new Map();
|
|
563
|
+
if (needCveResolution) {
|
|
564
|
+
const allGhsaIds = new Set();
|
|
565
|
+
for (const advList of Object.values(advisories)) {
|
|
566
|
+
for (const a of advList) {
|
|
567
|
+
const ghsaId = extractGhsaId(a.url);
|
|
568
|
+
if (ghsaId) {
|
|
569
|
+
allGhsaIds.add(ghsaId);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
ghsaToCves = await resolveGhsaToCves([...allGhsaIds]);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const minIdx = SEVERITY_ORDER.indexOf(opts.minSeverity ?? "low");
|
|
577
|
+
const severityCounts = {};
|
|
578
|
+
let totalVulnerabilities = 0;
|
|
579
|
+
let prodVulnerabilities = 0;
|
|
580
|
+
let devVulnerabilities = 0;
|
|
581
|
+
const filteredAdvisories = {};
|
|
582
|
+
|
|
583
|
+
for (const [pkg, advList] of Object.entries(advisories)) {
|
|
584
|
+
const versions = packages.get(pkg);
|
|
585
|
+
const isProd = versions
|
|
586
|
+
? [...versions].some((v) => prodPackages.has(`${pkg}@${v}`))
|
|
587
|
+
: false;
|
|
588
|
+
const depType = isProd ? "prod" : "dev";
|
|
589
|
+
|
|
590
|
+
if (opts.prod && !isProd) {
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const filtered = advList.filter((a) => {
|
|
595
|
+
const idx = SEVERITY_ORDER.indexOf(a.severity);
|
|
596
|
+
if (idx < minIdx) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const ghsaId = extractGhsaId(a.url);
|
|
601
|
+
if (ghsaId && ignoredGhsas.has(ghsaId)) {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (ghsaId && needCveResolution) {
|
|
606
|
+
const cves = ghsaToCves.get(ghsaId) ?? [];
|
|
607
|
+
if (cves.some((cve) => ignoredCves.has(cve))) {
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return true;
|
|
613
|
+
});
|
|
614
|
+
if (filtered.length > 0) {
|
|
615
|
+
filteredAdvisories[pkg] = filtered.map((a) => ({ ...a, depType }));
|
|
616
|
+
for (const a of filtered) {
|
|
617
|
+
severityCounts[a.severity] = (severityCounts[a.severity] ?? 0) + 1;
|
|
618
|
+
totalVulnerabilities++;
|
|
619
|
+
if (isProd) {
|
|
620
|
+
prodVulnerabilities++;
|
|
621
|
+
} else {
|
|
622
|
+
devVulnerabilities++;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return {
|
|
629
|
+
advisories: filteredAdvisories,
|
|
630
|
+
totalVulnerabilities,
|
|
631
|
+
severityCounts,
|
|
632
|
+
prodVulnerabilities,
|
|
633
|
+
devVulnerabilities,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Format an audit result for human-readable console output.
|
|
639
|
+
*/
|
|
640
|
+
export function formatAuditReport(result) {
|
|
641
|
+
if (result.totalVulnerabilities === 0) {
|
|
642
|
+
return "No vulnerabilities found.";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const lines = [];
|
|
646
|
+
lines.push(
|
|
647
|
+
`Found ${result.totalVulnerabilities} vulnerabilit${result.totalVulnerabilities === 1 ? "y" : "ies"} (${result.prodVulnerabilities} prod, ${result.devVulnerabilities} dev):\n`,
|
|
648
|
+
);
|
|
649
|
+
|
|
650
|
+
for (const severity of SEVERITY_ORDER) {
|
|
651
|
+
const count = result.severityCounts[severity];
|
|
652
|
+
if (count) {
|
|
653
|
+
lines.push(` ${severity}: ${count}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
lines.push("");
|
|
657
|
+
|
|
658
|
+
for (const [pkg, advList] of Object.entries(result.advisories)) {
|
|
659
|
+
for (const adv of advList) {
|
|
660
|
+
const tag = adv.depType === "prod" ? "prod" : "dev";
|
|
661
|
+
// eslint-disable-next-line no-magic-numbers
|
|
662
|
+
lines.push(` ${adv.severity.toUpperCase().padEnd(9)} ${pkg} [${tag}]`);
|
|
663
|
+
lines.push(` ${adv.title}`);
|
|
664
|
+
lines.push(` Vulnerable: ${adv.vulnerable_versions}`);
|
|
665
|
+
lines.push(` ${adv.url}`);
|
|
666
|
+
lines.push("");
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return lines.join("\n");
|
|
671
|
+
}
|
package/src/audit/cli.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* eslint-disable no-console, n/no-process-exit */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* CLI wrapper for the bulk advisory audit.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* pnpm-bulk-audit [--lockfile path] [--registry url] [--severity level] [--json]
|
|
9
|
+
*
|
|
10
|
+
* Defaults:
|
|
11
|
+
* --lockfile pnpm-lock.yaml (in cwd)
|
|
12
|
+
* --registry https://registry.npmjs.org
|
|
13
|
+
* --severity low
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { dirname, resolve } from "node:path";
|
|
18
|
+
|
|
19
|
+
import { audit, formatAuditReport, readAuditConfig } from "./bulkAudit.js";
|
|
20
|
+
|
|
21
|
+
function parseArgs(args) {
|
|
22
|
+
let lockfilePath = resolve("pnpm-lock.yaml");
|
|
23
|
+
let registry = "https://registry.npmjs.org";
|
|
24
|
+
let minSeverity = "low";
|
|
25
|
+
let json = false;
|
|
26
|
+
let noIgnore = false;
|
|
27
|
+
let prod = false;
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
const arg = args[i];
|
|
31
|
+
if (arg === "--lockfile" && args[i + 1]) {
|
|
32
|
+
lockfilePath = resolve(args[++i]);
|
|
33
|
+
} else if (arg === "--registry" && args[i + 1]) {
|
|
34
|
+
registry = args[++i];
|
|
35
|
+
} else if (arg === "--severity" && args[i + 1]) {
|
|
36
|
+
minSeverity = args[++i];
|
|
37
|
+
const validSeverities = ["low", "moderate", "high", "critical"];
|
|
38
|
+
if (!validSeverities.includes(minSeverity)) {
|
|
39
|
+
console.error(
|
|
40
|
+
`Error: unknown severity "${minSeverity}". Must be one of: ${validSeverities.join(", ")}`,
|
|
41
|
+
);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
} else if (arg === "--json") {
|
|
45
|
+
json = true;
|
|
46
|
+
} else if (arg === "--no-ignore") {
|
|
47
|
+
noIgnore = true;
|
|
48
|
+
} else if (arg === "--prod" || arg === "-P") {
|
|
49
|
+
prod = true;
|
|
50
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
51
|
+
console.log(`Usage: pnpm-bulk-audit [options]
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--lockfile <path> Path to pnpm-lock.yaml (default: ./pnpm-lock.yaml)
|
|
55
|
+
--registry <url> npm registry URL (default: https://registry.npmjs.org)
|
|
56
|
+
--severity <level> Minimum severity: low, moderate, high, critical (default: low)
|
|
57
|
+
--prod, -P Only report vulnerabilities in production dependencies
|
|
58
|
+
--json Output raw JSON
|
|
59
|
+
--no-ignore Skip reading ignoreCves/ignoreGhsas from config
|
|
60
|
+
--help, -h Show this help`);
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { lockfilePath, registry, minSeverity, json, noIgnore, prod };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
70
|
+
|
|
71
|
+
if (!existsSync(opts.lockfilePath)) {
|
|
72
|
+
console.error(`Error: lockfile not found at ${opts.lockfilePath}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const workspaceDir = dirname(opts.lockfilePath);
|
|
78
|
+
const auditConfig = opts.noIgnore
|
|
79
|
+
? undefined
|
|
80
|
+
: readAuditConfig(workspaceDir);
|
|
81
|
+
|
|
82
|
+
if (auditConfig && !opts.json) {
|
|
83
|
+
const ignoredCount =
|
|
84
|
+
(auditConfig.ignoreCves?.length ?? 0) +
|
|
85
|
+
(auditConfig.ignoreGhsas?.length ?? 0);
|
|
86
|
+
if (ignoredCount > 0) {
|
|
87
|
+
console.log(
|
|
88
|
+
`Ignoring ${ignoredCount} CVE/GHSA entr${ignoredCount === 1 ? "y" : "ies"} from config.\n`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const result = await audit({
|
|
94
|
+
lockfilePath: opts.lockfilePath,
|
|
95
|
+
registry: opts.registry,
|
|
96
|
+
minSeverity: opts.minSeverity,
|
|
97
|
+
auditConfig,
|
|
98
|
+
prod: opts.prod,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
if (opts.json) {
|
|
102
|
+
console.log(JSON.stringify(result, null, 2));
|
|
103
|
+
} else {
|
|
104
|
+
console.log(formatAuditReport(result));
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
process.exit(result.totalVulnerabilities > 0 ? 1 : 0);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error(
|
|
110
|
+
`Audit failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
111
|
+
);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// eslint-disable-next-line no-void
|
|
117
|
+
void main();
|