cve.now 0.0.1 → 0.1.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.
Files changed (4) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +77 -2
  3. package/cli.js +1574 -0
  4. package/package.json +31 -4
package/cli.js ADDED
@@ -0,0 +1,1574 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { parseArgs } from "node:util";
5
+ // package.json
6
+ var package_default = {
7
+ name: "@sazabi/cve-now-cli",
8
+ version: "0.1.0",
9
+ private: true,
10
+ type: "module",
11
+ description: "The cve.now CLI (npm `cve.now`): look up vulnerabilities, read feeds and the cve.now Index, search, and check lockfiles against cve.now's public REST API",
12
+ main: "src/cli.ts",
13
+ scripts: {
14
+ dev: "bun src/cli.ts",
15
+ build: "bun build.ts",
16
+ clean: "rm -rf dist",
17
+ typecheck: "tsrs --noEmit",
18
+ "typecheck:local": "tsrs --noEmit",
19
+ lint: "oxlint . && oxfmt --check '**/*.{ts,tsx,js,jsx,json}'",
20
+ "lint:fix": "oxlint --fix . && oxfmt '**/*.{ts,tsx,js,jsx,json}'"
21
+ },
22
+ devDependencies: {
23
+ "@sazabi/advisory-catalog": "workspace:*",
24
+ "@types/bun": "^1.3.11",
25
+ "@types/node": "^25.5.2",
26
+ typescript: "7.1.0-beta"
27
+ }
28
+ };
29
+
30
+ // src/api.ts
31
+ var VERSION = package_default.version;
32
+ var DEFAULT_API_URL = "https://cve.now";
33
+ var MAX_RETRIES = 2;
34
+ var MAX_RETRY_WAIT_S = 60;
35
+
36
+ class ApiError extends Error {
37
+ status;
38
+ body;
39
+ constructor(status, message, body) {
40
+ super(message);
41
+ this.status = status;
42
+ this.body = body;
43
+ }
44
+ }
45
+
46
+ class NetworkError extends Error {
47
+ }
48
+ function apiBase(env) {
49
+ return (env.CVE_NOW_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
50
+ }
51
+ var errorText = (body, status) => body && typeof body === "object" && ("error" in body) ? String(body.error) : `cve.now answered ${status}`;
52
+ async function request({
53
+ path,
54
+ method = "GET",
55
+ body,
56
+ options
57
+ }) {
58
+ const url = `${options.baseUrl ?? DEFAULT_API_URL}${path}`;
59
+ const headers = {
60
+ Accept: "application/json",
61
+ "User-Agent": `cve.now-cli/${VERSION}`
62
+ };
63
+ if (body !== undefined)
64
+ headers["Content-Type"] = "application/json";
65
+ if (options.apiKey)
66
+ headers.Authorization = `Bearer ${options.apiKey}`;
67
+ if (process.env.CI)
68
+ headers["X-CVE-Now-CI"] = "1";
69
+ for (let attempt = 0;; attempt++) {
70
+ let res;
71
+ try {
72
+ res = await fetch(url, {
73
+ method,
74
+ headers,
75
+ body: body === undefined ? undefined : JSON.stringify(body),
76
+ redirect: "follow"
77
+ });
78
+ } catch (e) {
79
+ throw new NetworkError(`Could not reach ${new URL(url).origin}: ${e.message}`);
80
+ }
81
+ if (res.status === 429 && attempt < MAX_RETRIES) {
82
+ const wait = Math.min(MAX_RETRY_WAIT_S, Math.max(1, Number(res.headers.get("retry-after")) || 5));
83
+ options.onRetry?.(wait);
84
+ await new Promise((resolve) => setTimeout(resolve, wait * 1000));
85
+ continue;
86
+ }
87
+ let text;
88
+ try {
89
+ text = await res.text();
90
+ } catch (e) {
91
+ throw new NetworkError(`cve.now's answer was cut off: ${e.message}`);
92
+ }
93
+ let json;
94
+ try {
95
+ json = text ? JSON.parse(text) : null;
96
+ } catch {
97
+ if (res.ok)
98
+ throw new NetworkError(`cve.now answered ${res.status} without JSON`);
99
+ json = null;
100
+ }
101
+ if (!res.ok) {
102
+ const message = res.status === 429 ? `${errorText(json, res.status)}. Set CVE_NOW_API_KEY to a cve.now API key for a per-key limit instead of the shared per-IP one.` : errorText(json, res.status);
103
+ throw new ApiError(res.status, message, json);
104
+ }
105
+ return json;
106
+ }
107
+ }
108
+ function query(params) {
109
+ const search = new URLSearchParams;
110
+ for (const [key, value] of Object.entries(params)) {
111
+ if (value === undefined)
112
+ continue;
113
+ for (const v of Array.isArray(value) ? value : [value])
114
+ search.append(key, String(v));
115
+ }
116
+ const s = search.toString();
117
+ return s ? `?${s}` : "";
118
+ }
119
+
120
+ // src/commands.ts
121
+ var CLI_COMMANDS = [
122
+ {
123
+ name: "get",
124
+ tool: "get_vulnerability",
125
+ usage: "get <id...>",
126
+ summary: "Show entries by CVE, GHSA or OSV id (ids from stdin when none are given). `cve.now <id>` works too."
127
+ },
128
+ {
129
+ name: "feed",
130
+ tool: "list_feed",
131
+ usage: "feed [name] [--filter critical,kev] [--tech <slug>] [--ecosystem <eco>] [--limit <n>]",
132
+ summary: "The newest items of a stored feed (default critical), or of a filter/tech/ecosystem selection."
133
+ },
134
+ {
135
+ name: "check",
136
+ tool: "check_stack",
137
+ usage: "check [path...] [--packages eco:name@version,...] [--stack <id>] [--fail-on kev|critical|high|any] [--ignore <id>]",
138
+ summary: "Find lockfiles under each path (default .), parse them locally and check their packages. Only (ecosystem, name, version) is sent."
139
+ },
140
+ {
141
+ name: "top",
142
+ tool: "get_leaderboard",
143
+ usage: "top [--window week|month|year|all] [--week YYYY-Www] [--tech <slug>] [--ecosystem <eco>] [--filter critical,kev] [--limit <n>]",
144
+ summary: "The cve.now Index: the worst vulnerabilities this week or window."
145
+ },
146
+ {
147
+ name: "search",
148
+ tool: "search",
149
+ usage: 'search "<query>" [--severity <list>] [--tech <slug>] [--ecosystem <eco>] [--vendor <v>] [--package <p>] [--kev true|false] [--exploited true|false] [--published <bound>] [--type cve|early_warning|all] [--limit <n>] [--cursor <c>]',
150
+ summary: "Search every entry and early warning; each result says why it matched."
151
+ }
152
+ ];
153
+
154
+ // src/commands/check.ts
155
+ import { existsSync as existsSync2 } from "node:fs";
156
+ import { resolve as resolve2 } from "node:path";
157
+ // ../advisory-catalog/src/flake-lock.ts
158
+ var GIT_ECOSYSTEM = "GIT";
159
+ var MAX_COMMITS = 1000;
160
+ var NIXPKGS_STALE_MS = 183 * 24 * 60 * 60 * 1000;
161
+ var COMMIT = /^[0-9a-f]{40}$/;
162
+ var text = (value) => typeof value === "string" && value ? value : undefined;
163
+ function cleanUrl(raw) {
164
+ const href = raw.replace(/^git\+/, "");
165
+ if (!URL.canParse(href))
166
+ return null;
167
+ const url = new URL(href);
168
+ const path = url.pathname.replace(/\/+$/, "").replace(/\.git$/, "");
169
+ return `${url.protocol}//${url.host}${path}`.slice(0, 200);
170
+ }
171
+ function sourceUrl(locked) {
172
+ const forges = {
173
+ github: "github.com",
174
+ gitlab: "gitlab.com",
175
+ sourcehut: "git.sr.ht"
176
+ };
177
+ const type = text(locked.type) ?? "";
178
+ const owner = text(locked.owner);
179
+ const repo = text(locked.repo);
180
+ const forge = forges[type];
181
+ if (forge)
182
+ return owner && repo ? cleanUrl(`https://${text(locked.host) ?? forge}/${owner}/${repo}`) : null;
183
+ const url = text(locked.url);
184
+ return (type === "git" || type === "tarball") && url ? cleanUrl(url) : null;
185
+ }
186
+ function nixpkgsLastModified(lock) {
187
+ const input = lock.nodes[lock.root ?? "root"]?.inputs?.nixpkgs;
188
+ const modified = typeof input === "string" ? lock.nodes[input]?.locked?.lastModified : undefined;
189
+ return typeof modified === "number" && modified > 0 ? new Date(modified * 1000).toISOString() : undefined;
190
+ }
191
+ function scanFlakeLock(content) {
192
+ const lock = JSON.parse(content);
193
+ const packages = [];
194
+ let unchecked = 0;
195
+ for (const [key, node] of Object.entries(lock.nodes)) {
196
+ const locked = node?.locked;
197
+ if (key === lock.root || !locked || typeof locked !== "object")
198
+ continue;
199
+ const rev = text(locked.rev);
200
+ const name = rev && COMMIT.test(rev) ? sourceUrl(locked) : null;
201
+ if (name && rev && packages.length < MAX_COMMITS)
202
+ packages.push({ ecosystem: GIT_ECOSYSTEM, name, version: rev });
203
+ else
204
+ unchecked++;
205
+ }
206
+ return {
207
+ packages,
208
+ coverage: { unchecked, nixpkgsLastModified: nixpkgsLastModified(lock) }
209
+ };
210
+ }
211
+
212
+ // ../advisory-catalog/src/mix-lock.ts
213
+ function fields(source) {
214
+ const result = [];
215
+ const closing = [];
216
+ let quote = "";
217
+ let field = "";
218
+ for (let i = 0;i < source.length; i++) {
219
+ const char = source[i];
220
+ if (quote) {
221
+ field += char;
222
+ if (char === "\\") {
223
+ if (++i >= source.length)
224
+ throw new Error("Invalid mix.lock string");
225
+ field += source[i];
226
+ } else if (char === quote)
227
+ quote = "";
228
+ } else if (char === "#") {
229
+ while (i + 1 < source.length && source[i + 1] !== `
230
+ `)
231
+ i++;
232
+ field += " ";
233
+ } else if (char === '"' || char === "'") {
234
+ quote = char;
235
+ field += char;
236
+ } else if (char === "{" || char === "[" || char === "(") {
237
+ closing.push(char === "{" ? "}" : char === "[" ? "]" : ")");
238
+ field += char;
239
+ } else if (char === "}" || char === "]" || char === ")") {
240
+ if (closing.pop() !== char)
241
+ throw new Error("Invalid mix.lock delimiters");
242
+ field += char;
243
+ } else if (char === "," && closing.length === 0) {
244
+ if (!field.trim())
245
+ throw new Error("Invalid mix.lock field");
246
+ result.push(field.trim());
247
+ field = "";
248
+ } else
249
+ field += char;
250
+ }
251
+ if (quote || closing.length)
252
+ throw new Error("Incomplete mix.lock");
253
+ if (field.trim())
254
+ result.push(field.trim());
255
+ return result;
256
+ }
257
+ function parseMixLock(content) {
258
+ const roots = fields(content);
259
+ const map = roots.length === 1 ? roots[0]?.match(/^%\{([\s\S]*)\}$/) : null;
260
+ if (!map)
261
+ throw new Error("Expected a mix.lock map");
262
+ const packages = [];
263
+ for (const entry of fields(map[1])) {
264
+ const value = entry.match(/^(?:"(?:\\.|[^"\\])*"|:"(?:\\.|[^"\\])*"|:?[A-Za-z_][\w@]*)\s*(?:=>|:)\s*(\{[\s\S]*\}|nil)$/)?.[1];
265
+ if (!value)
266
+ throw new Error("Invalid mix.lock entry");
267
+ if (value === "nil")
268
+ continue;
269
+ const tuple = fields(value.slice(1, -1));
270
+ if (tuple[0] !== ":hex")
271
+ continue;
272
+ if (tuple[6] && tuple[6] !== "nil" && tuple[6] !== '"hexpm"')
273
+ continue;
274
+ const name = tuple[1]?.match(/^(?::"([A-Za-z0-9_.-]+)"|:([A-Za-z0-9_]+)|"([A-Za-z0-9_.-]+)")$/);
275
+ const version = tuple[2]?.match(/^"(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)"$/)?.[1];
276
+ if (!name || !version)
277
+ throw new Error("Invalid Hex package or version");
278
+ packages.push({
279
+ ecosystem: "Hex",
280
+ name: name[1] ?? name[2] ?? name[3],
281
+ version
282
+ });
283
+ }
284
+ return packages;
285
+ }
286
+
287
+ // ../advisory-catalog/src/stack.ts
288
+ var DIRECT_UPLOAD_THRESHOLD = 4 * 1024 * 1024;
289
+ var ECOSYSTEMS = {
290
+ npm: "npm",
291
+ pypi: "PyPI",
292
+ pip: "PyPI",
293
+ go: "Go",
294
+ golang: "Go",
295
+ "crates.io": "crates.io",
296
+ crates: "crates.io",
297
+ cargo: "crates.io",
298
+ rubygems: "RubyGems",
299
+ gem: "RubyGems",
300
+ hex: "Hex",
301
+ elixir: "Hex",
302
+ packagist: "Packagist",
303
+ composer: "Packagist",
304
+ nuget: "NuGet",
305
+ maven: "Maven"
306
+ };
307
+ function normalizeEcosystem(name) {
308
+ return ECOSYSTEMS[name.trim().toLowerCase()] ?? null;
309
+ }
310
+ function dedupe(list) {
311
+ const seen = new Map;
312
+ for (const p of list) {
313
+ const key = `${p.ecosystem}\x00${p.name}\x00${p.version}`;
314
+ const kept = seen.get(key);
315
+ if (kept)
316
+ kept.dev = kept.dev && p.dev;
317
+ else
318
+ seen.set(key, { ...p });
319
+ }
320
+ return [...seen.values()];
321
+ }
322
+ var isVersion = (v) => /^v?\d/.test(v);
323
+ function parsePackageLock(content) {
324
+ const json = JSON.parse(content);
325
+ const out = [];
326
+ if (json.packages) {
327
+ for (const [key, p] of Object.entries(json.packages)) {
328
+ if (!key || p.link || !p.version)
329
+ continue;
330
+ const idx = key.lastIndexOf("node_modules/");
331
+ const name = p.name ?? (idx === -1 ? null : key.slice(idx + "node_modules/".length));
332
+ if (name)
333
+ out.push({
334
+ ecosystem: "npm",
335
+ name,
336
+ version: p.version,
337
+ dev: p.dev === true
338
+ });
339
+ }
340
+ } else if (json.dependencies) {
341
+ const walk = (deps) => {
342
+ for (const [name, d] of Object.entries(deps)) {
343
+ if (d.version && isVersion(d.version))
344
+ out.push({
345
+ ecosystem: "npm",
346
+ name,
347
+ version: d.version,
348
+ dev: d.dev === true
349
+ });
350
+ if (d.dependencies)
351
+ walk(d.dependencies);
352
+ }
353
+ };
354
+ walk(json.dependencies);
355
+ }
356
+ return out;
357
+ }
358
+ function parseBunLock(content) {
359
+ const cleaned = content.replace(/,(\s*[}\]])/g, "$1");
360
+ const json = JSON.parse(cleaned);
361
+ const out = [];
362
+ for (const entry of Object.values(json.packages ?? {})) {
363
+ const spec = entry[0];
364
+ if (typeof spec !== "string")
365
+ continue;
366
+ const at = spec.lastIndexOf("@");
367
+ if (at <= 0)
368
+ continue;
369
+ const version = spec.slice(at + 1);
370
+ if (!isVersion(version))
371
+ continue;
372
+ out.push({ ecosystem: "npm", name: spec.slice(0, at), version });
373
+ }
374
+ return out;
375
+ }
376
+ function parsePnpmLock(content) {
377
+ const out = [];
378
+ const patterns = [
379
+ /^ {2}'?\/?((?:@[^/@\s']+\/)?[^/@\s']+)@([^:(\s']+)/gm,
380
+ /^ {2}\/((?:@[^/\s]+\/)?[^/@\s]+)\/([^_:(\s/]+)/gm
381
+ ];
382
+ for (const re of patterns)
383
+ for (const m of content.matchAll(re)) {
384
+ const name = m[1];
385
+ const version = m[2];
386
+ if (name && version && isVersion(version))
387
+ out.push({ ecosystem: "npm", name, version });
388
+ }
389
+ return out;
390
+ }
391
+ function parseYarnLock(content) {
392
+ const out = [];
393
+ let name = null;
394
+ for (const line of content.split(`
395
+ `)) {
396
+ if (!line || line.startsWith("#"))
397
+ continue;
398
+ if (!line.startsWith(" ")) {
399
+ name = null;
400
+ if (!line.trimEnd().endsWith(":") || line.startsWith("__metadata"))
401
+ continue;
402
+ const first = line.trimEnd().slice(0, -1).split(",")[0]?.trim().replace(/^"|"$/g, "") ?? "";
403
+ const at = first.lastIndexOf("@", first.indexOf(":") === -1 ? undefined : first.indexOf(":"));
404
+ const cut = first.indexOf("@", 1);
405
+ const idx = cut === -1 ? at : cut;
406
+ if (idx > 0)
407
+ name = first.slice(0, idx);
408
+ continue;
409
+ }
410
+ const m = line.match(/^ {2}version:? "?([^"\s]+)"?/);
411
+ if (m && name && m[1] && isVersion(m[1])) {
412
+ out.push({ ecosystem: "npm", name, version: m[1] });
413
+ name = null;
414
+ }
415
+ }
416
+ return out;
417
+ }
418
+ function parseRequirements(content) {
419
+ const out = [];
420
+ for (const line of content.split(`
421
+ `)) {
422
+ const m = line.match(/^\s*([A-Za-z0-9][A-Za-z0-9_.-]*)(?:\[[^\]]*\])?\s*===?\s*([^\s;#\\]+)/);
423
+ if (m?.[1] && m[2])
424
+ out.push({ ecosystem: "PyPI", name: m[1], version: m[2] });
425
+ }
426
+ return out;
427
+ }
428
+ function parseTomlPackages({
429
+ content,
430
+ ecosystem,
431
+ requireSource,
432
+ skip
433
+ }) {
434
+ const out = [];
435
+ for (const block of content.split("[[package]]").slice(1)) {
436
+ const name = block.match(/^name\s*=\s*"([^"]+)"/m)?.[1];
437
+ const version = block.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
438
+ if (!name || !version)
439
+ continue;
440
+ if (requireSource && !/^source\s*=/m.test(block))
441
+ continue;
442
+ if (skip?.test(block))
443
+ continue;
444
+ const category = block.match(/^category\s*=\s*"([^"]+)"/m)?.[1];
445
+ out.push(category ? { ecosystem, name, version, dev: category === "dev" } : { ecosystem, name, version });
446
+ }
447
+ return out;
448
+ }
449
+ function parseUvLock(content) {
450
+ return parseTomlPackages({
451
+ content,
452
+ ecosystem: "PyPI",
453
+ requireSource: false,
454
+ skip: /^source\s*=\s*\{\s*(editable|virtual)\s*=/m
455
+ });
456
+ }
457
+ function parseGoSum(content) {
458
+ const out = [];
459
+ for (const line of content.split(`
460
+ `)) {
461
+ const [mod, ver] = line.trim().split(/\s+/);
462
+ if (!mod || !ver || ver.endsWith("/go.mod"))
463
+ continue;
464
+ out.push({ ecosystem: "Go", name: mod, version: ver });
465
+ }
466
+ return out;
467
+ }
468
+ var GEM_PLATFORM = /-(?:x86|x64|i[3-6]86|arm|aarch64|universal|java|jruby|mswin|mingw|darwin|linux|freebsd|openbsd|netbsd|solaris|wasm|ruby)[\w.-]*$/;
469
+ function parseGemfileLock(content) {
470
+ const out = [];
471
+ for (const m of content.matchAll(/^ {4}([A-Za-z0-9_.-]+) \(([^)\s,]+)\)/gm)) {
472
+ if (m[1] && m[2])
473
+ out.push({
474
+ ecosystem: "RubyGems",
475
+ name: m[1],
476
+ version: m[2].replace(GEM_PLATFORM, "")
477
+ });
478
+ }
479
+ return out;
480
+ }
481
+ function parseComposerLock(content) {
482
+ const json = JSON.parse(content);
483
+ const out = [];
484
+ for (const [list, dev] of [
485
+ [json.packages, false],
486
+ [json["packages-dev"], true]
487
+ ])
488
+ for (const p of list ?? []) {
489
+ const version = p.version?.replace(/^v(?=\d)/, "");
490
+ if (p.name && version && /^\d/.test(version) && !/-dev$/.test(version))
491
+ out.push({ ecosystem: "Packagist", name: p.name, version, dev });
492
+ }
493
+ return out;
494
+ }
495
+ function parsePipfileLock(content) {
496
+ const json = JSON.parse(content);
497
+ const out = [];
498
+ for (const [section, dev] of [
499
+ ["default", false],
500
+ ["develop", true]
501
+ ])
502
+ for (const [name, p] of Object.entries(json[section] ?? {})) {
503
+ const version = p.version?.match(/^===?\s*(\S+)$/)?.[1];
504
+ if (version)
505
+ out.push({ ecosystem: "PyPI", name, version, dev });
506
+ }
507
+ return out;
508
+ }
509
+ function parseNugetLock(content) {
510
+ const json = JSON.parse(content);
511
+ const out = [];
512
+ for (const framework of Object.values(json.dependencies ?? {}))
513
+ for (const [name, p] of Object.entries(framework)) {
514
+ if (p.type !== "Project" && p.resolved && isVersion(p.resolved))
515
+ out.push({ ecosystem: "NuGet", name, version: p.resolved });
516
+ }
517
+ return out;
518
+ }
519
+ function parseGradleLockfile(content) {
520
+ const out = [];
521
+ for (const m of content.matchAll(/^([\w.-]+):([\w.-]+):([^:=\s]+)=(\S*)$/gm)) {
522
+ const [, group, artifact, version, configurations = ""] = m;
523
+ if (!group || !artifact || !version || !isVersion(version))
524
+ continue;
525
+ const used = configurations.split(",").filter(Boolean);
526
+ out.push({
527
+ ecosystem: "Maven",
528
+ name: `${group}:${artifact}`,
529
+ version,
530
+ dev: used.length > 0 && used.every((c) => /^test/i.test(c))
531
+ });
532
+ }
533
+ return out;
534
+ }
535
+
536
+ class LockfileError extends Error {
537
+ }
538
+ var SUPPORTED_LOCKFILES = "package-lock.json, npm-shrinkwrap.json, bun.lock, pnpm-lock.yaml, yarn.lock, poetry.lock, uv.lock, requirements.txt, Cargo.lock, go.sum, Gemfile.lock, mix.lock, flake.lock, composer.lock, Pipfile.lock, packages.lock.json (NuGet), gradle.lockfile";
539
+ var PARSERS = {
540
+ "package-lock.json": parsePackageLock,
541
+ "bun.lock": parseBunLock,
542
+ "pnpm-lock.yaml": parsePnpmLock,
543
+ "yarn.lock": parseYarnLock,
544
+ "poetry.lock": (content) => parseTomlPackages({ content, ecosystem: "PyPI", requireSource: false }),
545
+ "uv.lock": parseUvLock,
546
+ "Cargo.lock": (content) => parseTomlPackages({
547
+ content,
548
+ ecosystem: "crates.io",
549
+ requireSource: true
550
+ }),
551
+ "go.sum": parseGoSum,
552
+ "Gemfile.lock": parseGemfileLock,
553
+ "mix.lock": parseMixLock,
554
+ "flake.lock": (content) => scanFlakeLock(content).packages,
555
+ "requirements.txt": parseRequirements,
556
+ "composer.lock": parseComposerLock,
557
+ "Pipfile.lock": parsePipfileLock,
558
+ "packages.lock.json": parseNugetLock,
559
+ "gradle.lockfile": parseGradleLockfile
560
+ };
561
+ var MANIFESTS = [
562
+ {
563
+ file: "pyproject.toml",
564
+ lockfiles: "poetry.lock, uv.lock or requirements.txt",
565
+ test: /^\[(project|tool\.poetry|build-system)\]/m
566
+ },
567
+ {
568
+ file: "Cargo.toml",
569
+ lockfiles: "Cargo.lock",
570
+ test: /^\[(package|workspace|dependencies)\]/m
571
+ },
572
+ {
573
+ file: "go.mod",
574
+ lockfiles: "go.sum",
575
+ test: /^module \S+/m
576
+ },
577
+ {
578
+ file: "Gemfile",
579
+ lockfiles: "Gemfile.lock",
580
+ test: /^(source ['"]|gem ['"])/m
581
+ },
582
+ {
583
+ file: "Pipfile",
584
+ lockfiles: "Pipfile.lock",
585
+ test: /^\[(packages|dev-packages|requires)\]/m
586
+ },
587
+ {
588
+ file: "mix.exs",
589
+ lockfiles: "mix.lock",
590
+ test: /use Mix\.Project/
591
+ }
592
+ ];
593
+ var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
594
+ function detectJson({
595
+ content,
596
+ base
597
+ }) {
598
+ let json = null;
599
+ try {
600
+ json = JSON.parse(content);
601
+ } catch {
602
+ if (/"lockfileVersion"/.test(content) && /"workspaces"/.test(content))
603
+ return "bun.lock";
604
+ if (base === "package-lock.json" || base === "npm-shrinkwrap.json")
605
+ return "package-lock.json";
606
+ return null;
607
+ }
608
+ if (!json || typeof json !== "object")
609
+ return null;
610
+ if (Array.isArray(json.packages) && (("content-hash" in json) || ("packages-dev" in json)))
611
+ return "composer.lock";
612
+ if (isObject(json._meta) && (("default" in json) || ("develop" in json)))
613
+ return "Pipfile.lock";
614
+ if (typeof json.version === "number" && isObject(json.dependencies) && Object.values(json.dependencies).some((framework) => isObject(framework) && Object.values(framework).some((p) => isObject(p) && (("resolved" in p) || ("contentHash" in p)))))
615
+ return "packages.lock.json";
616
+ if ("require" in json || "require-dev" in json)
617
+ return { manifest: "composer.json", lockfiles: "composer.lock" };
618
+ if ("lockfileVersion" in json) {
619
+ if ("workspaces" in json)
620
+ return "bun.lock";
621
+ if ("packages" in json || "dependencies" in json)
622
+ return "package-lock.json";
623
+ return null;
624
+ }
625
+ if (json.packages && Object.keys(json.packages).some((k) => k.startsWith("node_modules/")))
626
+ return "package-lock.json";
627
+ if ("dependencies" in json || "devDependencies" in json || "name" in json)
628
+ return "package.json";
629
+ if (typeof json.nodes === "object" && json.nodes !== null && typeof json.root === "string" && typeof json.version === "number")
630
+ return "flake.lock";
631
+ return null;
632
+ }
633
+ function detectLockfile({
634
+ filename,
635
+ content
636
+ }) {
637
+ const base = filename.split(/[\\/]/).pop()?.toLowerCase() ?? "";
638
+ const text = content.replace(/^\uFEFF/, "");
639
+ const trimmed = text.trimStart();
640
+ if (trimmed.startsWith("{"))
641
+ return detectJson({ content: text, base });
642
+ if (/^lockfileVersion:/m.test(text))
643
+ return "pnpm-lock.yaml";
644
+ if (/^# yarn lockfile v1/m.test(text) || /^__metadata:/m.test(text))
645
+ return "yarn.lock";
646
+ if (/^\[\[package\]\]/m.test(text)) {
647
+ if (/^source\s*=\s*\{/m.test(text) || base === "uv.lock")
648
+ return "uv.lock";
649
+ if (/^python-versions\s*=|^\[package\.dependencies\]|^content-hash\s*=|^files\s*=\s*\[/m.test(text))
650
+ return "poetry.lock";
651
+ if (/^checksum\s*=|^source\s*=\s*"registry\+/m.test(text))
652
+ return "Cargo.lock";
653
+ return base === "poetry.lock" ? "poetry.lock" : "Cargo.lock";
654
+ }
655
+ if (/^\S+ v\S+ h1:/m.test(text))
656
+ return "go.sum";
657
+ if (/^# This is a Gradle generated file/m.test(text) || /^[\w.-]+:[\w.-]+:[^:=\s]+=\S*$/m.test(text))
658
+ return "gradle.lockfile";
659
+ if (/^(GEM|GIT|PATH)$/m.test(text) && /^ {2}specs:/m.test(text))
660
+ return "Gemfile.lock";
661
+ if (/^(?:#[^\n]*\n\s*)*%\{/.test(trimmed) && /:hex/.test(text))
662
+ return "mix.lock";
663
+ const manifest = MANIFESTS.find((m) => m.test.test(text));
664
+ if (manifest)
665
+ return { manifest: manifest.file, lockfiles: manifest.lockfiles };
666
+ if (/^\s*[A-Za-z0-9][A-Za-z0-9_.-]*(\[[^\]]*\])?\s*===?\s*\S/m.test(text))
667
+ return "requirements.txt";
668
+ return null;
669
+ }
670
+ function parseLockfile({
671
+ filename,
672
+ content
673
+ }) {
674
+ const kind = detectLockfile({ filename, content });
675
+ if (kind === "package.json")
676
+ throw new LockfileError("This is package.json, not a lockfile. Run `npm install --package-lock-only` and upload package-lock.json (or bun.lock, pnpm-lock.yaml, yarn.lock).");
677
+ if (kind && typeof kind === "object")
678
+ throw new LockfileError(`This looks like ${kind.manifest}, a dependency manifest, not a lockfile. Upload the lockfile your package manager writes (${kind.lockfiles}).`);
679
+ if (!kind)
680
+ throw new LockfileError(`Could not recognize that file as a lockfile. Supported: ${SUPPORTED_LOCKFILES}.`);
681
+ try {
682
+ return dedupe(PARSERS[kind](content.replace(/^\uFEFF/, "")));
683
+ } catch {
684
+ throw new LockfileError(`Could not parse that ${kind}. Check its format and try again.`);
685
+ }
686
+ }
687
+ function parseStackParam(param) {
688
+ const out = [];
689
+ for (const raw of param.split(",")) {
690
+ const item = raw.trim();
691
+ if (!item)
692
+ continue;
693
+ const colon = item.indexOf(":");
694
+ if (colon <= 0)
695
+ throw new Error(`Invalid stack item: ${item}`);
696
+ const ecosystem = normalizeEcosystem(item.slice(0, colon));
697
+ if (!ecosystem)
698
+ throw new Error(`Unknown ecosystem in: ${item}`);
699
+ const rest = item.slice(colon + 1);
700
+ const at = rest.lastIndexOf("@");
701
+ if (at <= 0 || at === rest.length - 1)
702
+ throw new Error(`Invalid stack item: ${item}`);
703
+ out.push({
704
+ ecosystem,
705
+ name: rest.slice(0, at),
706
+ version: rest.slice(at + 1)
707
+ });
708
+ }
709
+ return dedupe(out);
710
+ }
711
+ // ../advisory-catalog/src/sources/osv.ts
712
+ var LISTING_CHUNK_BYTES = 64 * 1024;
713
+ var LISTING_MAX_BYTES = 8 * 1024 * 1024;
714
+ // ../advisory-catalog/src/sources/public-exploits.ts
715
+ var URLS = {
716
+ metasploit: "https://raw.githubusercontent.com/rapid7/metasploit-framework/master/db/modules_metadata_base.json",
717
+ nuclei: "https://raw.githubusercontent.com/projectdiscovery/nuclei-templates/main/cves.json",
718
+ "poc-in-github": "https://raw.githubusercontent.com/nomi-sec/PoC-in-GitHub/master/README.md"
719
+ };
720
+ var PUBLIC_EXPLOIT_SOURCES = Object.keys(URLS);
721
+ // ../advisory-catalog/src/sources/nvd.ts
722
+ var MAX_WINDOW_MS = 120 * 86400000;
723
+ // ../advisory-catalog/src/signals/text.ts
724
+ var ACCESS = /\b(breach(?:ed|es)?|hacked|compromised|compromise|stole|stolen|steal(?:ing)?|theft|exfiltrat\w*|leaked|exposed|accessed|gained access|unauthori[sz]ed access|intrusion|ransomware|infiltrat\w*|encrypted|obtained|downloaded|copied|took)\b/gi;
725
+ var ACTION_LIST_TAIL = new RegExp(`(?:[\\s,]+|\\b(?:or|and)\\b|${ACCESS.source})+$`, "i");
726
+ var STATES_ACCESS = new Set(["press", "vendor", "official"]);
727
+ var STOP_WORDS = new Set("a an and any are as at be before by for from had has have if in into is it its of on or that the their them then there these this those to up was were what when whether which while who will with you your yours".split(" "));
728
+
729
+ // ../advisory-catalog/src/signals/types.ts
730
+ var PUBLIC_SIGNAL_STATUSES = new Set([
731
+ "corroborated",
732
+ "confirmed"
733
+ ]);
734
+ var LEAK_RELAY_ACCOUNTS = new Set([
735
+ "mastodon:@RedPacketSecurity@mastodon.social",
736
+ "mastodon:@Ransomlook@social.circl.lu",
737
+ "mastodon:@darkwebsonar@infosec.exchange",
738
+ "mastodon:@DevaOnBreaches@infosec.exchange"
739
+ ]);
740
+ // ../advisory-catalog/src/breaches/types.ts
741
+ var SENSITIVE_DATA_TYPES = new Set([
742
+ "password_plain",
743
+ "password_hash",
744
+ "ssn",
745
+ "gov_id",
746
+ "health",
747
+ "payment_card",
748
+ "bank"
749
+ ]);
750
+ var PUBLIC_BREACH_STATUSES = new Set([
751
+ "corroborated",
752
+ "confirmed",
753
+ "disputed"
754
+ ]);
755
+ var DUPLICATE_WINDOW_MS = 7 * 86400000;
756
+ // src/context.ts
757
+ var EXIT = { ok: 0, findings: 1, usage: 2, api: 3 };
758
+
759
+ class UsageError extends Error {
760
+ }
761
+ var list = (v) => Array.isArray(v) && v.length ? v : undefined;
762
+ var str = (v) => typeof v === "string" ? v : undefined;
763
+ function intOption(v, flag) {
764
+ if (v === undefined)
765
+ return;
766
+ const n = Number(v);
767
+ if (typeof v !== "string" || !Number.isInteger(n) || n < 1)
768
+ throw new UsageError(`${flag} takes a whole number of 1 or more`);
769
+ return n;
770
+ }
771
+ var printJson = (ctx, value) => ctx.out(JSON.stringify(value, null, 2));
772
+
773
+ // src/ignore.ts
774
+ import { existsSync, readFileSync } from "node:fs";
775
+ import { dirname, join, relative, resolve } from "node:path";
776
+ var IGNORE_FILE = ".cve-now-ignore";
777
+ function parseIgnoreFile(content) {
778
+ return content.split(`
779
+ `).map((line) => line.replace(/#.*/, "").trim()).filter(Boolean).flatMap((line) => line.split(/[\s,]+/)).map((id) => id.toUpperCase());
780
+ }
781
+ var cache = new Map;
782
+ function dirIgnores(dir) {
783
+ let ids = cache.get(dir);
784
+ if (!ids) {
785
+ const file = join(dir, IGNORE_FILE);
786
+ ids = existsSync(file) ? parseIgnoreFile(readFileSync(file, "utf8")) : [];
787
+ cache.set(dir, ids);
788
+ }
789
+ return ids;
790
+ }
791
+ function sharedIgnores({
792
+ cwd,
793
+ flags
794
+ }) {
795
+ const ids = new Set(dirIgnores(resolve(cwd)));
796
+ for (const id of flags.flatMap((f) => f.split(",")))
797
+ if (id.trim())
798
+ ids.add(id.trim().toUpperCase());
799
+ return ids;
800
+ }
801
+ function ignoresFor({
802
+ shared,
803
+ file,
804
+ root
805
+ }) {
806
+ const ids = new Set(shared);
807
+ let dir = dirname(resolve(file));
808
+ const stop = resolve(root);
809
+ for (;; ) {
810
+ for (const id of dirIgnores(dir))
811
+ ids.add(id);
812
+ const rel = relative(stop, dir);
813
+ if (!rel || rel.startsWith("..") || dir === dirname(dir))
814
+ break;
815
+ dir = dirname(dir);
816
+ }
817
+ return ids;
818
+ }
819
+
820
+ // src/lockfiles.ts
821
+ import { readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
822
+ import { basename, join as join2, relative as relative2 } from "node:path";
823
+ var LOCKFILE_NAMES = new Set([
824
+ "package-lock.json",
825
+ "npm-shrinkwrap.json",
826
+ "bun.lock",
827
+ "pnpm-lock.yaml",
828
+ "yarn.lock",
829
+ "poetry.lock",
830
+ "uv.lock",
831
+ "Pipfile.lock",
832
+ "requirements.txt",
833
+ "Cargo.lock",
834
+ "go.sum",
835
+ "Gemfile.lock",
836
+ "mix.lock",
837
+ "flake.lock",
838
+ "composer.lock",
839
+ "packages.lock.json",
840
+ "gradle.lockfile"
841
+ ]);
842
+ var SKIP_DIRS = new Set([
843
+ "node_modules",
844
+ ".git",
845
+ "vendor",
846
+ "target",
847
+ ".venv",
848
+ "venv",
849
+ "__pycache__",
850
+ "dist",
851
+ ".next",
852
+ ".turbo"
853
+ ]);
854
+ var MAX_DEPTH = 12;
855
+ function findLockfiles(path) {
856
+ const stat = statSync(path);
857
+ if (stat.isFile())
858
+ return [path];
859
+ const found = [];
860
+ const walk = (dir, depth) => {
861
+ let entries;
862
+ try {
863
+ entries = readdirSync(dir, { withFileTypes: true });
864
+ } catch {
865
+ return;
866
+ }
867
+ for (const entry of entries) {
868
+ if (entry.isDirectory()) {
869
+ if (!SKIP_DIRS.has(entry.name) && depth < MAX_DEPTH)
870
+ walk(join2(dir, entry.name), depth + 1);
871
+ } else if (entry.isFile() && LOCKFILE_NAMES.has(entry.name))
872
+ found.push(join2(dir, entry.name));
873
+ }
874
+ };
875
+ walk(path, 0);
876
+ return found.sort();
877
+ }
878
+ function parseLockfileAt({
879
+ file,
880
+ cwd
881
+ }) {
882
+ const shown = relative2(cwd, file) || basename(file);
883
+ let content;
884
+ try {
885
+ content = readFileSync2(file, "utf8");
886
+ } catch (e) {
887
+ return { file: shown, error: e.message };
888
+ }
889
+ const filename = basename(file);
890
+ let packages;
891
+ try {
892
+ packages = parseLockfile({ filename, content });
893
+ } catch (e) {
894
+ return { file: shown, error: e.message };
895
+ }
896
+ if (detectLockfile({ filename, content }) === "flake.lock")
897
+ return {
898
+ file: shown,
899
+ body: { lockfile_name: filename, lockfile_content: content },
900
+ packageCount: packages.length
901
+ };
902
+ return {
903
+ file: shown,
904
+ body: {
905
+ packages: packages.map(({ ecosystem, name, version }) => ({
906
+ ecosystem,
907
+ name,
908
+ version
909
+ }))
910
+ },
911
+ packageCount: packages.length
912
+ };
913
+ }
914
+
915
+ // src/style.ts
916
+ var wrap = ({ on, open, close }) => (s) => on ? `\x1B[${open}m${s}\x1B[${close}m` : s;
917
+ function makeStyle(on) {
918
+ return {
919
+ bold: wrap({ on, open: 1, close: 22 }),
920
+ dim: wrap({ on, open: 2, close: 22 }),
921
+ red: wrap({ on, open: 31, close: 39 }),
922
+ yellow: wrap({ on, open: 33, close: 39 }),
923
+ green: wrap({ on, open: 32, close: 39 }),
924
+ cyan: wrap({ on, open: 36, close: 39 })
925
+ };
926
+ }
927
+ function ratingText(style, rating) {
928
+ const word = rating ?? "unrated";
929
+ if (word === "critical")
930
+ return style.red(style.bold(word));
931
+ if (word === "high")
932
+ return style.red(word);
933
+ if (word === "medium")
934
+ return style.yellow(word);
935
+ return style.dim(word);
936
+ }
937
+ var ratingFor = (cvss) => cvss === null || cvss === undefined ? null : cvss >= 9 ? "critical" : cvss >= 7 ? "high" : cvss >= 4 ? "medium" : cvss > 0 ? "low" : "none";
938
+
939
+ // src/commands/check.ts
940
+ var MAX_PACKAGES = 1e4;
941
+ var FAIL_ON = ["kev", "critical", "high", "any"];
942
+ var cvssOf = (f) => f.severity?.cvss ?? -1;
943
+ var ratingOf = (f) => f.rating ?? ratingFor(f.severity?.cvss);
944
+ function meets(f, failOn) {
945
+ if (failOn === "any" || f.kev)
946
+ return true;
947
+ if (failOn === "kev")
948
+ return false;
949
+ const rating = ratingOf(f);
950
+ const cvss = cvssOf(f);
951
+ if (failOn === "critical")
952
+ return cvss >= 9 || rating === "critical";
953
+ return cvss >= 7 || rating === "critical" || rating === "high";
954
+ }
955
+ function rank(a, b) {
956
+ const exploited = (f) => f.kev || f.activelyExploited ? 1 : 0;
957
+ return exploited(b) - exploited(a) || Number(!!b.publicExploit) - Number(!!a.publicExploit) || cvssOf(b) - cvssOf(a) || Number(!!b.epssReached) - Number(!!a.epssReached) || a.id.localeCompare(b.id);
958
+ }
959
+ async function checkBody(ctx, body) {
960
+ if (!("packages" in body) || body.packages.length <= MAX_PACKAGES)
961
+ return request({
962
+ path: "/api/v1/check",
963
+ method: "POST",
964
+ body,
965
+ options: ctx.api
966
+ });
967
+ const merged = {
968
+ coverage: "",
969
+ checked: 0,
970
+ notes: [],
971
+ affected: []
972
+ };
973
+ const seen = new Set;
974
+ for (let i = 0;i < body.packages.length; i += MAX_PACKAGES) {
975
+ const part = await request({
976
+ path: "/api/v1/check",
977
+ method: "POST",
978
+ body: { packages: body.packages.slice(i, i + MAX_PACKAGES) },
979
+ options: ctx.api
980
+ });
981
+ merged.coverage = part.coverage;
982
+ merged.checked += part.checked;
983
+ for (const note of part.notes ?? [])
984
+ if (!merged.notes.includes(note))
985
+ merged.notes.push(note);
986
+ for (const f of part.affected)
987
+ if (!seen.has(f.id)) {
988
+ seen.add(f.id);
989
+ merged.affected.push(f);
990
+ }
991
+ }
992
+ if (!merged.notes.length)
993
+ delete merged.notes;
994
+ return merged;
995
+ }
996
+ function gatherSources(ctx, positionals, values) {
997
+ const sources = [];
998
+ const packages = list(values.packages);
999
+ if (packages) {
1000
+ let parsed;
1001
+ try {
1002
+ parsed = parseStackParam(packages.join(","));
1003
+ } catch (e) {
1004
+ throw new UsageError(`--packages: ${e.message}. Use ecosystem:name@version, e.g. npm:lodash@4.17.20`);
1005
+ }
1006
+ sources.push({
1007
+ source: "--packages",
1008
+ body: {
1009
+ packages: parsed.map(({ ecosystem, name, version }) => ({
1010
+ ecosystem,
1011
+ name,
1012
+ version
1013
+ }))
1014
+ }
1015
+ });
1016
+ }
1017
+ const stack = str(values.stack);
1018
+ if (stack)
1019
+ sources.push({ source: `--stack ${stack}`, body: { stack_id: stack } });
1020
+ const paths = positionals.length || sources.length ? positionals : ["."];
1021
+ for (const path of paths) {
1022
+ const full = resolve2(ctx.cwd, path);
1023
+ if (!existsSync2(full))
1024
+ throw new UsageError(`No such file or directory: ${path}`);
1025
+ const files = findLockfiles(full);
1026
+ if (!files.length) {
1027
+ ctx.err(`No supported lockfile found under ${path}`);
1028
+ continue;
1029
+ }
1030
+ for (const file of files) {
1031
+ const parsed = parseLockfileAt({ file, cwd: ctx.cwd });
1032
+ const lockfile = { file, root: full };
1033
+ sources.push("error" in parsed ? { source: parsed.file, error: parsed.error, lockfile } : { source: parsed.file, body: parsed.body, lockfile });
1034
+ }
1035
+ }
1036
+ return sources;
1037
+ }
1038
+ async function checkCommand(ctx, positionals, values) {
1039
+ const failOnValue = str(values["fail-on"]);
1040
+ if (failOnValue && !FAIL_ON.includes(failOnValue))
1041
+ throw new UsageError(`--fail-on takes ${FAIL_ON.join(", ")}`);
1042
+ const failOn = failOnValue;
1043
+ const sources = gatherSources(ctx, positionals, values);
1044
+ if (!sources.length)
1045
+ throw new UsageError("Nothing to check: pass a path with a lockfile, --packages or --stack");
1046
+ const shared = sharedIgnores({
1047
+ cwd: ctx.cwd,
1048
+ flags: list(values.ignore) ?? []
1049
+ });
1050
+ const { style, out, err } = ctx;
1051
+ let parseErrors = 0;
1052
+ let apiErrors = 0;
1053
+ let failing = 0;
1054
+ const ignored = new Set;
1055
+ const results = [];
1056
+ for (const [index, src] of sources.entries()) {
1057
+ if (src.error || !src.body) {
1058
+ parseErrors++;
1059
+ err(`${src.source}: ${src.error}`);
1060
+ results.push({ source: src.source, error: src.error });
1061
+ continue;
1062
+ }
1063
+ let response;
1064
+ try {
1065
+ response = await checkBody(ctx, src.body);
1066
+ } catch (e) {
1067
+ if (!(e instanceof ApiError || e instanceof NetworkError))
1068
+ throw e;
1069
+ if (e instanceof ApiError && [400, 404, 413, 422].includes(e.status))
1070
+ parseErrors++;
1071
+ else
1072
+ apiErrors++;
1073
+ err(`${src.source}: ${e.message}`);
1074
+ results.push({ source: src.source, error: e.message });
1075
+ if (e instanceof NetworkError)
1076
+ break;
1077
+ continue;
1078
+ }
1079
+ const ignores = src.lockfile ? ignoresFor({ shared, ...src.lockfile }) : shared;
1080
+ const findings = response.affected.filter((f) => {
1081
+ if (!ignores.has(f.id.toUpperCase()))
1082
+ return true;
1083
+ ignored.add(f.id);
1084
+ return false;
1085
+ }).sort(rank);
1086
+ if (failOn)
1087
+ failing += findings.filter((f) => meets(f, failOn)).length;
1088
+ results.push({ source: src.source, response });
1089
+ if (ctx.json)
1090
+ continue;
1091
+ if (index)
1092
+ out();
1093
+ const count = `${response.checked.toLocaleString("en-US")} ${response.checked === 1 ? "package" : "packages"}`;
1094
+ out(`${style.bold(src.source)} ${style.dim(`(${count})`)}: ${findings.length ? style.red(`${findings.length} found`) : style.green("none found")}`);
1095
+ for (const f of findings) {
1096
+ const cvss = f.severity?.cvss;
1097
+ const flags = [
1098
+ f.kev ? "KEV" : f.activelyExploited ? "actively exploited" : "",
1099
+ f.publicExploit ? "public exploit" : "",
1100
+ f.epssReached ? "EPSS reached 0.1" : ""
1101
+ ].filter(Boolean);
1102
+ out(` ${[
1103
+ ratingText(style, ratingOf(f)) + (cvss !== undefined ? ` ${cvss.toFixed(1)}` : ""),
1104
+ style.bold(f.id),
1105
+ `${f.package.name} ${f.package.version}${f.fixed?.length ? ` → ${f.fixed.join(", ")}` : ""}`,
1106
+ flags.length ? style.red(flags.join(" · ")) : ""
1107
+ ].filter(Boolean).join(" ")}`);
1108
+ out(` ${f.title}`);
1109
+ if (f.exposure)
1110
+ out(style.dim(` Exposure: ${f.exposure}`));
1111
+ out(style.cyan(` ${f.url}`));
1112
+ }
1113
+ for (const note of response.notes ?? [])
1114
+ out(style.yellow(` ${note}`));
1115
+ }
1116
+ if (ctx.json)
1117
+ printJson(ctx, {
1118
+ schemaVersion: 1,
1119
+ results,
1120
+ ignored: [...ignored].sort(),
1121
+ ...failOn ? { failOn, failing } : {}
1122
+ });
1123
+ else {
1124
+ out();
1125
+ if (ignored.size)
1126
+ out(style.dim(`Ignored: ${[...ignored].sort().join(", ")}`));
1127
+ out(style.dim("No match means cve.now's index has no known vulnerability for that version, not a guarantee that none exists."));
1128
+ }
1129
+ if (apiErrors)
1130
+ return EXIT.api;
1131
+ if (parseErrors)
1132
+ return EXIT.usage;
1133
+ if (failOn && failing) {
1134
+ if (!ctx.json)
1135
+ err(`${failing} ${failing === 1 ? "finding meets" : "findings meet"} --fail-on ${failOn}`);
1136
+ return EXIT.findings;
1137
+ }
1138
+ return EXIT.ok;
1139
+ }
1140
+
1141
+ // src/commands/feed.ts
1142
+ var DEFAULT_LIMIT = 20;
1143
+ async function feedCommand(ctx, positionals, values) {
1144
+ if (positionals.length > 1)
1145
+ throw new UsageError("feed takes at most one feed name");
1146
+ const feed = await request({
1147
+ path: `/api/v1/entries${query({
1148
+ name: positionals[0],
1149
+ filter: list(values.filter),
1150
+ tech: list(values.tech),
1151
+ ecosystem: list(values.ecosystem),
1152
+ limit: intOption(values.limit, "--limit") ?? DEFAULT_LIMIT
1153
+ })}`,
1154
+ options: ctx.api
1155
+ });
1156
+ if (ctx.json) {
1157
+ printJson(ctx, feed);
1158
+ return EXIT.ok;
1159
+ }
1160
+ const { style, out } = ctx;
1161
+ out(`${style.bold(feed.title)} ${style.dim(`(updated ${feed.updatedAt})`)}`);
1162
+ if (!feed.items.length)
1163
+ out(style.dim("No items."));
1164
+ for (const item of feed.items) {
1165
+ out();
1166
+ out(`${style.bold(item.id)} ${item.title}`);
1167
+ if (item.updateNote)
1168
+ out(style.yellow(item.updateNote));
1169
+ out(style.dim(`${item.updatedAt.slice(0, 10)} https://cve.now/${encodeURIComponent(item.id)}`));
1170
+ }
1171
+ return EXIT.ok;
1172
+ }
1173
+
1174
+ // src/commands/get.ts
1175
+ var MAX_AFFECTED = 8;
1176
+ var rangeText = (r) => {
1177
+ const from = r.introduced && r.introduced !== "0" ? r.introduced : null;
1178
+ if (r.lastAffected && from === r.lastAffected)
1179
+ return `=${from}`;
1180
+ const upper = r.fixed ? `<${r.fixed}` : r.lastAffected ? `<=${r.lastAffected}` : r.limit ? `<${r.limit}` : "(no fix listed)";
1181
+ return from ? `>=${from} ${upper}` : upper === "(no fix listed)" ? "all versions (no fix listed)" : upper;
1182
+ };
1183
+ function signalsOf(e) {
1184
+ const x = e.exploitation ?? {};
1185
+ const out = [];
1186
+ if (x.kev)
1187
+ out.push(`CISA KEV since ${x.kev.dateAdded}`);
1188
+ else if (e.ssvc?.exploitation === "active")
1189
+ out.push("actively exploited");
1190
+ if (x.metasploit?.modules.length)
1191
+ out.push("Metasploit module");
1192
+ if (x.exploitDb?.ids.length || x.githubPoc?.exploits?.length)
1193
+ out.push("public exploit");
1194
+ if (x.nuclei?.templates.length)
1195
+ out.push("Nuclei template");
1196
+ if (x.epssReached)
1197
+ out.push("EPSS reached 0.1");
1198
+ return out;
1199
+ }
1200
+ function printEntry(ctx, e) {
1201
+ const { style, out } = ctx;
1202
+ const cvss = e.severity?.cvss;
1203
+ const rating = ratingFor(cvss) ?? e.statedSeverity?.value ?? null;
1204
+ const signals = signalsOf(e);
1205
+ out([
1206
+ style.bold(e.id),
1207
+ ratingText(style, rating) + (cvss !== undefined ? ` ${cvss.toFixed(1)}` : ""),
1208
+ signals.length ? style.red(signals.join(" · ")) : ""
1209
+ ].filter(Boolean).join(" "));
1210
+ out(e.title);
1211
+ if (e.exposure)
1212
+ out(`${style.dim("Exposure:")} ${e.exposure}`);
1213
+ const about = e.context?.sentence ?? e.summary?.text ?? e.originalDescription;
1214
+ if (about && about !== e.title)
1215
+ out(about);
1216
+ const conditions = e.context?.conditions ?? [];
1217
+ if (conditions.length) {
1218
+ out(style.dim("Conditions:"));
1219
+ for (const c of conditions)
1220
+ out(` - ${c.text}`);
1221
+ }
1222
+ if (e.context?.impact)
1223
+ out(`${style.dim("Impact:")} ${e.context.impact.text}`);
1224
+ const fix = e.context?.fix;
1225
+ if (fix?.versions?.length || fix?.workaround)
1226
+ out(`${style.dim("Fix:")} ${[
1227
+ fix.versions?.length ? fix.versions.join(", ") : "",
1228
+ fix.workaround ? `workaround: ${fix.workaround}` : "",
1229
+ fix.partial ? "(partial)" : ""
1230
+ ].filter(Boolean).join(" ")}`);
1231
+ const affected = e.affected ?? [];
1232
+ if (affected.length) {
1233
+ out(style.dim("Affected:"));
1234
+ for (const a of affected.slice(0, MAX_AFFECTED)) {
1235
+ const name = [a.ecosystem ?? a.vendor, a.package ?? a.product].filter(Boolean).join(" ");
1236
+ const ranges = (a.ranges ?? []).map(rangeText);
1237
+ out(` ${name}: ${ranges.length ? ranges.join("; ") : (a.versions ?? []).join(", ") || "versions not listed"}`);
1238
+ }
1239
+ if (affected.length > MAX_AFFECTED)
1240
+ out(style.dim(` and ${affected.length - MAX_AFFECTED} more`));
1241
+ }
1242
+ const ssvc = e.ssvc;
1243
+ if (ssvc && (ssvc.exploitation || ssvc.automatable || ssvc.technicalImpact))
1244
+ out(`${style.dim("CISA SSVC:")} exploitation ${ssvc.exploitation ?? "unknown"} · automatable ${ssvc.automatable ?? "unknown"} · technical impact ${ssvc.technicalImpact ?? "unknown"}`);
1245
+ if (e.aliases?.length)
1246
+ out(`${style.dim("Also:")} ${e.aliases.join(", ")}`);
1247
+ out(style.cyan(`https://cve.now/${encodeURIComponent(e.id)}`));
1248
+ }
1249
+ async function getCommand(ctx, ids) {
1250
+ let missing = 0;
1251
+ const found = [];
1252
+ for (const id of ids) {
1253
+ try {
1254
+ const entry = await request({
1255
+ path: `/api/v1/entries/${encodeURIComponent(id)}`,
1256
+ options: ctx.api
1257
+ });
1258
+ if (ctx.json)
1259
+ found.push(entry);
1260
+ else {
1261
+ if (found.length || missing)
1262
+ ctx.out();
1263
+ found.push(entry);
1264
+ printEntry(ctx, entry);
1265
+ }
1266
+ } catch (e) {
1267
+ if (e instanceof ApiError && e.status === 404) {
1268
+ missing++;
1269
+ ctx.err(`No cve.now entry for ${id}`);
1270
+ continue;
1271
+ }
1272
+ throw e;
1273
+ }
1274
+ }
1275
+ if (ctx.json)
1276
+ printJson(ctx, ids.length === 1 ? found[0] ?? null : found);
1277
+ return missing ? EXIT.findings : EXIT.ok;
1278
+ }
1279
+
1280
+ // src/commands/search.ts
1281
+ async function searchCommand(ctx, positionals, values) {
1282
+ const results = await request({
1283
+ path: `/api/v1/search${query({
1284
+ q: positionals.length ? positionals.join(" ") : undefined,
1285
+ severity: list(values.severity),
1286
+ tech: list(values.tech),
1287
+ ecosystem: list(values.ecosystem),
1288
+ vendor: list(values.vendor),
1289
+ package: list(values.package),
1290
+ kev: str(values.kev),
1291
+ exploited: str(values.exploited),
1292
+ published: list(values.published),
1293
+ type: str(values.type),
1294
+ limit: intOption(values.limit, "--limit"),
1295
+ cursor: str(values.cursor)
1296
+ })}`,
1297
+ options: ctx.api
1298
+ });
1299
+ if (ctx.json) {
1300
+ printJson(ctx, results);
1301
+ return EXIT.ok;
1302
+ }
1303
+ const { style, out, err } = ctx;
1304
+ if (!results.entries.length) {
1305
+ out("No matches.");
1306
+ if (results.note)
1307
+ out(style.dim(results.note));
1308
+ const suggestions = (results.suggestions ?? []).map((s) => typeof s === "string" ? s : `${s.q}${s.total ? ` (${s.total})` : ""}`);
1309
+ if (suggestions.length) {
1310
+ out("Broader queries that match:");
1311
+ for (const s of suggestions)
1312
+ out(` ${s}`);
1313
+ }
1314
+ return EXIT.ok;
1315
+ }
1316
+ for (const [i, e] of results.entries.entries()) {
1317
+ if (i)
1318
+ out();
1319
+ const flags = [e.kev ? "KEV" : "", e.exploit ? "public exploit" : ""].filter(Boolean).join(" · ");
1320
+ out([
1321
+ style.bold(e.id),
1322
+ ratingText(style, e.severity) + (e.cvss !== null && e.cvss !== undefined ? ` ${e.cvss.toFixed(1)}` : ""),
1323
+ flags ? style.red(flags) : ""
1324
+ ].filter(Boolean).join(" "));
1325
+ out(e.title);
1326
+ out(style.dim(`${e.published?.slice(0, 10) ?? ""} ${e.url}`.trim()));
1327
+ }
1328
+ if (results.next_cursor)
1329
+ err(style.dim(`${results.total !== undefined ? `${results.total.toLocaleString("en-US")} matches. ` : ""}Next page: --cursor ${results.next_cursor}`));
1330
+ return EXIT.ok;
1331
+ }
1332
+
1333
+ // src/commands/top.ts
1334
+ async function topCommand(ctx, values) {
1335
+ const board = await request({
1336
+ path: `/api/v1/leaderboard${query({
1337
+ window: str(values.window),
1338
+ week: str(values.week),
1339
+ tech: list(values.tech),
1340
+ ecosystem: list(values.ecosystem),
1341
+ filter: list(values.filter),
1342
+ limit: intOption(values.limit, "--limit")
1343
+ })}`,
1344
+ options: ctx.api
1345
+ });
1346
+ if (ctx.json) {
1347
+ printJson(ctx, board);
1348
+ return EXIT.ok;
1349
+ }
1350
+ const { style, out } = ctx;
1351
+ out(style.bold(`cve.now Index · ${board.week ? `week ${board.week}` : `this ${board.window ?? "week"}`}`));
1352
+ if (!board.rows.length)
1353
+ out(style.dim("No entries."));
1354
+ for (const row of board.rows) {
1355
+ const move = row.movement === "new" ? style.yellow("new") : typeof row.movement === "number" && row.movement !== 0 ? row.movement > 0 ? style.red(`▲${row.movement}`) : style.green(`▼${-row.movement}`) : "";
1356
+ out();
1357
+ out(`${style.bold(`#${row.rank}`)} ${style.bold(String(row.score))} ${row.id} ${move}`.trimEnd());
1358
+ out(row.product ? `${row.product}: ${row.title}` : row.title);
1359
+ const facts = [
1360
+ row.cvss !== null && row.cvss !== undefined ? `CVSS ${row.cvss}` : "",
1361
+ row.signals?.length ? row.signals.join(", ") : "",
1362
+ row.fix?.length ? `fix: ${row.fix.join(", ")}` : ""
1363
+ ].filter(Boolean);
1364
+ if (facts.length)
1365
+ out(style.dim(facts.join(" · ")));
1366
+ }
1367
+ out();
1368
+ out(style.cyan(board.url));
1369
+ return EXIT.ok;
1370
+ }
1371
+
1372
+ // src/cli.ts
1373
+ var OPTIONS = {
1374
+ json: { type: "boolean" },
1375
+ "no-color": { type: "boolean" },
1376
+ "api-key": { type: "string" },
1377
+ help: { type: "boolean", short: "h" },
1378
+ version: { type: "boolean", short: "v" },
1379
+ filter: { type: "string", multiple: true },
1380
+ tech: { type: "string", multiple: true },
1381
+ ecosystem: { type: "string", multiple: true },
1382
+ limit: { type: "string" },
1383
+ window: { type: "string" },
1384
+ week: { type: "string" },
1385
+ packages: { type: "string", multiple: true },
1386
+ stack: { type: "string" },
1387
+ "fail-on": { type: "string" },
1388
+ ignore: { type: "string", multiple: true },
1389
+ severity: { type: "string", multiple: true },
1390
+ vendor: { type: "string", multiple: true },
1391
+ package: { type: "string", multiple: true },
1392
+ kev: { type: "string" },
1393
+ exploited: { type: "string" },
1394
+ published: { type: "string", multiple: true },
1395
+ type: { type: "string" },
1396
+ cursor: { type: "string" }
1397
+ };
1398
+ var COMMAND_OPTIONS = {
1399
+ get: [],
1400
+ feed: ["filter", "tech", "ecosystem", "limit"],
1401
+ check: ["packages", "stack", "fail-on", "ignore"],
1402
+ top: ["window", "week", "tech", "ecosystem", "filter", "limit"],
1403
+ search: [
1404
+ "severity",
1405
+ "tech",
1406
+ "ecosystem",
1407
+ "vendor",
1408
+ "package",
1409
+ "kev",
1410
+ "exploited",
1411
+ "published",
1412
+ "type",
1413
+ "limit",
1414
+ "cursor"
1415
+ ]
1416
+ };
1417
+ var GLOBAL_OPTIONS = new Set([
1418
+ "json",
1419
+ "no-color",
1420
+ "api-key",
1421
+ "help",
1422
+ "version"
1423
+ ]);
1424
+ var looksLikeId = (s) => /^(CVE-\d{4}-\d+|GHSA(-[0-9a-z]{4}){3}|[A-Z][A-Z0-9]*-[\w.:-]+)$/i.test(s);
1425
+ function help() {
1426
+ const width = Math.max(...CLI_COMMANDS.map((c) => c.name.length));
1427
+ return `cve.now ${VERSION}: vulnerabilities from cve.now in your terminal and CI
1428
+
1429
+ Usage: cve.now <command> [options]
1430
+ cve.now <id> same as cve.now get <id>
1431
+
1432
+ Commands:
1433
+ ${CLI_COMMANDS.map((c) => ` ${c.name.padEnd(width)} ${c.summary}`).join(`
1434
+ `)}
1435
+
1436
+ ${CLI_COMMANDS.map((c) => ` cve.now ${c.usage}`).join(`
1437
+ `)}
1438
+
1439
+ Options:
1440
+ --json Print the API's JSON (check wraps each lockfile's answer in { schemaVersion: 1, results })
1441
+ --no-color No ANSI colors (also NO_COLOR)
1442
+ --api-key <key> A cve.now API key (cvn_...); also CVE_NOW_API_KEY. Not needed: anonymous use is free
1443
+ -v, --version Print the version
1444
+ -h, --help Print this help
1445
+
1446
+ Exit codes: 0 ok, 1 findings at or above --fail-on (or an id not found), 2 usage or parse error,
1447
+ 3 network or API error. CVE_NOW_API_URL points the CLI at another cve.now deployment.
1448
+ Docs: https://cve.now/llms.txt`;
1449
+ }
1450
+ async function readStdinIds() {
1451
+ if (process.stdin.isTTY)
1452
+ return [];
1453
+ const chunks = [];
1454
+ for await (const chunk of process.stdin)
1455
+ chunks.push(chunk);
1456
+ return Buffer.concat(chunks).toString("utf8").split(/[\s,]+/).filter(Boolean);
1457
+ }
1458
+ async function main(argv, env = process.env) {
1459
+ const err = (line = "") => process.stderr.write(`${line}
1460
+ `);
1461
+ let parsed;
1462
+ try {
1463
+ parsed = parseArgs({
1464
+ args: argv,
1465
+ options: OPTIONS,
1466
+ allowPositionals: true,
1467
+ strict: true
1468
+ });
1469
+ } catch (e) {
1470
+ err(`${e.message}
1471
+ Run cve.now --help for usage.`);
1472
+ return EXIT.usage;
1473
+ }
1474
+ const values = parsed.values;
1475
+ let [command, ...rest] = parsed.positionals;
1476
+ if (values.version) {
1477
+ process.stdout.write(`${VERSION}
1478
+ `);
1479
+ return EXIT.ok;
1480
+ }
1481
+ if (values.help || !command) {
1482
+ process.stdout.write(`${help()}
1483
+ `);
1484
+ return command || values.help ? EXIT.ok : EXIT.usage;
1485
+ }
1486
+ if (!Object.hasOwn(COMMAND_OPTIONS, command)) {
1487
+ if (!looksLikeId(command)) {
1488
+ err(`Unknown command: ${command}
1489
+ Run cve.now --help for usage.`);
1490
+ return EXIT.usage;
1491
+ }
1492
+ rest = [command, ...rest];
1493
+ command = "get";
1494
+ }
1495
+ const allowed = new Set([...GLOBAL_OPTIONS, ...COMMAND_OPTIONS[command]]);
1496
+ const stray = Object.keys(values).filter((key) => !allowed.has(key));
1497
+ if (stray.length) {
1498
+ err(`${command} doesn't take ${stray.map((key) => `--${key}`).join(", ")}
1499
+ Run cve.now --help for usage.`);
1500
+ return EXIT.usage;
1501
+ }
1502
+ const json = values.json === true;
1503
+ const color = !json && values["no-color"] !== true && !env.NO_COLOR && process.stdout.isTTY === true;
1504
+ const style = makeStyle(color);
1505
+ const baseUrl = apiBase(env);
1506
+ if (!isHttpUrl(baseUrl)) {
1507
+ err(`CVE_NOW_API_URL must be an http(s) URL, got "${baseUrl}"`);
1508
+ return EXIT.usage;
1509
+ }
1510
+ const ctx = {
1511
+ json,
1512
+ style,
1513
+ cwd: process.cwd(),
1514
+ out: (line = "") => process.stdout.write(`${line}
1515
+ `),
1516
+ err,
1517
+ api: {
1518
+ apiKey: str(values["api-key"]) ?? env.CVE_NOW_API_KEY,
1519
+ baseUrl,
1520
+ onRetry: (seconds) => err(style.dim(`cve.now asked to slow down; retrying in ${seconds}s`))
1521
+ }
1522
+ };
1523
+ try {
1524
+ switch (command) {
1525
+ case "get": {
1526
+ const ids = rest.length ? rest : await readStdinIds();
1527
+ if (!ids.length)
1528
+ throw new UsageError("get takes one or more ids");
1529
+ return await getCommand(ctx, ids);
1530
+ }
1531
+ case "feed":
1532
+ return await feedCommand(ctx, rest, values);
1533
+ case "check":
1534
+ return await checkCommand(ctx, rest, values);
1535
+ case "top":
1536
+ if (rest.length)
1537
+ throw new UsageError("top takes no arguments");
1538
+ return await topCommand(ctx, values);
1539
+ default:
1540
+ return await searchCommand(ctx, rest, values);
1541
+ }
1542
+ } catch (e) {
1543
+ if (e instanceof UsageError) {
1544
+ err(`${e.message}
1545
+ Run cve.now --help for usage.`);
1546
+ return EXIT.usage;
1547
+ }
1548
+ if (e instanceof ApiError && e.status === 404 && (command === "top" || command === "search")) {
1549
+ err(`cve.now ${command} is not available on cve.now yet.`);
1550
+ return EXIT.usage;
1551
+ }
1552
+ if (e instanceof ApiError) {
1553
+ err(e.message);
1554
+ return e.status >= 400 && e.status < 500 && e.status !== 429 ? EXIT.usage : EXIT.api;
1555
+ }
1556
+ if (e instanceof NetworkError) {
1557
+ err(e.message);
1558
+ return EXIT.api;
1559
+ }
1560
+ throw e;
1561
+ }
1562
+ }
1563
+ function isHttpUrl(value) {
1564
+ try {
1565
+ const { protocol } = new URL(value);
1566
+ return protocol === "https:" || protocol === "http:";
1567
+ } catch {
1568
+ return false;
1569
+ }
1570
+ }
1571
+ process.exitCode = await main(process.argv.slice(2));
1572
+ export {
1573
+ main
1574
+ };