ohdear-npm-audit 1.3.0 → 1.4.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.md CHANGED
@@ -33,7 +33,13 @@ export default withOhDearHealth({
33
33
  });
34
34
  ```
35
35
 
36
- The wrapper generates the manifest automatically when the config is evaluated (before the build starts). It also checks for critical vulnerabilities at build time and logs the results.
36
+ The wrapper generates the manifest automatically when the config is evaluated (before the build starts). It also checks for critical vulnerabilities at build time and logs enriched results including dependency chains and vulnerable version ranges:
37
+
38
+ ```
39
+ ohdear-npm-audit: 1 critical vulnerabilities:
40
+ - form-data@1.0.0 (via axios → form-data): unsafe random function
41
+ vulnerable: <1.0.1 — https://github.com/advisories/GHSA-xxx
42
+ ```
37
43
 
38
44
  ```js
39
45
  withOhDearHealth(nextConfig, {
@@ -72,8 +78,8 @@ This must match the secret configured in Oh Dear for your application health che
72
78
 
73
79
  ## How it works
74
80
 
75
- 1. **Build time** — The CLI or Next.js wrapper runs `pnpm list` / `npm ls` to extract all production dependencies (including transitive) and writes them to a JSON manifest. When using the Next.js wrapper, a build-time vulnerability check is also performed and results are logged
76
- 2. **Runtime** — On each GET request, the handler verifies the Oh Dear secret header, POSTs the manifest to the [npm bulk advisory API](https://docs.npmjs.com/about-audit-reports), filters for critical severity, and returns the result in the [Oh Dear health check format](https://ohdear.app/docs/features/application-health-monitoring)
81
+ 1. **Build time** — The CLI or Next.js wrapper runs `pnpm list` / `npm ls` to extract all production dependencies (including transitive) and writes them to a JSON manifest that includes a reverse dependency map for tracing dependency chains. When using the Next.js wrapper, a build-time vulnerability check is performed and results are logged with the full dependency chain (e.g. `axios → form-data`) and vulnerable version ranges
82
+ 2. **Runtime** — On each GET request, the handler verifies the Oh Dear secret header, POSTs the manifest to the [npm bulk advisory API](https://docs.npmjs.com/about-audit-reports), filters for critical severity, and returns the result in the [Oh Dear health check format](https://ohdear.app/docs/features/application-health-monitoring). Each vulnerability includes installed versions, vulnerable version range, and the dependency chain
77
83
 
78
84
  ### Response format
79
85
 
@@ -93,6 +99,19 @@ This must match the secret configured in Oh Dear for your application health che
93
99
  }
94
100
  ```
95
101
 
102
+ When vulnerabilities are found, `meta.vulnerabilities` contains an array of:
103
+
104
+ ```json
105
+ {
106
+ "package": "form-data",
107
+ "installedVersions": ["1.0.0"],
108
+ "title": "form-data uses unsafe random function",
109
+ "url": "https://github.com/advisories/GHSA-xxx",
110
+ "vulnerableVersions": "<1.0.1",
111
+ "dependencyChain": ["axios", "form-data"]
112
+ }
113
+ ```
114
+
96
115
  `status` is `"ok"` when there are no critical vulnerabilities, `"warning"` when the npm advisory API is unreachable, `"failed"` otherwise.
97
116
 
98
117
  ## API
@@ -132,7 +151,7 @@ Defaults to `deps-manifest.json` in the current directory. Detects the package m
132
151
  ```
133
152
  src/
134
153
  ├── types.ts # Shared types (DepsManifest, Vulnerability, HealthCheckResponse)
135
- ├── generate.ts # Manifest generation logic (build-time, execSync)
154
+ ├── generate.ts # Manifest + reverse dep map generation (build-time, execSync)
136
155
  ├── handler.ts # createHealthHandler factory — main export "."
137
156
  ├── next.ts # withOhDearHealth wrapper — export "./next"
138
157
  └── bin.ts # CLI entry point — bin "ohdear-deps-manifest"
package/dist/bin.js CHANGED
@@ -11,5 +11,5 @@ if (outputIdx !== -1 && args[outputIdx + 1]) {
11
11
  }
12
12
  const cwd = process.cwd();
13
13
  const outputPath = (0, node_path_1.resolve)(cwd, output);
14
- const { manifest } = (0, generate_js_1.writeManifest)(outputPath, cwd);
15
- console.log(`deps-manifest.json: ${Object.keys(manifest).length} packages written`);
14
+ const manifest = (0, generate_js_1.writeManifest)(outputPath, cwd);
15
+ console.log(`${output}: ${Object.keys(manifest.packages).length} packages written`);
@@ -1,11 +1,3 @@
1
1
  import type { DepsManifest } from "./types.js";
2
- export interface GenerateResult {
3
- manifest: DepsManifest;
4
- reverseDeps: Record<string, string[]>;
5
- }
6
- export declare function generateManifest(cwd: string): GenerateResult;
7
- export interface WriteManifestResult {
8
- manifest: DepsManifest;
9
- reverseMapPath: string;
10
- }
11
- export declare function writeManifest(outputPath: string, cwd: string): WriteManifestResult;
2
+ export declare function generateManifest(cwd: string): DepsManifest;
3
+ export declare function writeManifest(outputPath: string, cwd: string): DepsManifest;
package/dist/generate.js CHANGED
@@ -23,9 +23,9 @@ function walkDeps(deps, acc, reverseDeps, parent) {
23
23
  if (!acc[name])
24
24
  acc[name] = new Set();
25
25
  acc[name].add(info.version);
26
+ if (!reverseDeps[name])
27
+ reverseDeps[name] = new Set();
26
28
  if (parent !== "root") {
27
- if (!reverseDeps[name])
28
- reverseDeps[name] = new Set();
29
29
  reverseDeps[name].add(parent);
30
30
  }
31
31
  walkDeps(info.dependencies, acc, reverseDeps, name);
@@ -75,21 +75,19 @@ function generateManifest(cwd) {
75
75
  const acc = {};
76
76
  const reverseAcc = {};
77
77
  walkDeps(tree.dependencies, acc, reverseAcc, "root");
78
- const manifest = {};
78
+ const packages = {};
79
79
  for (const [name, versions] of Object.entries(acc)) {
80
- manifest[name] = [...versions];
80
+ packages[name] = [...versions];
81
81
  }
82
82
  const reverseDeps = {};
83
83
  for (const [name, parents] of Object.entries(reverseAcc)) {
84
84
  reverseDeps[name] = [...parents];
85
85
  }
86
- return { manifest, reverseDeps };
86
+ return { packages, reverseDeps };
87
87
  }
88
88
  function writeManifest(outputPath, cwd) {
89
- const { manifest, reverseDeps } = generateManifest(cwd);
89
+ const manifest = generateManifest(cwd);
90
90
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(outputPath), { recursive: true });
91
91
  (0, node_fs_1.writeFileSync)(outputPath, JSON.stringify(manifest, null, 2) + "\n");
92
- const reverseMapPath = (0, node_path_1.resolve)((0, node_path_1.dirname)(outputPath), "deps-reverse-map.json");
93
- (0, node_fs_1.writeFileSync)(reverseMapPath, JSON.stringify(reverseDeps, null, 2) + "\n");
94
- return { manifest, reverseMapPath };
92
+ return manifest;
95
93
  }
package/dist/handler.js CHANGED
@@ -3,6 +3,25 @@ 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
+ /** BFS from pkg up through reverseMap to find the shortest chain to a root dep. */
7
+ function buildChain(pkg, reverseMap) {
8
+ const queue = [[pkg]];
9
+ const visited = new Set([pkg]);
10
+ while (queue.length > 0) {
11
+ const path = queue.shift();
12
+ const current = path[path.length - 1];
13
+ const parents = reverseMap[current];
14
+ if (!parents || parents.length === 0)
15
+ return [...path].reverse();
16
+ for (const parent of parents) {
17
+ if (visited.has(parent))
18
+ continue;
19
+ visited.add(parent);
20
+ queue.push([...path, parent]);
21
+ }
22
+ }
23
+ return [pkg];
24
+ }
6
25
  function makeWarningResponse(message) {
7
26
  return {
8
27
  finishedAt: Math.floor(Date.now() / 1000),
@@ -36,7 +55,7 @@ function createHealthHandler(manifest, options) {
36
55
  res = await fetch(NPM_BULK_ADVISORY_URL, {
37
56
  method: "POST",
38
57
  headers: { "Content-Type": "application/json" },
39
- body: JSON.stringify(manifest),
58
+ body: JSON.stringify(manifest.packages),
40
59
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
41
60
  });
42
61
  }
@@ -60,14 +79,16 @@ function createHealthHandler(manifest, options) {
60
79
  for (const [pkg, entries] of Object.entries(advisories)) {
61
80
  for (const entry of entries) {
62
81
  if (entry.severity === "critical") {
63
- const versions = manifest[pkg] ?? [];
64
- critical.push({
82
+ const versions = manifest.packages[pkg] ?? [];
83
+ const vuln = {
65
84
  package: pkg,
66
- installedVersion: versions.join(", "),
85
+ installedVersions: versions,
67
86
  title: entry.title,
68
87
  url: entry.url,
69
88
  vulnerableVersions: entry.vulnerable_versions ?? "",
70
- });
89
+ dependencyChain: buildChain(pkg, manifest.reverseDeps),
90
+ };
91
+ critical.push(vuln);
71
92
  }
72
93
  }
73
94
  }
package/dist/next.js CHANGED
@@ -52,14 +52,21 @@ function hasRecentLock(output) {
52
52
  */
53
53
  function buildChainScript() {
54
54
  return [
55
- `function buildChain(pkg, reverseMap, visited) {`,
56
- ` if (visited.has(pkg)) return [pkg];`,
57
- ` visited.add(pkg);`,
58
- ` const parents = reverseMap[pkg];`,
59
- ` if (!parents || parents.length === 0) return [pkg];`,
60
- ` const chain = buildChain(parents[0], reverseMap, visited);`,
61
- ` chain.push(pkg);`,
62
- ` return chain;`,
55
+ `function buildChain(pkg, reverseMap) {`,
56
+ ` const queue = [[pkg]];`,
57
+ ` const visited = new Set([pkg]);`,
58
+ ` while (queue.length > 0) {`,
59
+ ` const path = queue.shift();`,
60
+ ` const current = path[path.length - 1];`,
61
+ ` const parents = reverseMap[current];`,
62
+ ` if (!parents || parents.length === 0) return path.slice().reverse();`,
63
+ ` for (const parent of parents) {`,
64
+ ` if (visited.has(parent)) continue;`,
65
+ ` visited.add(parent);`,
66
+ ` queue.push([...path, parent]);`,
67
+ ` }`,
68
+ ` }`,
69
+ ` return [pkg];`,
63
70
  `}`,
64
71
  ].join("\n");
65
72
  }
@@ -68,18 +75,18 @@ function buildChainScript() {
68
75
  * API. Output is inherited so logs appear in the build output. Does not
69
76
  * block the build — the subprocess runs in parallel with Next.js compilation.
70
77
  */
71
- function checkVulnerabilities(manifestPath, reverseMapPath) {
72
- const safeManifestPath = JSON.stringify(manifestPath);
73
- const safeReverseMapPath = JSON.stringify(reverseMapPath);
78
+ function checkVulnerabilities(manifestPath) {
79
+ const safePath = JSON.stringify(manifestPath);
74
80
  const script = [
75
81
  `const fs = require("fs");`,
76
- `const manifest = JSON.parse(fs.readFileSync(${safeManifestPath}, "utf-8"));`,
77
- `const reverseMap = JSON.parse(fs.readFileSync(${safeReverseMapPath}, "utf-8"));`,
82
+ `const data = JSON.parse(fs.readFileSync(${safePath}, "utf-8"));`,
83
+ `const packages = data.packages;`,
84
+ `const reverseMap = data.reverseDeps;`,
78
85
  buildChainScript(),
79
86
  `fetch("https://registry.npmjs.org/-/npm/v1/security/advisories/bulk", {`,
80
87
  ` method: "POST",`,
81
88
  ` headers: { "Content-Type": "application/json" },`,
82
- ` body: JSON.stringify(manifest),`,
89
+ ` body: JSON.stringify(packages),`,
83
90
  ` signal: AbortSignal.timeout(8000),`,
84
91
  `})`,
85
92
  `.then(r => { if (!r.ok) throw new Error("HTTP " + r.status); return r.json(); })`,
@@ -88,8 +95,8 @@ function checkVulnerabilities(manifestPath, reverseMapPath) {
88
95
  ` for (const [pkg, entries] of Object.entries(data)) {`,
89
96
  ` for (const e of entries) {`,
90
97
  ` if (e.severity !== "critical") continue;`,
91
- ` const versions = manifest[pkg] || [];`,
92
- ` const chain = buildChain(pkg, reverseMap, new Set());`,
98
+ ` const versions = packages[pkg] || [];`,
99
+ ` const chain = buildChain(pkg, reverseMap);`,
93
100
  ` const versionStr = versions.join(", ");`,
94
101
  ` const chainStr = chain.length > 1 ? " (via " + chain.join(" \\u2192 ") + ")" : "";`,
95
102
  ` let line = " - " + pkg + "@" + versionStr + chainStr + ": " + e.title;`,
@@ -106,7 +113,7 @@ function checkVulnerabilities(manifestPath, reverseMapPath) {
106
113
  ` console.log("ohdear-npm-audit: no critical vulnerabilities \\u2713");`,
107
114
  ` }`,
108
115
  `})`,
109
- `.catch(() => {});`,
116
+ `.catch(err => console.warn("ohdear-npm-audit: build-time vulnerability check failed:", err.message || err));`,
110
117
  ].join("\n");
111
118
  const child = (0, node_child_process_1.spawn)("node", ["-e", script], {
112
119
  stdio: "inherit",
@@ -121,8 +128,8 @@ function withOhDearHealth(nextConfig, options) {
121
128
  if (hasRecentLock(output))
122
129
  return nextConfig;
123
130
  try {
124
- const { manifest, reverseMapPath } = (0, generate_js_1.writeManifest)(output, cwd);
125
- console.log(`ohdear-npm-audit: ${Object.keys(manifest).length} packages written → ${output}`);
131
+ const manifest = (0, generate_js_1.writeManifest)(output, cwd);
132
+ console.log(`ohdear-npm-audit: ${Object.keys(manifest.packages).length} packages written → ${output}`);
126
133
  const routePath = deriveRoutePath((0, node_path_1.relative)(cwd, output));
127
134
  const domain = process.env.VERCEL_PROJECT_PRODUCTION_URL;
128
135
  if (routePath && domain) {
@@ -138,7 +145,7 @@ function withOhDearHealth(nextConfig, options) {
138
145
  console.warn("ohdear-npm-audit: OHDEAR_HEALTH_SECRET is not set — health check will reject all requests.");
139
146
  }
140
147
  if (options?.checkOnBuild !== false) {
141
- checkVulnerabilities(output, reverseMapPath);
148
+ checkVulnerabilities(output);
142
149
  }
143
150
  }
144
151
  catch (err) {
package/dist/types.d.ts CHANGED
@@ -1,7 +1,10 @@
1
- export type DepsManifest = Record<string, string[]>;
1
+ export interface DepsManifest {
2
+ packages: Record<string, string[]>;
3
+ reverseDeps: Record<string, string[]>;
4
+ }
2
5
  export interface Vulnerability {
3
6
  package: string;
4
- installedVersion: string;
7
+ installedVersions: string[];
5
8
  title: string;
6
9
  url: string;
7
10
  vulnerableVersions: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohdear-npm-audit",
3
- "version": "1.3.0",
3
+ "version": "1.4.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",