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 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 makeWarningResponse(message) {
26
- return {
27
- finishedAt: Math.floor(Date.now() / 1000),
28
- checkResults: [
29
- {
30
- name: "npm_vulnerabilities",
31
- label: "NPM Critical Vulnerabilities",
32
- status: "warning",
33
- notificationMessage: message,
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(makeWarningResponse(message));
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
- return Response.json(makeWarningResponse(`npm advisory API returned HTTP ${res.status}`));
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(makeWarningResponse("Failed to parse npm advisory response"));
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
- const critical = [];
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
- if (entry.severity === "critical") {
82
- const versions = manifest.packages[pkg] ?? [];
83
- const vuln = {
84
- package: pkg,
85
- installedVersions: versions,
86
- title: entry.title,
87
- url: entry.url,
88
- vulnerableVersions: entry.vulnerable_versions ?? "",
89
- dependencyChain: buildChain(pkg, manifest.reverseDeps),
90
- };
91
- critical.push(vuln);
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 status = critical.length === 0 ? "ok" : "failed";
96
- const shortSummary = `${critical.length} critical`;
97
- const notificationMessage = critical.length === 0
98
- ? "No critical npm vulnerabilities found."
99
- : `Critical vulnerabilities in: ${critical.map((v) => v.package).join(", ")}`;
100
- const body = {
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 crits = [];`,
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 !== "critical") continue;`,
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
- ` crits.push(line);`,
112
+ ` bySev[e.severity].push(line);`,
107
113
  ` }`,
108
114
  ` }`,
109
- ` if (crits.length > 0) {`,
110
- ` console.warn("ohdear-npm-audit: " + crits.length + " critical vulnerabilities:");`,
111
- ` crits.forEach(c => console.warn(c));`,
112
- ` } else {`,
113
- ` console.log("ohdear-npm-audit: no critical vulnerabilities \\u2713");`,
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
@@ -1,3 +1,4 @@
1
+ export type Severity = "critical" | "high" | "moderate" | "low";
1
2
  export interface DepsManifest {
2
3
  packages: Record<string, string[]>;
3
4
  reverseDeps: Record<string, string[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohdear-npm-audit",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Oh Dear Application Health check for npm audit critical vulnerabilities",
5
5
  "main": "./dist/handler.js",
6
6
  "types": "./dist/handler.d.ts",