cve-guard-npm 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Naveen Rawat
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,147 @@
1
+ # cve-guard-npm
2
+
3
+ `cve-guard-npm` is a lifecycle hook guard for npm installs. It automatically scans package install targets for OSV/CVE vulnerabilities before install and audits the resulting dependency tree after install.
4
+
5
+ ## Features
6
+
7
+ - Pre-install CVE scanning for packages requested with `npm install`
8
+ - Interactive warnings and confirmation when vulnerabilities are detected
9
+ - Post-install audit report using `npm audit --json`
10
+ - Beautiful terminal experience with `chalk`, `boxen`, `cli-table3`, `gradient-string`, and `ora`
11
+ - Works without changing normal npm commands after one-time setup
12
+ - Graceful error handling for offline, malformed input, unsupported npm usage, and audit failures
13
+ - Bonus reputation details for packages: weekly downloads, publish date, maintainers count
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install --save-dev cve-guard-npm
19
+ ```
20
+
21
+ ## One-time Setup
22
+
23
+ Add lifecycle scripts to your `package.json`:
24
+
25
+ ```json
26
+ {
27
+ "scripts": {
28
+ "preinstall": "cve-guard-npm preinstall",
29
+ "postinstall": "cve-guard-npm postinstall"
30
+ }
31
+ }
32
+ ```
33
+
34
+ After setup, every future `npm install` call will trigger the CVE guard automatically.
35
+
36
+ ## Usage
37
+
38
+ ### Normal install
39
+
40
+ ```bash
41
+ npm install express
42
+ ```
43
+
44
+ This will automatically run the preinstall CVE scan for `express`, prompt if vulnerabilities exist, and run `npm audit --json` after install.
45
+
46
+ ### Preinstall flow
47
+
48
+ If the package scan detects issues, you will see a report and a prompt like:
49
+
50
+ ```text
51
+ Continue installation? (y/n):
52
+ ```
53
+
54
+ If you choose `n`, the install aborts safely.
55
+
56
+ ### Postinstall flow
57
+
58
+ After install completes, the package runs `npm audit --json` and prints a summary report for detected vulnerabilities.
59
+
60
+ ## Example Output
61
+
62
+ ### Preinstall
63
+
64
+ ```text
65
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
66
+ CVE PRE-INSTALL CHECK
67
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68
+
69
+ Package: lodash
70
+ • Weekly downloads: 23,000,000
71
+ • Last publish date: 2024-12-05
72
+ • Maintainers: 4
73
+
74
+ ┌───────────┬────────────┬──────────────────────────────────────────────────────────┐
75
+ │ Severity │ CVE / GHSA │ Summary │
76
+ ├───────────┼────────────┼──────────────────────────────────────────────────────────┤
77
+ │ HIGH │ GHSA-xxxx │ Prototype Pollution │
78
+ └───────────┴────────────┴──────────────────────────────────────────────────────────┘
79
+
80
+ Continue installation? (y/n): n
81
+
82
+ ❌ Installation aborted by user.
83
+ ```
84
+
85
+ ### Postinstall
86
+
87
+ ```text
88
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
89
+ DEPENDENCY SECURITY REPORT
90
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
91
+
92
+ ┌───────────┬───────┐
93
+ │ Severity │ Count │
94
+ ├───────────┼───────┤
95
+ │ CRITICAL │ 1 │
96
+ │ HIGH │ 2 │
97
+ │ MODERATE │ 5 │
98
+ │ LOW │ 1 │
99
+ └───────────┴───────┘
100
+
101
+ Affected Packages:
102
+
103
+ ┌──────────────────────────┬──────────┬──────────────────────────────┐
104
+ │ Package@Version │ Severity │ Identifier │
105
+ ├──────────────────────────┼──────────┼──────────────────────────────┤
106
+ │ lodash@4.17.15 │ high │ GHSA-xxxx │
107
+ └──────────────────────────┴──────────┴──────────────────────────────┘
108
+ ```
109
+
110
+ ## Architecture Overview
111
+
112
+ - `bin/cli.js` - executable entrypoint for `cve-guard-npm`
113
+ - `lib/preinstall.js` - pre-install lifecycle hook logic
114
+ - `lib/postinstall.js` - post-install audit reporting
115
+ - `lib/osv.js` - reusable OSV API integration with retry and timeout support
116
+ - `lib/audit.js` - npm audit execution and JSON parsing
117
+ - `lib/formatter.js` - CLI output formatting and tables
118
+ - `lib/logger.js` - rich boxed headers and log helpers
119
+ - `lib/prompt.js` - interactive confirmation helper
120
+ - `lib/utils.js` - npm argv parsing, package normalization, and package.json helpers
121
+ - `lib/constants.js` - shared configuration values and mappings
122
+
123
+ ## Screenshots
124
+
125
+ ![Screenshot 1](https://via.placeholder.com/800x320?text=Preinstall+Security+Report)
126
+
127
+ ![Screenshot 2](https://via.placeholder.com/800x320?text=Postinstall+Audit+Summary)
128
+
129
+ ## Roadmap
130
+
131
+ - [ ] Add support for lockfile-aware package resolution
132
+ - [ ] Add optional CI-only enforcement mode
133
+ - [ ] Add config file support for ignore rules
134
+ - [ ] Add support for Yarn and pnpm lifecycle flows
135
+ - [ ] Add package metadata caching to reduce network traffic
136
+
137
+ ## Contributing
138
+
139
+ 1. Fork the repository
140
+ 2. Create a branch for your feature or fix
141
+ 3. Submit a pull request with tests and documentation
142
+
143
+ Please follow the existing code style and keep features modular.
144
+
145
+ ## License
146
+
147
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env node
2
+ const { program } = require("commander");
3
+ const pkg = require("../package.json");
4
+
5
+ program
6
+ .name("cve-guard-npm")
7
+ .description("Guard npm installs with pre/post CVE and audit checks.")
8
+ .version(pkg.version);
9
+
10
+ program
11
+ .command("preinstall")
12
+ .description("Run preinstall CVE check for packages being installed.")
13
+ .action(async () => {
14
+ const runPreinstall = require("../lib/preinstall");
15
+ await runPreinstall();
16
+ });
17
+
18
+ program
19
+ .command("postinstall")
20
+ .description("Run postinstall audit report for installed dependencies.")
21
+ .action(async () => {
22
+ const runPostinstall = require("../lib/postinstall");
23
+ await runPostinstall();
24
+ });
25
+
26
+ program.parseAsync(process.argv).catch((error) => {
27
+ console.error(error instanceof Error ? error.message : error);
28
+ process.exit(1);
29
+ });
package/lib/audit.js ADDED
@@ -0,0 +1,96 @@
1
+ const { execSync } = require("child_process");
2
+ const { error, warn } = require("./logger");
3
+
4
+ function parseAuditResults(parsed) {
5
+ const counts = {
6
+ critical: 0,
7
+ high: 0,
8
+ moderate: 0,
9
+ low: 0,
10
+ unknown: 0,
11
+ };
12
+
13
+ const packages = [];
14
+ const vulnerabilities =
15
+ parsed.metadata?.vulnerabilities || parsed.vulnerabilities || {};
16
+
17
+ if (typeof vulnerabilities === "object") {
18
+ Object.entries(vulnerabilities).forEach(([pkgName, data]) => {
19
+ const severity = String(data.severity || "unknown").toLowerCase();
20
+ counts[severity] = (counts[severity] || 0) + 1;
21
+ const identifiers = Array.isArray(data.via)
22
+ ? data.via
23
+ .map((entry) => {
24
+ if (typeof entry === "string") {
25
+ return entry;
26
+ }
27
+ return entry.source || entry.title || "unknown";
28
+ })
29
+ .join(", ")
30
+ : "";
31
+
32
+ packages.push({
33
+ packageName: pkgName,
34
+ version: data.version || "unknown",
35
+ severity,
36
+ identifiers: identifiers || "n/a",
37
+ });
38
+ });
39
+ }
40
+
41
+ if (parsed.advisories && typeof parsed.advisories === "object") {
42
+ Object.values(parsed.advisories).forEach((advisory) => {
43
+ const severity = String(advisory.severity || "unknown").toLowerCase();
44
+ counts[severity] = (counts[severity] || 0) + 1;
45
+
46
+ packages.push({
47
+ packageName: advisory.module_name || advisory.package || "unknown",
48
+ version: advisory.patched_versions || "unknown",
49
+ severity,
50
+ identifiers: advisory.cves?.join(", ") || advisory.id || "n/a",
51
+ });
52
+ });
53
+ }
54
+
55
+ return { severityCounts: counts, packages };
56
+ }
57
+
58
+ function safeParseJson(value) {
59
+ try {
60
+ return JSON.parse(value);
61
+ } catch (err) {
62
+ return null;
63
+ }
64
+ }
65
+
66
+ async function runAudit() {
67
+ try {
68
+ const stdout = execSync("npm audit --json", {
69
+ stdio: ["pipe", "pipe", "pipe"],
70
+ encoding: "utf8",
71
+ timeout: 20000,
72
+ });
73
+
74
+ const parsed = safeParseJson(stdout);
75
+ if (!parsed) {
76
+ return { error: "Invalid npm audit JSON response." };
77
+ }
78
+
79
+ return parseAuditResults(parsed);
80
+ } catch (errorObj) {
81
+ const stdout = errorObj.stdout ? String(errorObj.stdout) : null;
82
+ const parsed = stdout ? safeParseJson(stdout) : null;
83
+
84
+ if (parsed) {
85
+ return parseAuditResults(parsed);
86
+ }
87
+
88
+ warn("npm audit failed. Falling back to error details.");
89
+ error(errorObj.message || "npm audit execution failed.");
90
+ return { error: errorObj.message || "npm audit execution failed." };
91
+ }
92
+ }
93
+
94
+ module.exports = {
95
+ runAudit,
96
+ };
@@ -0,0 +1,27 @@
1
+ const SEVERITY_COLORS = {
2
+ critical: "red",
3
+ high: "orange",
4
+ moderate: "yellow",
5
+ low: "blue",
6
+ unknown: "gray",
7
+ };
8
+
9
+ const SEVERITY_ORDER = ["critical", "high", "moderate", "low", "unknown"];
10
+
11
+ const OSV_ENDPOINT = "https://api.osv.dev/v1/query";
12
+ const NPM_REGISTRY = "https://registry.npmjs.org";
13
+ const NPM_DOWNLOADS = "https://api.npmjs.org/downloads/point/last-week";
14
+ const PACKAGE_MANAGER = "npm";
15
+ const API_TIMEOUT_MS = 10000;
16
+ const API_RETRY_COUNT = 2;
17
+
18
+ module.exports = {
19
+ SEVERITY_COLORS,
20
+ SEVERITY_ORDER,
21
+ OSV_ENDPOINT,
22
+ NPM_REGISTRY,
23
+ NPM_DOWNLOADS,
24
+ PACKAGE_MANAGER,
25
+ API_TIMEOUT_MS,
26
+ API_RETRY_COUNT,
27
+ };
@@ -0,0 +1,109 @@
1
+ const chalk = require("chalk");
2
+ const Table = require("cli-table3");
3
+ const gradient = require("gradient-string");
4
+ const { SEVERITY_COLORS, SEVERITY_ORDER } = require("./constants");
5
+
6
+ function paintSeverity(severity) {
7
+ const key = String(severity || "unknown").toLowerCase();
8
+ const color = SEVERITY_COLORS[key] || SEVERITY_COLORS.unknown;
9
+ return chalk.keyword(color)(String(severity || "UNKNOWN").toUpperCase());
10
+ }
11
+
12
+ function formatVulnerabilityReport(reports) {
13
+ const lines = [];
14
+ reports.forEach((report) => {
15
+ lines.push(
16
+ chalk.bold.white(`
17
+ Package: ${report.packageName}`),
18
+ );
19
+ if (report.reputation) {
20
+ lines.push(formatPackageReputationSummary(report.reputation));
21
+ }
22
+
23
+ const table = new Table({
24
+ head: [
25
+ chalk.bold("Severity"),
26
+ chalk.bold("CVE / GHSA"),
27
+ chalk.bold("Summary"),
28
+ ],
29
+ colWidths: [14, 24, 70],
30
+ wordWrap: true,
31
+ });
32
+
33
+ report.vulnerabilities.forEach((vuln) => {
34
+ table.push([
35
+ paintSeverity(vuln.severity),
36
+ vuln.id,
37
+ vuln.summary || "No summary available",
38
+ ]);
39
+ });
40
+
41
+ lines.push(table.toString());
42
+ });
43
+
44
+ return lines.join("\n");
45
+ }
46
+
47
+ function formatPackageReputationSummary(reputation) {
48
+ if (!reputation) {
49
+ return "";
50
+ }
51
+
52
+ const downloads = reputation.weeklyDownloads
53
+ ? chalk.green(reputation.weeklyDownloads.toLocaleString())
54
+ : chalk.gray("N/A");
55
+ const published = reputation.lastPublishDate
56
+ ? chalk.yellow(
57
+ new Date(reputation.lastPublishDate).toISOString().split("T")[0],
58
+ )
59
+ : chalk.gray("Unknown");
60
+ const maintainers =
61
+ reputation.maintainersCount != null
62
+ ? chalk.cyan(reputation.maintainersCount)
63
+ : chalk.gray("Unknown");
64
+
65
+ return ` • Weekly downloads: ${downloads}\n • Last publish date: ${published}\n • Maintainers: ${maintainers}`;
66
+ }
67
+
68
+ function formatAuditReport(audit) {
69
+ const { severityCounts, packages } = audit;
70
+ const severityTable = new Table({
71
+ head: [chalk.bold("Severity"), chalk.bold("Count")],
72
+ colWidths: [18, 12],
73
+ });
74
+
75
+ SEVERITY_ORDER.forEach((severity) => {
76
+ const count = severityCounts[severity] || 0;
77
+ if (count > 0) {
78
+ severityTable.push([paintSeverity(severity), chalk.bold(String(count))]);
79
+ }
80
+ });
81
+
82
+ const packageTable = new Table({
83
+ head: [
84
+ chalk.bold("Package@Version"),
85
+ chalk.bold("Severity"),
86
+ chalk.bold("Identifier"),
87
+ ],
88
+ colWidths: [30, 14, 56],
89
+ wordWrap: true,
90
+ });
91
+
92
+ packages.forEach((item) => {
93
+ packageTable.push([
94
+ `${item.packageName}@${item.version || "unknown"}`,
95
+ paintSeverity(item.severity),
96
+ item.identifiers || "n/a",
97
+ ]);
98
+ });
99
+
100
+ const header = gradient.rainbow("DEPENDENCY SECURITY REPORT");
101
+ return `${header}\n\n${severityTable.toString()}\n\nAffected Packages:\n${packageTable.toString()}`;
102
+ }
103
+
104
+ module.exports = {
105
+ paintSeverity,
106
+ formatVulnerabilityReport,
107
+ formatPackageReputationSummary,
108
+ formatAuditReport,
109
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,54 @@
1
+ const chalk = require("chalk");
2
+ const boxen = require("boxen");
3
+ const gradient = require("gradient-string");
4
+
5
+ function logBox(title, message, options = {}) {
6
+ const borderColor = options.borderColor || "cyan";
7
+ const titleText = chalk.bold.cyan(` ${title} `);
8
+ const content = `${titleText}\n${message}`;
9
+
10
+ console.log(
11
+ boxen(content, {
12
+ padding: 1,
13
+ margin: 1,
14
+ borderColor,
15
+ borderStyle: "round",
16
+ float: "center",
17
+ backgroundColor: "black",
18
+ }),
19
+ );
20
+ }
21
+
22
+ function header(text) {
23
+ return gradient.cristal.bold(text);
24
+ }
25
+
26
+ function info(message) {
27
+ console.log(chalk.cyan(message));
28
+ }
29
+
30
+ function success(message) {
31
+ console.log(chalk.green(message));
32
+ }
33
+
34
+ function warn(message) {
35
+ console.log(chalk.keyword("orange")(message));
36
+ }
37
+
38
+ function error(message) {
39
+ console.error(chalk.red(message));
40
+ }
41
+
42
+ function strong(message) {
43
+ return chalk.bold.white(message);
44
+ }
45
+
46
+ module.exports = {
47
+ logBox,
48
+ header,
49
+ info,
50
+ success,
51
+ warn,
52
+ error,
53
+ strong,
54
+ };
package/lib/osv.js ADDED
@@ -0,0 +1,157 @@
1
+ const axios = require("axios");
2
+ const { warn } = require("./logger");
3
+ const {
4
+ OSV_ENDPOINT,
5
+ NPM_REGISTRY,
6
+ NPM_DOWNLOADS,
7
+ API_TIMEOUT_MS,
8
+ API_RETRY_COUNT,
9
+ } = require("./constants");
10
+
11
+ function deriveSeverity(vuln) {
12
+ if (!vuln) {
13
+ return "unknown";
14
+ }
15
+
16
+ const severityBlock = Array.isArray(vuln.severity)
17
+ ? vuln.severity[0]
18
+ : vuln.severity;
19
+ if (severityBlock && typeof severityBlock === "object") {
20
+ const score = parseFloat(severityBlock.score);
21
+ if (!Number.isNaN(score)) {
22
+ if (score >= 9) return "critical";
23
+ if (score >= 7) return "high";
24
+ if (score >= 4) return "moderate";
25
+ return "low";
26
+ }
27
+
28
+ if (severityBlock.type) {
29
+ const label = String(severityBlock.type).toLowerCase();
30
+ if (label.includes("critical")) return "critical";
31
+ if (label.includes("high")) return "high";
32
+ if (label.includes("moderate")) return "moderate";
33
+ if (label.includes("low")) return "low";
34
+ }
35
+ }
36
+
37
+ if (typeof severityBlock === "string") {
38
+ const label = severityBlock.toLowerCase();
39
+ if (label.includes("critical")) return "critical";
40
+ if (label.includes("high")) return "high";
41
+ if (label.includes("moderate")) return "moderate";
42
+ if (label.includes("low")) return "low";
43
+ }
44
+
45
+ if (Array.isArray(vuln.aliases)) {
46
+ const aliasText = vuln.aliases.join(" ").toLowerCase();
47
+ if (aliasText.includes("cve-")) {
48
+ return "high";
49
+ }
50
+ }
51
+
52
+ return "unknown";
53
+ }
54
+
55
+ async function requestWithRetry(url, payload) {
56
+ let attempt = 0;
57
+
58
+ while (attempt < API_RETRY_COUNT) {
59
+ try {
60
+ return await axios.post(url, payload, {
61
+ timeout: API_TIMEOUT_MS,
62
+ headers: {
63
+ "Content-Type": "application/json",
64
+ Accept: "application/json",
65
+ },
66
+ });
67
+ } catch (error) {
68
+ attempt += 1;
69
+ if (attempt >= API_RETRY_COUNT) {
70
+ throw error;
71
+ }
72
+ await new Promise((resolve) => setTimeout(resolve, 500));
73
+ }
74
+ }
75
+
76
+ throw new Error("OSV request failed after retries");
77
+ }
78
+
79
+ async function queryOsvForPackage(packageName) {
80
+ const payload = {
81
+ package: {
82
+ name: packageName,
83
+ ecosystem: "npm",
84
+ },
85
+ };
86
+
87
+ try {
88
+ const response = await requestWithRetry(OSV_ENDPOINT, payload);
89
+ const vulns = Array.isArray(response.data?.vulns)
90
+ ? response.data.vulns
91
+ : [];
92
+ return {
93
+ packageName,
94
+ vulnerabilities: vulns.map((vuln) => ({
95
+ id: String(vuln.id || vuln.aliases?.[0] || "UNKNOWN"),
96
+ summary: String(
97
+ vuln.summary || vuln.details || "No summary available",
98
+ ).trim(),
99
+ severity: deriveSeverity(vuln),
100
+ aliases: Array.isArray(vuln.aliases) ? vuln.aliases : [],
101
+ references: Array.isArray(vuln.references)
102
+ ? vuln.references.map((ref) => ref.url).filter(Boolean)
103
+ : [],
104
+ })),
105
+ };
106
+ } catch (error) {
107
+ warn(
108
+ `OSV query failed for ${packageName}: ${error.message || "request error"}`,
109
+ );
110
+ return {
111
+ packageName,
112
+ vulnerabilities: [],
113
+ };
114
+ }
115
+ }
116
+
117
+ async function fetchPackageMetadata(packageName) {
118
+ const url = `${NPM_REGISTRY}/${encodeURIComponent(packageName)}`;
119
+ const response = await axios.get(url, { timeout: API_TIMEOUT_MS });
120
+ return response.data;
121
+ }
122
+
123
+ async function fetchPackageDownloads(packageName) {
124
+ const url = `${NPM_DOWNLOADS}/${encodeURIComponent(packageName)}`;
125
+ const response = await axios.get(url, { timeout: API_TIMEOUT_MS });
126
+ return response.data;
127
+ }
128
+
129
+ async function fetchPackageReputation(packageName) {
130
+ try {
131
+ const [metadata, downloads] = await Promise.all([
132
+ fetchPackageMetadata(packageName),
133
+ fetchPackageDownloads(packageName),
134
+ ]);
135
+
136
+ const latestTag = metadata["dist-tags"]?.latest;
137
+ const lastPublishDate = metadata.time?.[latestTag] || null;
138
+
139
+ return {
140
+ weeklyDownloads: downloads?.downloads || 0,
141
+ lastPublishDate,
142
+ maintainersCount: Array.isArray(metadata.maintainers)
143
+ ? metadata.maintainers.length
144
+ : 0,
145
+ };
146
+ } catch (error) {
147
+ warn(
148
+ `Unable to fetch reputation for ${packageName}: ${error.message || "network failure"}`,
149
+ );
150
+ return null;
151
+ }
152
+ }
153
+
154
+ module.exports = {
155
+ queryOsvForPackage,
156
+ fetchPackageReputation,
157
+ };
@@ -0,0 +1,31 @@
1
+ const ora = require("ora");
2
+ const { runAudit } = require("./audit");
3
+ const { formatAuditReport } = require("./formatter");
4
+ const { logBox, success, warn, error } = require("./logger");
5
+
6
+ async function runPostinstall() {
7
+ const spinner = ora("Running npm audit postinstall...").start();
8
+
9
+ const auditResult = await runAudit();
10
+ spinner.stop();
11
+
12
+ if (auditResult.error) {
13
+ warn("npm audit could not complete successfully.");
14
+ error(auditResult.error);
15
+ return;
16
+ }
17
+
18
+ if (
19
+ !Array.isArray(auditResult.packages) ||
20
+ auditResult.packages.length === 0
21
+ ) {
22
+ success("No dependency vulnerabilities found by npm audit.");
23
+ return;
24
+ }
25
+
26
+ logBox("DEPENDENCY SECURITY REPORT", formatAuditReport(auditResult), {
27
+ borderColor: "yellow",
28
+ });
29
+ }
30
+
31
+ module.exports = runPostinstall;
@@ -0,0 +1,99 @@
1
+ const path = require("path");
2
+ const fs = require("fs-extra");
3
+ const ora = require("ora");
4
+ const {
5
+ getNpmConfigArgv,
6
+ extractInstallTargetsFromNpmArgv,
7
+ } = require("./utils");
8
+ const { queryOsvForPackage, fetchPackageReputation } = require("./osv");
9
+ const {
10
+ formatVulnerabilityReport,
11
+ formatPackageReputationSummary,
12
+ } = require("./formatter");
13
+ const { logBox, info, success, warn } = require("./logger");
14
+ const { askContinue } = require("./prompt");
15
+
16
+ async function loadPackageJson() {
17
+ const packagePath = path.join(process.cwd(), "package.json");
18
+ if (await fs.pathExists(packagePath)) {
19
+ try {
20
+ return await fs.readJson(packagePath);
21
+ } catch {
22
+ return null;
23
+ }
24
+ }
25
+ return null;
26
+ }
27
+
28
+ async function runPreinstall() {
29
+ const packageJson = await loadPackageJson();
30
+ const rawArgv = getNpmConfigArgv();
31
+ const installTargets = extractInstallTargetsFromNpmArgv(rawArgv);
32
+
33
+ if (!installTargets.length) {
34
+ info("No install target packages detected. Skipping CVE pre-install scan.");
35
+ return;
36
+ }
37
+
38
+ const uniqueTargets = [...new Set(installTargets)];
39
+ const spinner = ora(
40
+ "Scanning requested packages for CVE exposure...",
41
+ ).start();
42
+ const results = [];
43
+
44
+ for (const packageName of uniqueTargets) {
45
+ spinner.text = `Scanning ${packageName}`;
46
+ const [osvResult, reputation] = await Promise.all([
47
+ queryOsvForPackage(packageName),
48
+ fetchPackageReputation(packageName),
49
+ ]);
50
+
51
+ results.push({
52
+ packageName,
53
+ vulnerabilities: osvResult.vulnerabilities,
54
+ reputation,
55
+ });
56
+ }
57
+
58
+ spinner.stop();
59
+
60
+ const vulnerablePackages = results.filter(
61
+ (result) => result.vulnerabilities.length > 0,
62
+ );
63
+
64
+ logBox(
65
+ "CVE PRE-INSTALL CHECK",
66
+ "Review detected issues before continuing installation.",
67
+ {
68
+ borderColor: vulnerablePackages.length > 0 ? "red" : "green",
69
+ },
70
+ );
71
+
72
+ if (!vulnerablePackages.length) {
73
+ success("No known CVEs detected for the requested install packages.");
74
+ return;
75
+ }
76
+
77
+ console.log(formatVulnerabilityReport(vulnerablePackages));
78
+
79
+ const reputationInfo = results
80
+ .filter((result) => result.reputation)
81
+ .map(
82
+ (result) =>
83
+ `\n${result.packageName}\n${formatPackageReputationSummary(result.reputation)}`,
84
+ )
85
+ .join("\n");
86
+
87
+ if (reputationInfo) {
88
+ console.log(reputationInfo);
89
+ }
90
+
91
+ const shouldContinue = await askContinue();
92
+ if (!shouldContinue) {
93
+ process.exit(1);
94
+ }
95
+
96
+ success("Installation will continue.");
97
+ }
98
+
99
+ module.exports = runPreinstall;
package/lib/prompt.js ADDED
@@ -0,0 +1,41 @@
1
+ const inquirer = require("inquirer");
2
+ const { success, error } = require("./logger");
3
+
4
+ async function askContinue() {
5
+ const question = [
6
+ {
7
+ type: "input",
8
+ name: "confirm",
9
+ message: "Continue installation? (y/n):",
10
+ validate: (value) => {
11
+ if (typeof value !== "string") {
12
+ return "Please enter y or n.";
13
+ }
14
+
15
+ const normalized = value.trim().toLowerCase();
16
+ if (["y", "yes", "n", "no"].includes(normalized)) {
17
+ return true;
18
+ }
19
+
20
+ return "Answer must be y/yes or n/no.";
21
+ },
22
+ filter: (value) =>
23
+ typeof value === "string" ? value.trim().toLowerCase() : value,
24
+ },
25
+ ];
26
+
27
+ const { confirm } = await inquirer.prompt(question);
28
+ const accepted = ["y", "yes"].includes(confirm);
29
+
30
+ if (accepted) {
31
+ success("✔ Continuing installation...");
32
+ return true;
33
+ }
34
+
35
+ error("❌ Installation aborted by user.");
36
+ return false;
37
+ }
38
+
39
+ module.exports = {
40
+ askContinue,
41
+ };
package/lib/utils.js ADDED
@@ -0,0 +1,103 @@
1
+ function safeJsonParse(value, fallback = null) {
2
+ if (typeof value !== "string") {
3
+ return fallback;
4
+ }
5
+
6
+ try {
7
+ return JSON.parse(value);
8
+ } catch (error) {
9
+ return fallback;
10
+ }
11
+ }
12
+
13
+ function getNpmConfigArgv() {
14
+ const rawArgv = process.env.npm_config_argv;
15
+ if (!rawArgv) {
16
+ return null;
17
+ }
18
+
19
+ if (typeof rawArgv === "string") {
20
+ return safeJsonParse(rawArgv, null);
21
+ }
22
+
23
+ if (typeof rawArgv === "object") {
24
+ return rawArgv;
25
+ }
26
+
27
+ return null;
28
+ }
29
+
30
+ function normalizePackageToken(token) {
31
+ if (typeof token !== "string") {
32
+ return null;
33
+ }
34
+
35
+ const trimmed = token.trim();
36
+ if (!trimmed || trimmed.startsWith("-")) {
37
+ return null;
38
+ }
39
+
40
+ if (trimmed.startsWith("@")) {
41
+ const secondAt = trimmed.indexOf("@", 1);
42
+ return secondAt > 0 ? trimmed.slice(0, secondAt) : trimmed;
43
+ }
44
+
45
+ const firstAt = trimmed.indexOf("@");
46
+ return firstAt > 0 ? trimmed.slice(0, firstAt) : trimmed;
47
+ }
48
+
49
+ function isInstallCommand(argvArray) {
50
+ if (!Array.isArray(argvArray)) {
51
+ return false;
52
+ }
53
+
54
+ return argvArray.some((token) => ["install", "i", "add"].includes(token));
55
+ }
56
+
57
+ function extractInstallTargetsFromNpmArgv(argv) {
58
+ if (!argv || typeof argv !== "object") {
59
+ return [];
60
+ }
61
+
62
+ const rawTokens = Array.isArray(argv.original)
63
+ ? argv.original
64
+ : Array.isArray(argv.cooked)
65
+ ? argv.cooked
66
+ : Array.isArray(argv)
67
+ ? argv
68
+ : [];
69
+
70
+ if (!isInstallCommand(rawTokens)) {
71
+ return [];
72
+ }
73
+
74
+ const targets = [];
75
+ for (const token of rawTokens) {
76
+ if (typeof token !== "string") {
77
+ continue;
78
+ }
79
+
80
+ const lower = token.toLowerCase();
81
+ if (["npm", "install", "i", "add", "update", "audit"].includes(lower)) {
82
+ continue;
83
+ }
84
+
85
+ if (lower.startsWith("-")) {
86
+ continue;
87
+ }
88
+
89
+ const pkgName = normalizePackageToken(token);
90
+ if (pkgName) {
91
+ targets.push(pkgName);
92
+ }
93
+ }
94
+
95
+ return targets;
96
+ }
97
+
98
+ module.exports = {
99
+ safeJsonParse,
100
+ getNpmConfigArgv,
101
+ extractInstallTargetsFromNpmArgv,
102
+ normalizePackageToken,
103
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "cve-guard-npm",
3
+ "version": "1.0.0",
4
+ "description": "A lifecycle hook guard for npm installs that scans CVEs before package installation and audits dependencies after install.",
5
+ "author": "Naveen Rawat <naveenrawat51@gmail.com>",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "cve-guard-npm": "./bin/cli.js"
9
+ },
10
+ "main": "lib/preinstall.js",
11
+ "files": [
12
+ "bin",
13
+ "lib",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "keywords": [
18
+ "npm",
19
+ "security",
20
+ "CVE",
21
+ "audit",
22
+ "npm-hooks",
23
+ "osv",
24
+ "vulnerability"
25
+ ],
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "https://github.com/naveenrawat51/cve-guard-npm.git"
29
+ },
30
+ "engines": {
31
+ "node": ">=18"
32
+ },
33
+ "dependencies": {
34
+ "axios": "^1.6.0",
35
+ "boxen": "^7.1.0",
36
+ "chalk": "^5.3.0",
37
+ "cli-table3": "^0.6.3",
38
+ "commander": "^11.0.0",
39
+ "fs-extra": "^11.1.1",
40
+ "gradient-string": "^2.0.1",
41
+ "inquirer": "^9.2.10",
42
+ "ora": "^7.1.1"
43
+ }
44
+ }