@rainy-updates/cli 0.5.2-rc.1 → 0.5.2-rc.2

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.
@@ -0,0 +1,125 @@
1
+ import { asyncPool } from "../../../utils/async-pool.js";
2
+ const GITHUB_ADVISORY_API = "https://api.github.com/advisories";
3
+ export const githubAuditSource = {
4
+ name: "github",
5
+ async fetch(targets, options) {
6
+ const tasks = targets.map((target) => () => queryGitHub(target, options.registryTimeoutMs));
7
+ const results = await asyncPool(options.concurrency, tasks);
8
+ const advisories = [];
9
+ let successfulTargets = 0;
10
+ let failedTargets = 0;
11
+ const errorCounts = new Map();
12
+ for (const result of results) {
13
+ if (result instanceof Error) {
14
+ failedTargets += 1;
15
+ incrementCount(errorCounts, "internal-error");
16
+ continue;
17
+ }
18
+ advisories.push(...result.advisories);
19
+ if (result.ok) {
20
+ successfulTargets += 1;
21
+ }
22
+ else {
23
+ failedTargets += 1;
24
+ incrementCount(errorCounts, result.error ?? "request-failed");
25
+ }
26
+ }
27
+ const status = failedTargets === 0
28
+ ? "ok"
29
+ : successfulTargets === 0
30
+ ? "failed"
31
+ : "partial";
32
+ return {
33
+ advisories,
34
+ warnings: createSourceWarnings("GitHub Advisory DB", targets.length, successfulTargets, failedTargets),
35
+ health: {
36
+ source: "github",
37
+ status,
38
+ attemptedTargets: targets.length,
39
+ successfulTargets,
40
+ failedTargets,
41
+ advisoriesFound: advisories.length,
42
+ message: formatDominantError(errorCounts),
43
+ },
44
+ };
45
+ },
46
+ };
47
+ async function queryGitHub(target, timeoutMs) {
48
+ const url = new URL(GITHUB_ADVISORY_API);
49
+ url.searchParams.set("ecosystem", "npm");
50
+ url.searchParams.set("affects", `${target.name}@${target.version}`);
51
+ url.searchParams.set("per_page", "100");
52
+ let response;
53
+ try {
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
56
+ response = await fetch(url, {
57
+ headers: {
58
+ Accept: "application/vnd.github+json",
59
+ "User-Agent": "rainy-updates-cli",
60
+ },
61
+ signal: controller.signal,
62
+ });
63
+ clearTimeout(timer);
64
+ }
65
+ catch (error) {
66
+ return { advisories: [], ok: false, error: classifyFetchError(error) };
67
+ }
68
+ if (!response.ok) {
69
+ return { advisories: [], ok: false, error: `http-${response.status}` };
70
+ }
71
+ const data = (await response.json());
72
+ const advisories = [];
73
+ for (const item of data) {
74
+ const vulnerability = item.vulnerabilities?.find((entry) => entry.package?.name === target.name);
75
+ const severity = normalizeSeverity(item.severity);
76
+ advisories.push({
77
+ cveId: item.ghsa_id ?? item.cve_id ?? "UNKNOWN",
78
+ packageName: target.name,
79
+ currentVersion: target.version,
80
+ severity,
81
+ vulnerableRange: vulnerability?.vulnerable_version_range ?? "*",
82
+ patchedVersion: vulnerability?.first_patched_version?.identifier?.trim() || null,
83
+ title: item.summary ?? item.ghsa_id ?? "GitHub Advisory",
84
+ url: item.html_url ?? `https://github.com/advisories/${item.ghsa_id}`,
85
+ sources: ["github"],
86
+ });
87
+ }
88
+ return { advisories, ok: true };
89
+ }
90
+ function normalizeSeverity(value) {
91
+ const normalized = (value ?? "medium").toLowerCase();
92
+ if (normalized === "critical" ||
93
+ normalized === "high" ||
94
+ normalized === "medium" ||
95
+ normalized === "low") {
96
+ return normalized;
97
+ }
98
+ if (normalized === "moderate")
99
+ return "medium";
100
+ return "medium";
101
+ }
102
+ function createSourceWarnings(label, attemptedTargets, successfulTargets, failedTargets) {
103
+ if (failedTargets === 0)
104
+ return [];
105
+ if (successfulTargets === 0) {
106
+ return [
107
+ `${label} unavailable for all ${attemptedTargets} audit target${attemptedTargets === 1 ? "" : "s"}.`,
108
+ ];
109
+ }
110
+ return [
111
+ `${label} partially unavailable: ${failedTargets}/${attemptedTargets} audit target${attemptedTargets === 1 ? "" : "s"} failed.`,
112
+ ];
113
+ }
114
+ function classifyFetchError(error) {
115
+ if (error instanceof Error && error.name === "AbortError")
116
+ return "timeout";
117
+ return "network";
118
+ }
119
+ function incrementCount(map, key) {
120
+ map.set(key, (map.get(key) ?? 0) + 1);
121
+ }
122
+ function formatDominantError(errorCounts) {
123
+ const sorted = [...errorCounts.entries()].sort((a, b) => b[1] - a[1]);
124
+ return sorted[0]?.[0];
125
+ }
@@ -0,0 +1,6 @@
1
+ import type { AuditOptions, AuditSourceName } from "../../../types/index.js";
2
+ import type { AuditTarget } from "../targets.js";
3
+ import type { AuditSourceAdapter, AuditSourceAggregateResult } from "./types.js";
4
+ export declare function fetchAdvisoriesFromSources(targets: AuditTarget[], options: Pick<AuditOptions, "concurrency" | "registryTimeoutMs" | "sourceMode">, sourceMap?: Record<AuditSourceName, AuditSourceAdapter>): Promise<AuditSourceAggregateResult & {
5
+ sourcesUsed: AuditSourceName[];
6
+ }>;
@@ -0,0 +1,92 @@
1
+ import { compareVersions, parseVersion } from "../../../utils/semver.js";
2
+ import { githubAuditSource } from "./github.js";
3
+ import { osvAuditSource } from "./osv.js";
4
+ const SOURCE_MAP = {
5
+ osv: osvAuditSource,
6
+ github: githubAuditSource,
7
+ };
8
+ export async function fetchAdvisoriesFromSources(targets, options, sourceMap = SOURCE_MAP) {
9
+ const selected = selectSources(options.sourceMode);
10
+ const results = await Promise.all(selected.map((name) => sourceMap[name].fetch(targets, options)));
11
+ const warnings = normalizeSourceWarnings(results.flatMap((result) => result.warnings), results.map((result) => result.health));
12
+ const merged = mergeAdvisories(results.flatMap((result) => result.advisories));
13
+ return {
14
+ advisories: merged,
15
+ warnings,
16
+ sourcesUsed: selected,
17
+ sourceHealth: results.map((result) => result.health),
18
+ };
19
+ }
20
+ function selectSources(mode) {
21
+ if (mode === "osv")
22
+ return ["osv"];
23
+ if (mode === "github")
24
+ return ["github"];
25
+ return ["osv", "github"];
26
+ }
27
+ function mergeAdvisories(advisories) {
28
+ const merged = new Map();
29
+ for (const advisory of advisories) {
30
+ const key = [
31
+ advisory.packageName,
32
+ advisory.currentVersion ?? "?",
33
+ advisory.cveId,
34
+ ].join("|");
35
+ const existing = merged.get(key);
36
+ if (!existing) {
37
+ merged.set(key, advisory);
38
+ continue;
39
+ }
40
+ merged.set(key, {
41
+ ...existing,
42
+ severity: severityRank(advisory.severity) > severityRank(existing.severity)
43
+ ? advisory.severity
44
+ : existing.severity,
45
+ vulnerableRange: existing.vulnerableRange === "*" && advisory.vulnerableRange !== "*"
46
+ ? advisory.vulnerableRange
47
+ : existing.vulnerableRange,
48
+ patchedVersion: choosePreferredPatch(existing.patchedVersion, advisory.patchedVersion),
49
+ title: existing.title.length >= advisory.title.length ? existing.title : advisory.title,
50
+ url: existing.url.length >= advisory.url.length ? existing.url : advisory.url,
51
+ sources: [...new Set([...existing.sources, ...advisory.sources])].sort(),
52
+ });
53
+ }
54
+ return [...merged.values()];
55
+ }
56
+ function normalizeSourceWarnings(warnings, sourceHealth) {
57
+ const normalized = [...warnings];
58
+ const successful = sourceHealth.filter((item) => item.status !== "failed");
59
+ const failed = sourceHealth.filter((item) => item.status === "failed");
60
+ if (failed.length > 0 && successful.length > 0) {
61
+ const failedNames = failed.map((item) => formatSourceName(item.source)).join(", ");
62
+ const successfulNames = successful
63
+ .map((item) => formatSourceName(item.source))
64
+ .join(", ");
65
+ normalized.push(`Continuing with partial advisory coverage: ${failedNames} failed, ${successfulNames} still returned results.`);
66
+ }
67
+ return normalized;
68
+ }
69
+ function formatSourceName(source) {
70
+ return source === "osv" ? "OSV.dev" : "GitHub Advisory DB";
71
+ }
72
+ function choosePreferredPatch(current, next) {
73
+ if (!current)
74
+ return next;
75
+ if (!next)
76
+ return current;
77
+ const currentParsed = parseVersion(current);
78
+ const nextParsed = parseVersion(next);
79
+ if (currentParsed && nextParsed) {
80
+ return compareVersions(currentParsed, nextParsed) <= 0 ? current : next;
81
+ }
82
+ return current <= next ? current : next;
83
+ }
84
+ function severityRank(value) {
85
+ if (value === "critical")
86
+ return 4;
87
+ if (value === "high")
88
+ return 3;
89
+ if (value === "medium")
90
+ return 2;
91
+ return 1;
92
+ }
@@ -0,0 +1,2 @@
1
+ import type { AuditSourceAdapter } from "./types.js";
2
+ export declare const osvAuditSource: AuditSourceAdapter;
@@ -0,0 +1,131 @@
1
+ import { asyncPool } from "../../../utils/async-pool.js";
2
+ const OSV_API = "https://api.osv.dev/v1/query";
3
+ export const osvAuditSource = {
4
+ name: "osv",
5
+ async fetch(targets, options) {
6
+ const tasks = targets.map((target) => () => queryOsv(target, options.registryTimeoutMs));
7
+ const results = await asyncPool(options.concurrency, tasks);
8
+ const advisories = [];
9
+ let successfulTargets = 0;
10
+ let failedTargets = 0;
11
+ const errorCounts = new Map();
12
+ for (const result of results) {
13
+ if (result instanceof Error) {
14
+ failedTargets += 1;
15
+ incrementCount(errorCounts, "internal-error");
16
+ continue;
17
+ }
18
+ advisories.push(...result.advisories);
19
+ if (result.ok) {
20
+ successfulTargets += 1;
21
+ }
22
+ else {
23
+ failedTargets += 1;
24
+ incrementCount(errorCounts, result.error ?? "request-failed");
25
+ }
26
+ }
27
+ const status = failedTargets === 0
28
+ ? "ok"
29
+ : successfulTargets === 0
30
+ ? "failed"
31
+ : "partial";
32
+ return {
33
+ advisories,
34
+ warnings: createSourceWarnings("OSV.dev", targets.length, successfulTargets, failedTargets),
35
+ health: {
36
+ source: "osv",
37
+ status,
38
+ attemptedTargets: targets.length,
39
+ successfulTargets,
40
+ failedTargets,
41
+ advisoriesFound: advisories.length,
42
+ message: formatDominantError(errorCounts),
43
+ },
44
+ };
45
+ },
46
+ };
47
+ async function queryOsv(target, timeoutMs) {
48
+ const body = JSON.stringify({
49
+ package: { name: target.name, ecosystem: "npm" },
50
+ version: target.version,
51
+ });
52
+ let response;
53
+ try {
54
+ const controller = new AbortController();
55
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
56
+ response = await fetch(OSV_API, {
57
+ method: "POST",
58
+ headers: { "Content-Type": "application/json" },
59
+ body,
60
+ signal: controller.signal,
61
+ });
62
+ clearTimeout(timer);
63
+ }
64
+ catch (error) {
65
+ return { advisories: [], ok: false, error: classifyFetchError(error) };
66
+ }
67
+ if (!response.ok) {
68
+ return { advisories: [], ok: false, error: `http-${response.status}` };
69
+ }
70
+ const data = (await response.json());
71
+ const advisories = [];
72
+ for (const vuln of data.vulns ?? []) {
73
+ const cveId = vuln.id ?? "UNKNOWN";
74
+ const rawSeverity = (vuln.database_specific?.severity ?? "medium").toLowerCase();
75
+ const severity = (["critical", "high", "medium", "low"].includes(rawSeverity)
76
+ ? rawSeverity
77
+ : "medium");
78
+ let patchedVersion = null;
79
+ let vulnerableRange = "*";
80
+ for (const affected of vuln.affected ?? []) {
81
+ if (affected.package?.name !== target.name)
82
+ continue;
83
+ for (const range of affected.ranges ?? []) {
84
+ const fixedEvent = range.events?.find((event) => event.fixed);
85
+ if (fixedEvent?.fixed) {
86
+ patchedVersion = fixedEvent.fixed;
87
+ const introducedEvent = range.events?.find((event) => event.introduced);
88
+ vulnerableRange = introducedEvent?.introduced
89
+ ? `>=${introducedEvent.introduced} <${patchedVersion}`
90
+ : `<${patchedVersion}`;
91
+ }
92
+ }
93
+ }
94
+ advisories.push({
95
+ cveId,
96
+ packageName: target.name,
97
+ currentVersion: target.version,
98
+ severity,
99
+ vulnerableRange,
100
+ patchedVersion,
101
+ title: vuln.summary ?? cveId,
102
+ url: vuln.references?.[0]?.url ?? `https://osv.dev/vulnerability/${cveId}`,
103
+ sources: ["osv"],
104
+ });
105
+ }
106
+ return { advisories, ok: true };
107
+ }
108
+ function createSourceWarnings(label, attemptedTargets, successfulTargets, failedTargets) {
109
+ if (failedTargets === 0)
110
+ return [];
111
+ if (successfulTargets === 0) {
112
+ return [
113
+ `${label} unavailable for all ${attemptedTargets} audit target${attemptedTargets === 1 ? "" : "s"}.`,
114
+ ];
115
+ }
116
+ return [
117
+ `${label} partially unavailable: ${failedTargets}/${attemptedTargets} audit target${attemptedTargets === 1 ? "" : "s"} failed.`,
118
+ ];
119
+ }
120
+ function classifyFetchError(error) {
121
+ if (error instanceof Error && error.name === "AbortError")
122
+ return "timeout";
123
+ return "network";
124
+ }
125
+ function incrementCount(map, key) {
126
+ map.set(key, (map.get(key) ?? 0) + 1);
127
+ }
128
+ function formatDominantError(errorCounts) {
129
+ const sorted = [...errorCounts.entries()].sort((a, b) => b[1] - a[1]);
130
+ return sorted[0]?.[0];
131
+ }
@@ -0,0 +1,21 @@
1
+ import type { AuditOptions, AuditSourceStatus, AuditSourceName, CveAdvisory } from "../../../types/index.js";
2
+ import type { AuditTarget } from "../targets.js";
3
+ export interface AuditSourceTargetResult {
4
+ advisories: CveAdvisory[];
5
+ ok: boolean;
6
+ error?: string;
7
+ }
8
+ export interface AuditSourceFetchResult {
9
+ advisories: CveAdvisory[];
10
+ warnings: string[];
11
+ health: AuditSourceStatus;
12
+ }
13
+ export interface AuditSourceAggregateResult {
14
+ advisories: CveAdvisory[];
15
+ warnings: string[];
16
+ sourceHealth: AuditSourceStatus[];
17
+ }
18
+ export interface AuditSourceAdapter {
19
+ name: AuditSourceName;
20
+ fetch(targets: AuditTarget[], options: Pick<AuditOptions, "concurrency" | "registryTimeoutMs">): Promise<AuditSourceFetchResult>;
21
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import type { PackageDependency } from "../../types/index.js";
2
+ export interface AuditTarget {
3
+ name: string;
4
+ version: string;
5
+ packageDir: string;
6
+ manifestRange: string;
7
+ resolution: "lockfile" | "manifest";
8
+ lockfilePath?: string;
9
+ }
10
+ export interface AuditTargetResolution {
11
+ targets: AuditTarget[];
12
+ warnings: string[];
13
+ resolution: {
14
+ lockfile: number;
15
+ manifest: number;
16
+ unresolved: number;
17
+ };
18
+ }
19
+ export declare function extractAuditVersion(range: string): string | null;
20
+ export declare function resolveAuditTargets(rootCwd: string, packageDirs: string[], depsByDir: Map<string, PackageDependency[]>): Promise<AuditTargetResolution>;