kubeagent 0.1.35 → 0.1.36

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,87 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseImage, normalizeVersion, majorMinor, major } from "./image-parser.js";
3
+ describe("parseImage", () => {
4
+ it("parses a bare image name", () => {
5
+ expect(parseImage("nginx")).toEqual({
6
+ registry: "docker.io",
7
+ repository: "library/nginx",
8
+ name: "nginx",
9
+ tag: "latest",
10
+ digest: undefined,
11
+ });
12
+ });
13
+ it("parses a bare image with a tag", () => {
14
+ expect(parseImage("nginx:1.24.0")).toMatchObject({
15
+ registry: "docker.io",
16
+ repository: "library/nginx",
17
+ name: "nginx",
18
+ tag: "1.24.0",
19
+ });
20
+ });
21
+ it("parses a docker hub user/repo image", () => {
22
+ expect(parseImage("bitnami/postgresql:15")).toMatchObject({
23
+ registry: "docker.io",
24
+ repository: "bitnami/postgresql",
25
+ name: "postgresql",
26
+ tag: "15",
27
+ });
28
+ });
29
+ it("parses a custom registry", () => {
30
+ expect(parseImage("quay.io/prometheus/prometheus:v2.45.0")).toMatchObject({
31
+ registry: "quay.io",
32
+ repository: "prometheus/prometheus",
33
+ name: "prometheus",
34
+ tag: "v2.45.0",
35
+ });
36
+ });
37
+ it("parses registry.k8s.io single-segment paths without forcing library/", () => {
38
+ expect(parseImage("registry.k8s.io/etcd:3.5.9-0")).toMatchObject({
39
+ registry: "registry.k8s.io",
40
+ repository: "etcd",
41
+ name: "etcd",
42
+ tag: "3.5.9-0",
43
+ });
44
+ });
45
+ it("parses a registry with a port", () => {
46
+ expect(parseImage("localhost:5000/mything:1.0")).toMatchObject({
47
+ registry: "localhost:5000",
48
+ repository: "mything",
49
+ name: "mything",
50
+ tag: "1.0",
51
+ });
52
+ });
53
+ it("parses a digest-only reference", () => {
54
+ const p = parseImage("nginx@sha256:abcdef");
55
+ expect(p.repository).toBe("library/nginx");
56
+ expect(p.tag).toBe("");
57
+ expect(p.digest).toBe("sha256:abcdef");
58
+ });
59
+ it("parses tag + digest", () => {
60
+ const p = parseImage("nginx:1.24@sha256:abcdef");
61
+ expect(p.tag).toBe("1.24");
62
+ expect(p.digest).toBe("sha256:abcdef");
63
+ });
64
+ });
65
+ describe("normalizeVersion", () => {
66
+ it("strips a leading v", () => {
67
+ expect(normalizeVersion("v1.2.3")).toBe("1.2.3");
68
+ });
69
+ it("strips suffixes like -alpine", () => {
70
+ expect(normalizeVersion("15.2-alpine")).toBe("15.2");
71
+ });
72
+ it("returns empty for placeholder tags", () => {
73
+ expect(normalizeVersion("latest")).toBe("");
74
+ expect(normalizeVersion("stable")).toBe("");
75
+ expect(normalizeVersion("")).toBe("");
76
+ });
77
+ });
78
+ describe("major/majorMinor", () => {
79
+ it("extracts major", () => {
80
+ expect(major("15.2.1")).toBe("15");
81
+ expect(major("11")).toBe("11");
82
+ });
83
+ it("extracts major.minor", () => {
84
+ expect(majorMinor("15.2.1")).toBe("15.2");
85
+ expect(majorMinor("11")).toBe("11");
86
+ });
87
+ });
@@ -0,0 +1,33 @@
1
+ import type { KubectlOptions } from "../kubectl.js";
2
+ import { type Candidate, type WorkloadRef } from "./sources.js";
3
+ import { type CveFinding } from "./osv.js";
4
+ import { type EolStatus } from "./eol.js";
5
+ export interface DetectedApp {
6
+ canonical: string;
7
+ display: string;
8
+ version: string;
9
+ sources: Array<Candidate["source"]>;
10
+ workloads: WorkloadRef[];
11
+ cves: CveFinding[];
12
+ eol: EolStatus | null;
13
+ catalogMatch: boolean;
14
+ }
15
+ export interface UnknownApp {
16
+ name: string;
17
+ version: string;
18
+ workloads: WorkloadRef[];
19
+ }
20
+ export interface DetectionResult {
21
+ apps: DetectedApp[];
22
+ unknowns: UnknownApp[];
23
+ scanned: {
24
+ workloads: number;
25
+ helmReleases: number;
26
+ };
27
+ }
28
+ export interface DetectOptions extends KubectlOptions {
29
+ noCache?: boolean;
30
+ onProgress?: (step: string) => void;
31
+ }
32
+ export declare function detectApplications(opts?: DetectOptions): Promise<DetectionResult>;
33
+ export declare function formatApplicationsMarkdown(result: DetectionResult): string;
@@ -0,0 +1,217 @@
1
+ import { fetchWorkloadCandidates, fetchHelmCandidates, fetchIngressHostMap, workloadKey, } from "./sources.js";
2
+ import { parseImage, normalizeVersion } from "./image-parser.js";
3
+ import { lookupCatalog, lookupChart } from "./catalog.js";
4
+ import { queryOsv } from "./osv.js";
5
+ import { queryEol } from "./eol.js";
6
+ function recordWorkload(map, ref, hostsByWorkload) {
7
+ const key = workloadKey(ref);
8
+ if (!map.has(key)) {
9
+ const hosts = hostsByWorkload?.get(key);
10
+ map.set(key, hosts && hosts.length > 0 ? { ...ref, hosts } : ref);
11
+ }
12
+ }
13
+ function helmWorkloadRef(c) {
14
+ return { kind: "Deployment", namespace: c.namespace, name: c.releaseName };
15
+ }
16
+ export async function detectApplications(opts = {}) {
17
+ const { onProgress, noCache } = opts;
18
+ const kubectlOpts = {
19
+ context: opts.context,
20
+ namespace: opts.namespace,
21
+ timeout: opts.timeout,
22
+ };
23
+ onProgress?.("workloads");
24
+ const [workloadCandidates, ingressMap] = await Promise.all([
25
+ fetchWorkloadCandidates(kubectlOpts),
26
+ fetchIngressHostMap(kubectlOpts).catch(() => new Map()),
27
+ ]);
28
+ onProgress?.("helm");
29
+ const helmCandidates = await fetchHelmCandidates(kubectlOpts);
30
+ const known = new Map();
31
+ const unknowns = new Map();
32
+ for (const c of workloadCandidates) {
33
+ if (c.source === "image") {
34
+ const parsed = parseImage(c.image);
35
+ const version = normalizeVersion(parsed.tag);
36
+ const match = lookupCatalog(parsed.repository, parsed.name);
37
+ if (match && version) {
38
+ const key = `${match.canonical}@${version}`;
39
+ let det = known.get(key);
40
+ if (!det) {
41
+ det = {
42
+ canonical: match.canonical,
43
+ display: match.entry.display,
44
+ version,
45
+ entry: match.entry,
46
+ sources: new Set(),
47
+ workloads: new Map(),
48
+ };
49
+ known.set(key, det);
50
+ }
51
+ det.sources.add("image");
52
+ recordWorkload(det.workloads, c.workload, ingressMap);
53
+ }
54
+ else if (version) {
55
+ const key = `${parsed.name}@${version}`;
56
+ let u = unknowns.get(key);
57
+ if (!u) {
58
+ u = { name: parsed.name, version, workloads: [] };
59
+ unknowns.set(key, u);
60
+ }
61
+ if (!u.workloads.some((w) => w.kind === c.workload.kind && w.namespace === c.workload.namespace && w.name === c.workload.name)) {
62
+ const hosts = ingressMap.get(workloadKey(c.workload));
63
+ u.workloads.push(hosts && hosts.length > 0 ? { ...c.workload, hosts } : c.workload);
64
+ }
65
+ }
66
+ }
67
+ else if (c.source === "label") {
68
+ const match = lookupCatalog(c.appName, c.appName);
69
+ const version = normalizeVersion(c.appVersion);
70
+ if (match && version) {
71
+ const key = `${match.canonical}@${version}`;
72
+ let det = known.get(key);
73
+ if (!det) {
74
+ det = {
75
+ canonical: match.canonical,
76
+ display: match.entry.display,
77
+ version,
78
+ entry: match.entry,
79
+ sources: new Set(),
80
+ workloads: new Map(),
81
+ };
82
+ known.set(key, det);
83
+ }
84
+ det.sources.add("label");
85
+ recordWorkload(det.workloads, c.workload, ingressMap);
86
+ }
87
+ }
88
+ }
89
+ for (const c of helmCandidates) {
90
+ const match = lookupChart(c.chart);
91
+ const version = normalizeVersion(c.appVersion ?? c.chartVersion);
92
+ if (match && version) {
93
+ const key = `${match.canonical}@${version}`;
94
+ let det = known.get(key);
95
+ if (!det) {
96
+ det = {
97
+ canonical: match.canonical,
98
+ display: match.entry.display,
99
+ version,
100
+ entry: match.entry,
101
+ sources: new Set(),
102
+ workloads: new Map(),
103
+ };
104
+ known.set(key, det);
105
+ }
106
+ det.sources.add("helm");
107
+ recordWorkload(det.workloads, helmWorkloadRef(c), ingressMap);
108
+ }
109
+ }
110
+ onProgress?.("cve");
111
+ const apps = [];
112
+ for (const det of known.values()) {
113
+ const cves = det.entry.osv
114
+ ? await queryOsv(det.entry.osv, det.version, { noCache })
115
+ : [];
116
+ const eol = det.entry.eolProduct
117
+ ? await queryEol(det.entry.eolProduct, det.version, { noCache })
118
+ : null;
119
+ apps.push({
120
+ canonical: det.canonical,
121
+ display: det.display,
122
+ version: det.version,
123
+ sources: [...det.sources],
124
+ workloads: [...det.workloads.values()],
125
+ cves,
126
+ eol,
127
+ catalogMatch: true,
128
+ });
129
+ }
130
+ apps.sort((a, b) => {
131
+ const sev = (x) => {
132
+ if (x.eol?.eol)
133
+ return 0;
134
+ if (x.cves.length > 0)
135
+ return 1;
136
+ return 2;
137
+ };
138
+ const s = sev(a) - sev(b);
139
+ return s !== 0 ? s : a.display.localeCompare(b.display);
140
+ });
141
+ return {
142
+ apps,
143
+ unknowns: [...unknowns.values()].sort((a, b) => a.name.localeCompare(b.name)),
144
+ scanned: {
145
+ workloads: workloadCandidates.length,
146
+ helmReleases: helmCandidates.length,
147
+ },
148
+ };
149
+ }
150
+ function workloadLabel(w) {
151
+ const base = `${w.kind}/${w.namespace}/${w.name}`;
152
+ return w.hosts && w.hosts.length > 0 ? `${base} (${w.hosts.join(", ")})` : base;
153
+ }
154
+ function escapeMd(s) {
155
+ return s.replace(/\|/g, "\\|");
156
+ }
157
+ export function formatApplicationsMarkdown(result) {
158
+ const lines = [];
159
+ lines.push("# Detected Open-Source Applications", "");
160
+ lines.push(`_Scanned ${result.scanned.workloads} workload container(s) and ${result.scanned.helmReleases} Helm release(s)._`, "");
161
+ const eolApps = result.apps.filter((a) => a.eol?.eol);
162
+ const cveApps = result.apps.filter((a) => !a.eol?.eol && a.cves.length > 0);
163
+ const healthy = result.apps.filter((a) => !a.eol?.eol && a.cves.length === 0);
164
+ if (eolApps.length > 0) {
165
+ lines.push("## End-of-Life", "");
166
+ lines.push("| App | Version | EOL since | Latest in cycle | Workloads |");
167
+ lines.push("|-----|---------|-----------|-----------------|-----------|");
168
+ for (const a of eolApps) {
169
+ const eolStr = a.eol?.eolDate ?? "yes";
170
+ const latest = a.eol?.latest ?? "—";
171
+ const wls = a.workloads.map(workloadLabel).join(", ");
172
+ lines.push(`| ${escapeMd(a.display)} | \`${a.version}\` | ${eolStr} | \`${latest}\` | ${escapeMd(wls)} |`);
173
+ }
174
+ lines.push("");
175
+ }
176
+ if (cveApps.length > 0) {
177
+ lines.push("## Known Vulnerabilities", "");
178
+ for (const a of cveApps) {
179
+ lines.push(`### ${a.display} \`${a.version}\``);
180
+ lines.push(`Workloads: ${a.workloads.map(workloadLabel).join(", ")}`);
181
+ lines.push("");
182
+ lines.push("| CVE | Severity | Fixed in | Summary |");
183
+ lines.push("|-----|----------|----------|---------|");
184
+ for (const c of a.cves.slice(0, 20)) {
185
+ const ref = c.reference ? `[${c.id}](${c.reference})` : c.id;
186
+ const fixed = c.fixed ? `\`${c.fixed}\`` : "—";
187
+ const sev = c.severity ?? "—";
188
+ lines.push(`| ${ref} | ${sev} | ${fixed} | ${escapeMd(c.summary || "")} |`);
189
+ }
190
+ if (a.cves.length > 20) {
191
+ lines.push(`| _… and ${a.cves.length - 20} more_ | | | |`);
192
+ }
193
+ lines.push("");
194
+ }
195
+ }
196
+ if (healthy.length > 0) {
197
+ lines.push("## Up-to-date", "");
198
+ lines.push("| App | Version | Workloads |");
199
+ lines.push("|-----|---------|-----------|");
200
+ for (const a of healthy) {
201
+ const wls = a.workloads.map(workloadLabel).join(", ");
202
+ lines.push(`| ${escapeMd(a.display)} | \`${a.version}\` | ${escapeMd(wls)} |`);
203
+ }
204
+ lines.push("");
205
+ }
206
+ if (result.unknowns.length > 0) {
207
+ lines.push("## Other / not in catalog", "");
208
+ lines.push("Images that look like OSS apps but aren't in the CVE/EOL catalog yet:");
209
+ lines.push("");
210
+ for (const u of result.unknowns) {
211
+ const wls = u.workloads.map(workloadLabel).join(", ");
212
+ lines.push(`- \`${u.name}:${u.version}\` — ${wls}`);
213
+ }
214
+ lines.push("");
215
+ }
216
+ return lines.join("\n");
217
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,210 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ vi.mock("./sources.js", () => ({
3
+ fetchWorkloadCandidates: vi.fn(),
4
+ fetchHelmCandidates: vi.fn(),
5
+ fetchIngressHostMap: vi.fn(),
6
+ workloadKey: (ref) => `${ref.kind}/${ref.namespace}/${ref.name}`,
7
+ }));
8
+ vi.mock("./osv.js", () => ({
9
+ queryOsv: vi.fn(),
10
+ }));
11
+ vi.mock("./eol.js", () => ({
12
+ queryEol: vi.fn(),
13
+ }));
14
+ import { fetchWorkloadCandidates, fetchHelmCandidates, fetchIngressHostMap } from "./sources.js";
15
+ import { queryOsv } from "./osv.js";
16
+ import { queryEol } from "./eol.js";
17
+ import { detectApplications, formatApplicationsMarkdown } from "./index.js";
18
+ const fetchWorkloads = vi.mocked(fetchWorkloadCandidates);
19
+ const fetchHelm = vi.mocked(fetchHelmCandidates);
20
+ const fetchIngress = vi.mocked(fetchIngressHostMap);
21
+ const osv = vi.mocked(queryOsv);
22
+ const eol = vi.mocked(queryEol);
23
+ describe("detectApplications", () => {
24
+ beforeEach(() => {
25
+ fetchWorkloads.mockReset();
26
+ fetchHelm.mockReset();
27
+ fetchIngress.mockReset();
28
+ osv.mockReset();
29
+ eol.mockReset();
30
+ fetchIngress.mockResolvedValue(new Map());
31
+ osv.mockResolvedValue([]);
32
+ eol.mockResolvedValue(null);
33
+ });
34
+ it("merges image and label candidates pointing at the same app/version", async () => {
35
+ fetchWorkloads.mockResolvedValue([
36
+ {
37
+ source: "image",
38
+ image: "postgres:15.2",
39
+ container: "db",
40
+ workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
41
+ },
42
+ {
43
+ source: "label",
44
+ appName: "postgres",
45
+ appVersion: "15.2",
46
+ workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
47
+ },
48
+ ]);
49
+ fetchHelm.mockResolvedValue([]);
50
+ const result = await detectApplications({ noCache: true });
51
+ expect(result.apps).toHaveLength(1);
52
+ expect(result.apps[0].canonical).toBe("postgres");
53
+ expect(result.apps[0].version).toBe("15.2");
54
+ expect(result.apps[0].sources.sort()).toEqual(["image", "label"]);
55
+ expect(result.apps[0].workloads).toHaveLength(1);
56
+ });
57
+ it("tracks images not in the catalog as unknowns", async () => {
58
+ fetchWorkloads.mockResolvedValue([
59
+ {
60
+ source: "image",
61
+ image: "acme/internal-app:1.2.3",
62
+ container: "app",
63
+ workload: { kind: "Deployment", namespace: "team-x", name: "foo" },
64
+ },
65
+ ]);
66
+ fetchHelm.mockResolvedValue([]);
67
+ const result = await detectApplications({ noCache: true });
68
+ expect(result.apps).toHaveLength(0);
69
+ expect(result.unknowns).toHaveLength(1);
70
+ expect(result.unknowns[0]).toMatchObject({ name: "internal-app", version: "1.2.3" });
71
+ });
72
+ it("flags EOL apps via the eol lookup", async () => {
73
+ fetchWorkloads.mockResolvedValue([
74
+ {
75
+ source: "image",
76
+ image: "postgres:11.18",
77
+ container: "db",
78
+ workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
79
+ },
80
+ ]);
81
+ fetchHelm.mockResolvedValue([]);
82
+ eol.mockResolvedValue({
83
+ product: "postgres",
84
+ cycle: "11",
85
+ eol: true,
86
+ eolDate: "2023-11-09",
87
+ latest: "11.22",
88
+ lts: false,
89
+ });
90
+ const result = await detectApplications({ noCache: true });
91
+ expect(result.apps[0].eol?.eol).toBe(true);
92
+ expect(result.apps[0].eol?.eolDate).toBe("2023-11-09");
93
+ });
94
+ it("attaches CVE findings from OSV", async () => {
95
+ fetchWorkloads.mockResolvedValue([
96
+ {
97
+ source: "image",
98
+ image: "redis:7.0.5",
99
+ container: "cache",
100
+ workload: { kind: "Deployment", namespace: "infra", name: "redis" },
101
+ },
102
+ ]);
103
+ fetchHelm.mockResolvedValue([]);
104
+ osv.mockResolvedValue([
105
+ { id: "CVE-2023-9999", summary: "Boom.", severity: "high", fixed: "7.0.12" },
106
+ ]);
107
+ const result = await detectApplications({ noCache: true });
108
+ expect(result.apps[0].cves).toHaveLength(1);
109
+ expect(result.apps[0].cves[0].id).toBe("CVE-2023-9999");
110
+ });
111
+ it("orders apps by severity (EOL → CVEs → healthy)", async () => {
112
+ fetchWorkloads.mockResolvedValue([
113
+ {
114
+ source: "image",
115
+ image: "nginx:1.25.3",
116
+ container: "web",
117
+ workload: { kind: "Deployment", namespace: "edge", name: "nginx" },
118
+ },
119
+ {
120
+ source: "image",
121
+ image: "postgres:11.18",
122
+ container: "db",
123
+ workload: { kind: "StatefulSet", namespace: "prod", name: "db" },
124
+ },
125
+ {
126
+ source: "image",
127
+ image: "redis:7.0.5",
128
+ container: "cache",
129
+ workload: { kind: "Deployment", namespace: "infra", name: "redis" },
130
+ },
131
+ ]);
132
+ fetchHelm.mockResolvedValue([]);
133
+ eol.mockImplementation(async (product) => product === "postgres"
134
+ ? { product: "postgres", cycle: "11", eol: true, eolDate: "2023-11-09", lts: false }
135
+ : null);
136
+ osv.mockImplementation(async (lookup) => lookup.name === "redis"
137
+ ? [{ id: "CVE-2023-1", summary: "x", severity: "medium" }]
138
+ : []);
139
+ const result = await detectApplications({ noCache: true });
140
+ expect(result.apps.map((a) => a.canonical)).toEqual(["postgres", "redis", "nginx"]);
141
+ });
142
+ it("uses Helm chart appVersion when available", async () => {
143
+ fetchWorkloads.mockResolvedValue([]);
144
+ fetchHelm.mockResolvedValue([
145
+ {
146
+ source: "helm",
147
+ chart: "postgresql",
148
+ chartVersion: "12.0.0",
149
+ appVersion: "15.2.0",
150
+ namespace: "data",
151
+ releaseName: "primary",
152
+ },
153
+ ]);
154
+ const result = await detectApplications({ noCache: true });
155
+ expect(result.apps).toHaveLength(1);
156
+ expect(result.apps[0].canonical).toBe("postgres");
157
+ expect(result.apps[0].version).toBe("15.2.0");
158
+ expect(result.apps[0].sources).toContain("helm");
159
+ });
160
+ });
161
+ describe("formatApplicationsMarkdown", () => {
162
+ it("produces sections for EOL, CVEs, healthy, and unknowns", () => {
163
+ const md = formatApplicationsMarkdown({
164
+ apps: [
165
+ {
166
+ canonical: "postgres",
167
+ display: "PostgreSQL",
168
+ version: "11.18",
169
+ sources: ["image"],
170
+ workloads: [{ kind: "StatefulSet", namespace: "prod", name: "db" }],
171
+ cves: [],
172
+ eol: { product: "postgres", cycle: "11", eol: true, eolDate: "2023-11-09", latest: "11.22", lts: false },
173
+ catalogMatch: true,
174
+ },
175
+ {
176
+ canonical: "redis",
177
+ display: "Redis",
178
+ version: "7.0.5",
179
+ sources: ["image"],
180
+ workloads: [{ kind: "Deployment", namespace: "infra", name: "redis" }],
181
+ cves: [{ id: "CVE-2023-1", summary: "boom", severity: "high", fixed: "7.0.12" }],
182
+ eol: null,
183
+ catalogMatch: true,
184
+ },
185
+ {
186
+ canonical: "nginx",
187
+ display: "nginx",
188
+ version: "1.25.3",
189
+ sources: ["image"],
190
+ workloads: [{ kind: "Deployment", namespace: "edge", name: "nginx" }],
191
+ cves: [],
192
+ eol: null,
193
+ catalogMatch: true,
194
+ },
195
+ ],
196
+ unknowns: [
197
+ { name: "internal-app", version: "1.2.3", workloads: [{ kind: "Deployment", namespace: "team-x", name: "foo" }] },
198
+ ],
199
+ scanned: { workloads: 4, helmReleases: 0 },
200
+ });
201
+ expect(md).toContain("## End-of-Life");
202
+ expect(md).toContain("PostgreSQL");
203
+ expect(md).toContain("## Known Vulnerabilities");
204
+ expect(md).toContain("CVE-2023-1");
205
+ expect(md).toContain("## Up-to-date");
206
+ expect(md).toContain("nginx");
207
+ expect(md).toContain("## Other / not in catalog");
208
+ expect(md).toContain("internal-app");
209
+ });
210
+ });
@@ -0,0 +1,12 @@
1
+ import type { OsvLookup } from "./catalog.js";
2
+ export interface CveFinding {
3
+ id: string;
4
+ summary: string;
5
+ severity?: string;
6
+ fixed?: string;
7
+ reference?: string;
8
+ }
9
+ export declare function queryOsv(lookup: OsvLookup, version: string, opts?: {
10
+ noCache?: boolean;
11
+ timeoutMs?: number;
12
+ }): Promise<CveFinding[]>;
@@ -0,0 +1,50 @@
1
+ import { readCache, readStaleCache, writeCache } from "./cache.js";
2
+ const OSV_URL = "https://api.osv.dev/v1/query";
3
+ function summarize(v) {
4
+ const fixed = v.affected?.[0]?.ranges?.[0]?.events?.find((e) => e.fixed)?.fixed;
5
+ const reference = v.references?.find((r) => r.type === "ADVISORY")?.url ??
6
+ v.references?.[0]?.url;
7
+ return {
8
+ id: v.id,
9
+ summary: (v.summary ?? v.details ?? "").slice(0, 200),
10
+ severity: v.database_specific?.severity,
11
+ fixed,
12
+ reference,
13
+ };
14
+ }
15
+ export async function queryOsv(lookup, version, opts = {}) {
16
+ if (!version)
17
+ return [];
18
+ const cacheKey = `${lookup.ecosystem ?? "any"}-${lookup.name}@${version}`;
19
+ if (!opts.noCache) {
20
+ const cached = readCache("osv", cacheKey);
21
+ if (cached)
22
+ return cached;
23
+ }
24
+ const body = {
25
+ package: lookup.ecosystem
26
+ ? { name: lookup.name, ecosystem: lookup.ecosystem }
27
+ : { name: lookup.name },
28
+ version,
29
+ };
30
+ try {
31
+ const res = await fetch(OSV_URL, {
32
+ method: "POST",
33
+ headers: { "Content-Type": "application/json" },
34
+ body: JSON.stringify(body),
35
+ signal: AbortSignal.timeout(opts.timeoutMs ?? 8000),
36
+ });
37
+ if (!res.ok) {
38
+ const stale = readStaleCache("osv", cacheKey);
39
+ return stale?.data ?? [];
40
+ }
41
+ const data = (await res.json());
42
+ const findings = (data.vulns ?? []).map(summarize);
43
+ writeCache("osv", cacheKey, findings);
44
+ return findings;
45
+ }
46
+ catch {
47
+ const stale = readStaleCache("osv", cacheKey);
48
+ return stale?.data ?? [];
49
+ }
50
+ }
@@ -0,0 +1 @@
1
+ export {};