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 +21 -0
- package/README.md +147 -0
- package/bin/cli.js +29 -0
- package/lib/audit.js +96 -0
- package/lib/constants.js +27 -0
- package/lib/formatter.js +109 -0
- package/lib/logger.js +54 -0
- package/lib/osv.js +157 -0
- package/lib/postinstall.js +31 -0
- package/lib/preinstall.js +99 -0
- package/lib/prompt.js +41 -0
- package/lib/utils.js +103 -0
- package/package.json +44 -0
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
|
+

|
|
126
|
+
|
|
127
|
+

|
|
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
|
+
};
|
package/lib/constants.js
ADDED
|
@@ -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
|
+
};
|
package/lib/formatter.js
ADDED
|
@@ -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
|
+
}
|