ohdear-npm-audit 1.4.0 → 1.5.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/dist/handler.d.ts +6 -2
- package/dist/handler.js +66 -48
- package/dist/next.d.ts +5 -0
- package/dist/next.js +22 -10
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/handler.d.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import type { DepsManifest } from "./types.js";
|
|
2
|
-
export type { DepsManifest, HealthCheckResponse, Vulnerability, } from "./types.js";
|
|
1
|
+
import type { DepsManifest, Severity } from "./types.js";
|
|
2
|
+
export type { DepsManifest, HealthCheckResponse, HealthCheckResult, Severity, Vulnerability, } from "./types.js";
|
|
3
3
|
export interface CreateHealthHandlerOptions {
|
|
4
4
|
/** Environment variable name for the secret. Default: "OHDEAR_HEALTH_SECRET" */
|
|
5
5
|
secretEnvVar?: string;
|
|
6
6
|
/** Header name for the secret. Default: "oh-dear-health-check-secret" */
|
|
7
7
|
secretHeader?: string;
|
|
8
|
+
/** Severity levels to report. Default: ["critical"] */
|
|
9
|
+
severity?: Severity[];
|
|
10
|
+
/** Package names to ignore (e.g. known/accepted vulnerabilities). Default: [] */
|
|
11
|
+
ignorePackages?: string[];
|
|
8
12
|
}
|
|
9
13
|
export declare function createHealthHandler(manifest: DepsManifest, options?: CreateHealthHandlerOptions): (request: Request) => Promise<Response>;
|
package/dist/handler.js
CHANGED
|
@@ -3,6 +3,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createHealthHandler = createHealthHandler;
|
|
4
4
|
const NPM_BULK_ADVISORY_URL = "https://registry.npmjs.org/-/npm/v1/security/advisories/bulk";
|
|
5
5
|
const FETCH_TIMEOUT_MS = 8_000;
|
|
6
|
+
const SEVERITY_OHDEAR_STATUS = {
|
|
7
|
+
critical: "failed",
|
|
8
|
+
high: "warning",
|
|
9
|
+
moderate: "warning",
|
|
10
|
+
low: "warning",
|
|
11
|
+
};
|
|
12
|
+
function capitalize(s) {
|
|
13
|
+
return s[0].toUpperCase() + s.slice(1);
|
|
14
|
+
}
|
|
6
15
|
/** BFS from pkg up through reverseMap to find the shortest chain to a root dep. */
|
|
7
16
|
function buildChain(pkg, reverseMap) {
|
|
8
17
|
const queue = [[pkg]];
|
|
@@ -22,24 +31,21 @@ function buildChain(pkg, reverseMap) {
|
|
|
22
31
|
}
|
|
23
32
|
return [pkg];
|
|
24
33
|
}
|
|
25
|
-
function
|
|
26
|
-
return {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
shortSummary: "check error",
|
|
35
|
-
meta: {},
|
|
36
|
-
},
|
|
37
|
-
],
|
|
38
|
-
};
|
|
34
|
+
function makeWarningResults(message, severityFilter) {
|
|
35
|
+
return severityFilter.map((sev) => ({
|
|
36
|
+
name: `npm_vulnerabilities_${sev}`,
|
|
37
|
+
label: `NPM ${capitalize(sev)} Vulnerabilities`,
|
|
38
|
+
status: "warning",
|
|
39
|
+
notificationMessage: message,
|
|
40
|
+
shortSummary: "check error",
|
|
41
|
+
meta: {},
|
|
42
|
+
}));
|
|
39
43
|
}
|
|
40
44
|
function createHealthHandler(manifest, options) {
|
|
41
45
|
const envVar = options?.secretEnvVar ?? "OHDEAR_HEALTH_SECRET";
|
|
42
46
|
const headerName = options?.secretHeader ?? "oh-dear-health-check-secret";
|
|
47
|
+
const severityFilter = options?.severity ?? ["critical"];
|
|
48
|
+
const ignorePackages = options?.ignorePackages ?? [];
|
|
43
49
|
let didWarn = false;
|
|
44
50
|
return async (request) => {
|
|
45
51
|
const secret = request.headers.get(headerName);
|
|
@@ -63,53 +69,65 @@ function createHealthHandler(manifest, options) {
|
|
|
63
69
|
const message = err instanceof DOMException && err.name === "TimeoutError"
|
|
64
70
|
? "npm advisory API timed out"
|
|
65
71
|
: "npm advisory API request failed";
|
|
66
|
-
return Response.json(
|
|
72
|
+
return Response.json({
|
|
73
|
+
finishedAt: Math.floor(Date.now() / 1000),
|
|
74
|
+
checkResults: makeWarningResults(message, severityFilter),
|
|
75
|
+
});
|
|
67
76
|
}
|
|
68
77
|
if (!res.ok) {
|
|
69
|
-
|
|
78
|
+
const msg = `npm advisory API returned HTTP ${res.status}`;
|
|
79
|
+
return Response.json({
|
|
80
|
+
finishedAt: Math.floor(Date.now() / 1000),
|
|
81
|
+
checkResults: makeWarningResults(msg, severityFilter),
|
|
82
|
+
});
|
|
70
83
|
}
|
|
71
84
|
let advisories;
|
|
72
85
|
try {
|
|
73
86
|
advisories = await res.json();
|
|
74
87
|
}
|
|
75
88
|
catch {
|
|
76
|
-
return Response.json(
|
|
89
|
+
return Response.json({
|
|
90
|
+
finishedAt: Math.floor(Date.now() / 1000),
|
|
91
|
+
checkResults: makeWarningResults("Failed to parse npm advisory response", severityFilter),
|
|
92
|
+
});
|
|
77
93
|
}
|
|
78
|
-
|
|
94
|
+
// Group vulnerabilities by severity level
|
|
95
|
+
const bySeverity = new Map(severityFilter.map((sev) => [sev, []]));
|
|
79
96
|
for (const [pkg, entries] of Object.entries(advisories)) {
|
|
97
|
+
if (ignorePackages.includes(pkg))
|
|
98
|
+
continue;
|
|
80
99
|
for (const entry of entries) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
100
|
+
const sev = entry.severity;
|
|
101
|
+
const bucket = bySeverity.get(sev);
|
|
102
|
+
if (!bucket)
|
|
103
|
+
continue;
|
|
104
|
+
bucket.push({
|
|
105
|
+
package: pkg,
|
|
106
|
+
installedVersions: manifest.packages[pkg] ?? [],
|
|
107
|
+
title: entry.title,
|
|
108
|
+
url: entry.url,
|
|
109
|
+
vulnerableVersions: entry.vulnerable_versions ?? "",
|
|
110
|
+
dependencyChain: buildChain(pkg, manifest.reverseDeps),
|
|
111
|
+
});
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
114
|
+
const checkResults = severityFilter.map((sev) => {
|
|
115
|
+
const vulns = bySeverity.get(sev);
|
|
116
|
+
const status = vulns.length === 0 ? "ok" : SEVERITY_OHDEAR_STATUS[sev];
|
|
117
|
+
return {
|
|
118
|
+
name: `npm_vulnerabilities_${sev}`,
|
|
119
|
+
label: `NPM ${capitalize(sev)} Vulnerabilities`,
|
|
120
|
+
status,
|
|
121
|
+
shortSummary: `${vulns.length} ${sev}`,
|
|
122
|
+
notificationMessage: vulns.length === 0
|
|
123
|
+
? `No ${sev} npm vulnerabilities found.`
|
|
124
|
+
: `${capitalize(sev)} vulnerabilities in: ${vulns.map((v) => v.package).join(", ")}`,
|
|
125
|
+
meta: vulns.length > 0 ? { vulnerabilities: vulns } : {},
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
return Response.json({
|
|
101
129
|
finishedAt: Math.floor(Date.now() / 1000),
|
|
102
|
-
checkResults
|
|
103
|
-
|
|
104
|
-
name: "npm_vulnerabilities",
|
|
105
|
-
label: "NPM Critical Vulnerabilities",
|
|
106
|
-
status,
|
|
107
|
-
notificationMessage,
|
|
108
|
-
shortSummary,
|
|
109
|
-
meta: critical.length > 0 ? { vulnerabilities: critical } : {},
|
|
110
|
-
},
|
|
111
|
-
],
|
|
112
|
-
};
|
|
113
|
-
return Response.json(body);
|
|
130
|
+
checkResults,
|
|
131
|
+
});
|
|
114
132
|
};
|
|
115
133
|
}
|
package/dist/next.d.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
+
import type { Severity } from "./types.js";
|
|
1
2
|
export interface WithOhDearHealthOptions {
|
|
2
3
|
/** Output path for the manifest, relative to project root.
|
|
3
4
|
* Default: "src/app/api/health/deps-manifest.json" */
|
|
4
5
|
output?: string;
|
|
5
6
|
/** Check for critical vulnerabilities at build time. Default: true */
|
|
6
7
|
checkOnBuild?: boolean;
|
|
8
|
+
/** Severity levels to check at build time. Default: ["critical"] */
|
|
9
|
+
severity?: Severity[];
|
|
10
|
+
/** Package names to ignore. Default: [] */
|
|
11
|
+
ignorePackages?: string[];
|
|
7
12
|
}
|
|
8
13
|
export declare function withOhDearHealth<T>(nextConfig: T, options?: WithOhDearHealthOptions): T;
|
package/dist/next.js
CHANGED
|
@@ -75,13 +75,17 @@ function buildChainScript() {
|
|
|
75
75
|
* API. Output is inherited so logs appear in the build output. Does not
|
|
76
76
|
* block the build — the subprocess runs in parallel with Next.js compilation.
|
|
77
77
|
*/
|
|
78
|
-
function checkVulnerabilities(manifestPath) {
|
|
78
|
+
function checkVulnerabilities(manifestPath, severity, ignorePackages) {
|
|
79
79
|
const safePath = JSON.stringify(manifestPath);
|
|
80
|
+
const safeSeverity = JSON.stringify(severity);
|
|
81
|
+
const safeIgnore = JSON.stringify(ignorePackages);
|
|
80
82
|
const script = [
|
|
81
83
|
`const fs = require("fs");`,
|
|
82
84
|
`const data = JSON.parse(fs.readFileSync(${safePath}, "utf-8"));`,
|
|
83
85
|
`const packages = data.packages;`,
|
|
84
86
|
`const reverseMap = data.reverseDeps;`,
|
|
87
|
+
`const severityFilter = ${safeSeverity};`,
|
|
88
|
+
`const ignorePackages = ${safeIgnore};`,
|
|
85
89
|
buildChainScript(),
|
|
86
90
|
`fetch("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {`,
|
|
87
91
|
` method: "POST",`,
|
|
@@ -91,10 +95,12 @@ function checkVulnerabilities(manifestPath) {
|
|
|
91
95
|
`})`,
|
|
92
96
|
`.then(r => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })`,
|
|
93
97
|
`.then(data => {`,
|
|
94
|
-
` const
|
|
98
|
+
` const bySev = {};`,
|
|
99
|
+
` severityFilter.forEach(s => bySev[s] = []);`,
|
|
95
100
|
` for (const [pkg, entries] of Object.entries(data)) {`,
|
|
101
|
+
` if (ignorePackages.includes(pkg)) continue;`,
|
|
96
102
|
` for (const e of entries) {`,
|
|
97
|
-
` if (e.severity
|
|
103
|
+
` if (!bySev[e.severity]) continue;`,
|
|
98
104
|
` const versions = packages[pkg] || [];`,
|
|
99
105
|
` const chain = buildChain(pkg, reverseMap);`,
|
|
100
106
|
` const versionStr = versions.join(", ");`,
|
|
@@ -103,14 +109,20 @@ function checkVulnerabilities(manifestPath) {
|
|
|
103
109
|
` if (e.vulnerable_versions) {`,
|
|
104
110
|
` line += "\\n vulnerable: " + e.vulnerable_versions + " \\u2014 " + e.url;`,
|
|
105
111
|
` }`,
|
|
106
|
-
`
|
|
112
|
+
` bySev[e.severity].push(line);`,
|
|
107
113
|
` }`,
|
|
108
114
|
` }`,
|
|
109
|
-
`
|
|
110
|
-
`
|
|
111
|
-
`
|
|
112
|
-
`
|
|
113
|
-
`
|
|
115
|
+
` let total = 0;`,
|
|
116
|
+
` for (const sev of severityFilter) {`,
|
|
117
|
+
` const lines = bySev[sev];`,
|
|
118
|
+
` total += lines.length;`,
|
|
119
|
+
` if (lines.length > 0) {`,
|
|
120
|
+
` console.warn("ohdear-npm-audit: " + lines.length + " " + sev + " vulnerabilities:");`,
|
|
121
|
+
` lines.forEach(c => console.warn(c));`,
|
|
122
|
+
` }`,
|
|
123
|
+
` }`,
|
|
124
|
+
` if (total === 0) {`,
|
|
125
|
+
` console.log("ohdear-npm-audit: no " + severityFilter.join("/") + " vulnerabilities \\u2713");`,
|
|
114
126
|
` }`,
|
|
115
127
|
`})`,
|
|
116
128
|
`.catch(err => console.warn("ohdear-npm-audit: build-time vulnerability check failed:", err.message || err));`,
|
|
@@ -145,7 +157,7 @@ function withOhDearHealth(nextConfig, options) {
|
|
|
145
157
|
console.warn("ohdear-npm-audit: OHDEAR_HEALTH_SECRET is not set — health check will reject all requests.");
|
|
146
158
|
}
|
|
147
159
|
if (options?.checkOnBuild !== false) {
|
|
148
|
-
checkVulnerabilities(output);
|
|
160
|
+
checkVulnerabilities(output, options?.severity ?? ["critical"], options?.ignorePackages ?? []);
|
|
149
161
|
}
|
|
150
162
|
}
|
|
151
163
|
catch (err) {
|
package/dist/types.d.ts
CHANGED