osv-depguard 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/README.md +75 -0
- package/depguard.js +448 -0
- package/package.json +26 -0
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# OSV-DepGuard π‘οΈ
|
|
2
|
+
|
|
3
|
+
**Deterministic Hybrid Vulnerability Scanner for Node.js projects.**
|
|
4
|
+
|
|
5
|
+
| Layer | Tool | Role |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| **Scanner** | OSV.dev (Google) | 100% deterministic CVE lookup β no hallucination |
|
|
8
|
+
| **Source** | `package-lock.json` | Exact installed versions, not semver ranges |
|
|
9
|
+
| **AI** | Claude (Anthropic) | Interprets OSV data into plain English + fix commands |
|
|
10
|
+
| **UI** | chalk + cli-table3 | Colour-coded terminal table |
|
|
11
|
+
|
|
12
|
+
## Setup
|
|
13
|
+
|
|
14
|
+
### 1. Install dependencies
|
|
15
|
+
```bash
|
|
16
|
+
npm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### 2. API key β add to .env
|
|
20
|
+
```
|
|
21
|
+
ANTHROPIC_API_KEY=sk-ant-...
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### IMPORTANT β do this immediately:
|
|
25
|
+
```bash
|
|
26
|
+
echo ".env" >> .gitignore
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
DepGuard will warn you at startup if .env is missing from .gitignore.
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
node depguard.js # scan ./package-lock.json
|
|
35
|
+
node depguard.js ~/projects/my-app # scan a specific directory
|
|
36
|
+
node depguard.js --no-dev # skip devDependencies
|
|
37
|
+
node depguard.js --min-severity high # only HIGH + CRITICAL
|
|
38
|
+
node depguard.js --json # raw JSON output for CI
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Install globally
|
|
42
|
+
```bash
|
|
43
|
+
npm install -g .
|
|
44
|
+
depguard
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
package-lock.json
|
|
51
|
+
β
|
|
52
|
+
βΌ exact installed versions
|
|
53
|
+
OSV.dev /v1/querybatch βββΊ real CVE data, zero hallucination
|
|
54
|
+
β
|
|
55
|
+
βΌ (if vulns found)
|
|
56
|
+
Anthropic API βββββββββββΊ plain English summary + remediation
|
|
57
|
+
(no web search β interprets OSV data only, cannot fabricate vulns)
|
|
58
|
+
β
|
|
59
|
+
βΌ
|
|
60
|
+
cli-table3 + chalk ββββββΊ colour-coded terminal table
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Security notes
|
|
64
|
+
|
|
65
|
+
- Never hardcode your API key. Use `.env` via dotenv.
|
|
66
|
+
- Always add `.env` to `.gitignore` before your first commit.
|
|
67
|
+
- OSV.dev is a public API β no key required, only package names + versions are sent.
|
|
68
|
+
|
|
69
|
+
## CI integration
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
node depguard.js --json --min-severity high | jq '.[].package'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Exit code `1` = scan failed (missing lockfile, API error). Exit code `0` = completed (check JSON for vulns).
|
package/depguard.js
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* OSV - DepGuard β Deterministic Hybrid Vulnerability Scanner
|
|
5
|
+
*
|
|
6
|
+
* Pipeline:
|
|
7
|
+
* 1. Parse package-lock.json β exact installed versions
|
|
8
|
+
* 2. Batch query OSV.dev API β deterministic, real CVE data
|
|
9
|
+
* 3. Send OSV results to AI β human-readable summaries & remediation steps only
|
|
10
|
+
* 4. Render colour-coded table via chalk + cli-table3
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import "dotenv/config";
|
|
14
|
+
import chalk from "chalk";
|
|
15
|
+
import ora from "ora";
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
import Table from "cli-table3";
|
|
18
|
+
import fs from "fs";
|
|
19
|
+
import path from "path";
|
|
20
|
+
|
|
21
|
+
// βββ API key guard ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
22
|
+
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
|
|
23
|
+
if (!ANTHROPIC_API_KEY) {
|
|
24
|
+
console.error(
|
|
25
|
+
chalk.red("\n β ANTHROPIC_API_KEY is not set.\n") +
|
|
26
|
+
chalk.gray(" Add it to a .env file or export it in your shell:\n\n") +
|
|
27
|
+
chalk.white(" echo 'ANTHROPIC_API_KEY=sk-ant-...' >> .env\n") +
|
|
28
|
+
chalk.yellow("\n β Make sure .env is listed in your .gitignore!\n")
|
|
29
|
+
);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// βββ CLI ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
34
|
+
const program = new Command();
|
|
35
|
+
program
|
|
36
|
+
.name("depguard")
|
|
37
|
+
.description("Deterministic hybrid dependency vulnerability scanner")
|
|
38
|
+
.version("1.0.0")
|
|
39
|
+
.argument("[path]", "Directory containing package-lock.json", ".")
|
|
40
|
+
.option("--no-dev", "Skip devDependencies")
|
|
41
|
+
.option(
|
|
42
|
+
"--min-severity <level>",
|
|
43
|
+
"Minimum severity to show: low | medium | high | critical",
|
|
44
|
+
"low"
|
|
45
|
+
)
|
|
46
|
+
.option("--json", "Output raw JSON instead of table")
|
|
47
|
+
.parse(process.argv);
|
|
48
|
+
|
|
49
|
+
const opts = program.opts();
|
|
50
|
+
const [scanDir] = program.args.length ? program.args : ["."];
|
|
51
|
+
const lockPath = path.resolve(scanDir, "package-lock.json");
|
|
52
|
+
|
|
53
|
+
// βββ Load package-lock.json βββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
54
|
+
if (!fs.existsSync(lockPath)) {
|
|
55
|
+
console.error(
|
|
56
|
+
chalk.red(`\n β Cannot find package-lock.json at: ${lockPath}`) +
|
|
57
|
+
chalk.gray("\n Run `npm install` first to generate a lockfile.\n")
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, "utf-8"));
|
|
63
|
+
const lockVersion = lock.lockfileVersion || 1;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Extract exact installed package versions from the lockfile.
|
|
67
|
+
* Supports lockfileVersion 1, 2, and 3.
|
|
68
|
+
*/
|
|
69
|
+
function extractPackages(lock, includeDev) {
|
|
70
|
+
const packages = {};
|
|
71
|
+
|
|
72
|
+
if (lockVersion >= 2 && lock.packages) {
|
|
73
|
+
// v2 / v3: "packages" map β keys like "node_modules/chalk"
|
|
74
|
+
for (const [key, meta] of Object.entries(lock.packages)) {
|
|
75
|
+
if (!key || key === "") continue; // skip the root project entry
|
|
76
|
+
if (!includeDev && meta.dev) continue;
|
|
77
|
+
const name = key.replace(/^.*node_modules\//, "");
|
|
78
|
+
if (name && meta.version) packages[name] = meta.version;
|
|
79
|
+
}
|
|
80
|
+
} else if (lock.dependencies) {
|
|
81
|
+
for (const [name, meta] of Object.entries(lock.dependencies)) {
|
|
82
|
+
if (!includeDev && meta.dev) continue;
|
|
83
|
+
if (meta.version) packages[name] = meta.version;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return packages;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const packageMap = extractPackages(lock, opts.dev !== false);
|
|
91
|
+
const packageEntries = Object.entries(packageMap);
|
|
92
|
+
|
|
93
|
+
if (packageEntries.length === 0) {
|
|
94
|
+
console.log(chalk.yellow("\n No packages found in lockfile.\n"));
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// βββ OSV.dev batch query ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
99
|
+
/**
|
|
100
|
+
* OSV batch endpoint β up to 1000 queries per call.
|
|
101
|
+
* https://google.github.io/osv.dev/post-v1-querybatch
|
|
102
|
+
*/
|
|
103
|
+
async function queryOSV(packages) {
|
|
104
|
+
const queries = packages.map(([name, version]) => ({
|
|
105
|
+
version,
|
|
106
|
+
package: { name, ecosystem: "npm" },
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const BATCH_SIZE = 1000;
|
|
110
|
+
const allResults = [];
|
|
111
|
+
|
|
112
|
+
for (let i = 0; i < queries.length; i += BATCH_SIZE) {
|
|
113
|
+
const batch = queries.slice(i, i + BATCH_SIZE);
|
|
114
|
+
const res = await fetch("https://api.osv.dev/v1/querybatch", {
|
|
115
|
+
method: "POST",
|
|
116
|
+
headers: { "Content-Type": "application/json" },
|
|
117
|
+
body: JSON.stringify({ queries: batch }),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!res.ok) throw new Error(`OSV API ${res.status}: ${res.statusText}`);
|
|
121
|
+
|
|
122
|
+
const data = await res.json();
|
|
123
|
+
allResults.push(...(data.results || []));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return allResults;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// βββ Severity helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
130
|
+
const SEVERITY_RANK = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1, UNKNOWN: 0 };
|
|
131
|
+
const MIN_RANK =
|
|
132
|
+
SEVERITY_RANK[(opts.minSeverity || "low").toUpperCase()] ?? 1;
|
|
133
|
+
|
|
134
|
+
function extractSeverity(vuln) {
|
|
135
|
+
const candidates = [
|
|
136
|
+
...(vuln.severity || []),
|
|
137
|
+
...(vuln.database_specific?.severity
|
|
138
|
+
? [{ score: vuln.database_specific.severity }]
|
|
139
|
+
: []),
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
for (const s of candidates) {
|
|
143
|
+
const score = (s.score || "").toUpperCase();
|
|
144
|
+
if (["CRITICAL", "HIGH", "MEDIUM", "LOW"].includes(score)) return score;
|
|
145
|
+
const num = parseFloat(score);
|
|
146
|
+
if (!isNaN(num)) {
|
|
147
|
+
if (num >= 9.0) return "CRITICAL";
|
|
148
|
+
if (num >= 7.0) return "HIGH";
|
|
149
|
+
if (num >= 4.0) return "MEDIUM";
|
|
150
|
+
return "LOW";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return "UNKNOWN";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function colourSeverity(sev) {
|
|
157
|
+
switch (sev) {
|
|
158
|
+
case "CRITICAL": return chalk.bgRed.white.bold(` ${sev} `);
|
|
159
|
+
case "HIGH": return chalk.red.bold(sev);
|
|
160
|
+
case "MEDIUM": return chalk.yellow.bold(sev);
|
|
161
|
+
case "LOW": return chalk.blue(sev);
|
|
162
|
+
default: return chalk.gray(sev);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// βββ Build vulnerability list from OSV data βββββββββββββββββββββββββββββββββββ
|
|
167
|
+
function buildVulnList(packages, osvResults) {
|
|
168
|
+
const vulns = [];
|
|
169
|
+
|
|
170
|
+
packages.forEach(([name, version], idx) => {
|
|
171
|
+
const result = osvResults[idx];
|
|
172
|
+
if (!result?.vulns?.length) return;
|
|
173
|
+
|
|
174
|
+
for (const vuln of result.vulns) {
|
|
175
|
+
const severity = extractSeverity(vuln);
|
|
176
|
+
if ((SEVERITY_RANK[severity] ?? 0) < MIN_RANK) continue;
|
|
177
|
+
|
|
178
|
+
// Extract fixed version from affected ranges
|
|
179
|
+
const fixedVersions = (vuln.affected || [])
|
|
180
|
+
.flatMap((a) => a.ranges || [])
|
|
181
|
+
.flatMap((r) => r.events || [])
|
|
182
|
+
.map((e) => e.fixed)
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
|
|
185
|
+
vulns.push({
|
|
186
|
+
package: name,
|
|
187
|
+
version,
|
|
188
|
+
id: vuln.id,
|
|
189
|
+
aliases: (vuln.aliases || []).filter((a) => a.startsWith("CVE-")),
|
|
190
|
+
severity,
|
|
191
|
+
summary: vuln.summary || "No summary available",
|
|
192
|
+
details: vuln.details || "",
|
|
193
|
+
fixedIn: fixedVersions[0] || null,
|
|
194
|
+
references: (vuln.references || []).map((r) => r.url).slice(0, 2),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Sort: highest severity first, then alphabetically by package name
|
|
200
|
+
vulns.sort(
|
|
201
|
+
(a, b) =>
|
|
202
|
+
(SEVERITY_RANK[b.severity] ?? 0) - (SEVERITY_RANK[a.severity] ?? 0) ||
|
|
203
|
+
a.package.localeCompare(b.package)
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
return vulns;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// βββ AI enrichment (interpretation only β no web search) βββββββββββββββββββββ
|
|
210
|
+
/**
|
|
211
|
+
* Claude (or any other AI model) receives only the verified OSV data and produces:
|
|
212
|
+
* - humanSummary: plain English explanation of the real risk
|
|
213
|
+
* - remediationStep: concrete actionable fix command
|
|
214
|
+
*
|
|
215
|
+
* No tools, no web search β Claude cannot invent vulnerabilities.
|
|
216
|
+
*/
|
|
217
|
+
async function enrichWithAI(vulns) {
|
|
218
|
+
if (vulns.length === 0) return [];
|
|
219
|
+
|
|
220
|
+
const payload = vulns.map((v) => ({
|
|
221
|
+
id: v.id,
|
|
222
|
+
package: v.package,
|
|
223
|
+
installedVersion: v.version,
|
|
224
|
+
severity: v.severity,
|
|
225
|
+
summary: v.summary,
|
|
226
|
+
details: v.details.slice(0, 600), // keep prompt size reasonable
|
|
227
|
+
fixedIn: v.fixedIn,
|
|
228
|
+
aliases: v.aliases,
|
|
229
|
+
}));
|
|
230
|
+
|
|
231
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers: {
|
|
234
|
+
"Content-Type": "application/json",
|
|
235
|
+
"x-api-key": ANTHROPIC_API_KEY,
|
|
236
|
+
"anthropic-version": "2023-06-01",
|
|
237
|
+
},
|
|
238
|
+
body: JSON.stringify({
|
|
239
|
+
model: "claude-sonnet-4-20250514",
|
|
240
|
+
max_tokens: 1000,
|
|
241
|
+
system: `You are a security advisor. You will receive structured vulnerability data sourced directly from the OSV.dev database.
|
|
242
|
+
Your only job is to interpret this data and produce output that is easier for developers to act on.
|
|
243
|
+
Do NOT invent, assume, or add any information not present in the input.
|
|
244
|
+
Respond ONLY with a valid JSON array (no markdown, no backticks, no preamble):
|
|
245
|
+
[
|
|
246
|
+
{
|
|
247
|
+
"id": "<OSV id from input>",
|
|
248
|
+
"humanSummary": "2-3 sentences in plain English describing the risk, attack vector, and potential impact",
|
|
249
|
+
"remediationStep": "A single specific command or action the developer should take (e.g. 'Run: npm install packageName@X.Y.Z')"
|
|
250
|
+
}
|
|
251
|
+
]`,
|
|
252
|
+
messages: [
|
|
253
|
+
{
|
|
254
|
+
role: "user",
|
|
255
|
+
content: `Generate human-readable summaries and remediation steps for these verified vulnerabilities:\n\n${JSON.stringify(payload, null, 2)}`,
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
}),
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
if (!res.ok) throw new Error(`Anthropic API ${res.status}: ${res.statusText}`);
|
|
262
|
+
|
|
263
|
+
const data = await res.json();
|
|
264
|
+
const text =
|
|
265
|
+
data.content
|
|
266
|
+
?.filter((b) => b.type === "text")
|
|
267
|
+
.map((b) => b.text)
|
|
268
|
+
.join("") || "[]";
|
|
269
|
+
|
|
270
|
+
const clean = text.replace(/```json|```/g, "").trim();
|
|
271
|
+
return JSON.parse(clean);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// βββ Render table βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
275
|
+
function renderTable(enriched) {
|
|
276
|
+
const table = new Table({
|
|
277
|
+
head: [
|
|
278
|
+
chalk.bold.white("Package"),
|
|
279
|
+
chalk.bold.white("Installed"),
|
|
280
|
+
chalk.bold.white("ID / CVE"),
|
|
281
|
+
chalk.bold.white("Severity"),
|
|
282
|
+
chalk.bold.white("Human Summary"),
|
|
283
|
+
chalk.bold.white("Remediation"),
|
|
284
|
+
],
|
|
285
|
+
colWidths: [20, 11, 22, 12, 44, 34],
|
|
286
|
+
wordWrap: true,
|
|
287
|
+
style: { head: [], border: ["gray"] },
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
for (const v of enriched) {
|
|
291
|
+
const cveLabel = v.aliases?.length
|
|
292
|
+
? chalk.gray("\n" + v.aliases.join(", "))
|
|
293
|
+
: "";
|
|
294
|
+
|
|
295
|
+
table.push([
|
|
296
|
+
chalk.bold.white(v.package),
|
|
297
|
+
chalk.gray(v.version),
|
|
298
|
+
chalk.cyan(v.id) + cveLabel,
|
|
299
|
+
colourSeverity(v.severity),
|
|
300
|
+
v.humanSummary || v.summary,
|
|
301
|
+
v.remediationStep ||
|
|
302
|
+
(v.fixedIn
|
|
303
|
+
? chalk.green(`npm i ${v.package}@${v.fixedIn}`)
|
|
304
|
+
: chalk.gray("No fix available")),
|
|
305
|
+
]);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
console.log(table.toString());
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// βββ .gitignore check ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
312
|
+
function checkGitignore(dir) {
|
|
313
|
+
const gitignorePath = path.resolve(dir, ".gitignore");
|
|
314
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
315
|
+
console.log(
|
|
316
|
+
chalk.yellow(" β No .gitignore found.\n") +
|
|
317
|
+
chalk.gray(' Create one and add .env:\n') +
|
|
318
|
+
chalk.white(' echo ".env" >> .gitignore\n')
|
|
319
|
+
);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
323
|
+
if (!content.split("\n").some((l) => l.trim() === ".env")) {
|
|
324
|
+
console.log(
|
|
325
|
+
chalk.yellow(" β .env is not in your .gitignore β your API key could be exposed!\n") +
|
|
326
|
+
chalk.white(' Fix it now: echo ".env" >> .gitignore\n')
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// βββ Main βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
332
|
+
async function main() {
|
|
333
|
+
console.log(
|
|
334
|
+
"\n" +
|
|
335
|
+
chalk.bold.cyan(" DepGuard") +
|
|
336
|
+
chalk.bold.gray(" v2") +
|
|
337
|
+
chalk.gray(" Β· Deterministic Hybrid Scanner\n") +
|
|
338
|
+
chalk.gray(` Lockfile : `) + chalk.white(lockPath) + "\n" +
|
|
339
|
+
chalk.gray(` Packages : `) + chalk.white(packageEntries.length) +
|
|
340
|
+
chalk.gray(` (lockfileVersion ${lockVersion})\n`)
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
checkGitignore(scanDir);
|
|
344
|
+
|
|
345
|
+
// ββ Step 1: Query OSV.dev βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
346
|
+
const osvSpinner = ora({
|
|
347
|
+
text: chalk.gray(`Querying OSV.dev for ${packageEntries.length} packagesβ¦`),
|
|
348
|
+
color: "cyan",
|
|
349
|
+
}).start();
|
|
350
|
+
|
|
351
|
+
let osvResults;
|
|
352
|
+
try {
|
|
353
|
+
osvResults = await queryOSV(packageEntries);
|
|
354
|
+
osvSpinner.succeed(chalk.green("OSV.dev scan complete β deterministic results"));
|
|
355
|
+
} catch (err) {
|
|
356
|
+
osvSpinner.fail(chalk.red("OSV.dev query failed: " + err.message));
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ββ Step 2: Build vuln list βββββββββββββββββββββββββββββββββββββββββββββββ
|
|
361
|
+
const vulns = buildVulnList(packageEntries, osvResults);
|
|
362
|
+
|
|
363
|
+
if (vulns.length === 0) {
|
|
364
|
+
console.log(
|
|
365
|
+
chalk.green(
|
|
366
|
+
"\n β No vulnerabilities found" +
|
|
367
|
+
(opts.minSeverity !== "low" ? ` at or above ${opts.minSeverity} severity` : "") +
|
|
368
|
+
".\n"
|
|
369
|
+
)
|
|
370
|
+
);
|
|
371
|
+
process.exit(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
console.log(
|
|
375
|
+
chalk.gray(`\n Found `) +
|
|
376
|
+
chalk.bold.red(vulns.length) +
|
|
377
|
+
chalk.gray(` vulnerabilit${vulns.length === 1 ? "y" : "ies"}`) +
|
|
378
|
+
chalk.gray(" β sending to AI for interpretationβ¦\n")
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// ββ Step 3: AI enrichment βββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
382
|
+
const aiSpinner = ora({
|
|
383
|
+
text: chalk.gray("Generating human-readable summaries & remediation stepsβ¦"),
|
|
384
|
+
color: "cyan",
|
|
385
|
+
}).start();
|
|
386
|
+
|
|
387
|
+
let aiData = [];
|
|
388
|
+
try {
|
|
389
|
+
aiData = await enrichWithAI(vulns);
|
|
390
|
+
aiSpinner.succeed(chalk.green("AI interpretation complete"));
|
|
391
|
+
} catch (err) {
|
|
392
|
+
aiSpinner.warn(
|
|
393
|
+
chalk.yellow("AI enrichment failed β falling back to raw OSV summaries\n ") +
|
|
394
|
+
chalk.gray(err.message)
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Merge AI data into vuln objects
|
|
399
|
+
const aiMap = Object.fromEntries((aiData || []).map((a) => [a.id, a]));
|
|
400
|
+
const enriched = vulns.map((v) => ({
|
|
401
|
+
...v,
|
|
402
|
+
humanSummary: aiMap[v.id]?.humanSummary || v.summary,
|
|
403
|
+
remediationStep:
|
|
404
|
+
aiMap[v.id]?.remediationStep ||
|
|
405
|
+
(v.fixedIn ? `Run: npm install ${v.package}@${v.fixedIn}` : "No fix available"),
|
|
406
|
+
}));
|
|
407
|
+
|
|
408
|
+
// ββ Step 4: Output ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
409
|
+
if (opts.json) {
|
|
410
|
+
console.log(JSON.stringify(enriched, null, 2));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
console.log();
|
|
415
|
+
renderTable(enriched);
|
|
416
|
+
|
|
417
|
+
// Summary bar
|
|
418
|
+
const counts = enriched.reduce((acc, v) => {
|
|
419
|
+
acc[v.severity] = (acc[v.severity] || 0) + 1;
|
|
420
|
+
return acc;
|
|
421
|
+
}, {});
|
|
422
|
+
|
|
423
|
+
const summaryParts = ["CRITICAL", "HIGH", "MEDIUM", "LOW", "UNKNOWN"]
|
|
424
|
+
.filter((s) => counts[s])
|
|
425
|
+
.map((s) => colourSeverity(s) + chalk.gray(` Γ${counts[s]}`));
|
|
426
|
+
|
|
427
|
+
console.log(
|
|
428
|
+
"\n " + chalk.bold("Summary ") + summaryParts.join(chalk.gray(" ")) + "\n"
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Advisory references
|
|
432
|
+
const withRefs = enriched.filter((v) => v.references?.length);
|
|
433
|
+
if (withRefs.length > 0) {
|
|
434
|
+
console.log(chalk.bold.gray(" Advisory Links"));
|
|
435
|
+
for (const v of withRefs) {
|
|
436
|
+
console.log(chalk.gray(` ${chalk.cyan(v.id)}`));
|
|
437
|
+
v.references.forEach((url) =>
|
|
438
|
+
console.log(chalk.gray(" β ") + chalk.underline(url))
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
console.log();
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
main().catch((err) => {
|
|
446
|
+
console.error(chalk.red("\n Unexpected error: " + err.message));
|
|
447
|
+
process.exit(1);
|
|
448
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "osv-depguard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scan npm dependencies for vulnerabilities via OSV.dev + AI summaries",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"osv-depguard": "./depguard.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": { "node": ">=18.0.0" },
|
|
10
|
+
"keywords": ["security", "vulnerability", "osv", "npm audit", "cli"],
|
|
11
|
+
"author": "Abbas Uddin",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://github.com/CodeAbbas/osv-depguard",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/CodeAbbas/osv-depguard.git"
|
|
17
|
+
},
|
|
18
|
+
"bugs": { "url": "https://github.com/CodeAbbas/osv-depguard/issues" },
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"chalk": "^5.3.0",
|
|
21
|
+
"cli-table3": "^0.6.5",
|
|
22
|
+
"commander": "^12.0.0",
|
|
23
|
+
"dotenv": "^16.4.5",
|
|
24
|
+
"ora": "^8.0.1"
|
|
25
|
+
}
|
|
26
|
+
}
|