Skip to content

Commit 4512009

Browse files
authored
Merge pull request #48 from sonukapoor/codex/bug-osv-cache-query-results
fix: cache OSV package query results
2 parents 618db89 + e1943b8 commit 4512009

4 files changed

Lines changed: 58 additions & 16 deletions

File tree

src/osv/cache.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import path from "node:path";
33
import os from "node:os";
44
import type { CacheFile } from "../types.js";
55

6+
function createEmptyCache(): CacheFile {
7+
return { version: 2, createdAt: new Date().toISOString(), entries: {}, queryEntries: {} };
8+
}
9+
610
export function getCacheFilePath(cacheDirOverride?: string): string {
711
const baseDir = cacheDirOverride
812
? path.resolve(cacheDirOverride)
@@ -15,19 +19,30 @@ export function getCacheFilePath(cacheDirOverride?: string): string {
1519
export function loadCache(cacheDirOverride?: string): CacheFile {
1620
const filePath = getCacheFilePath(cacheDirOverride);
1721
if (!fs.existsSync(filePath)) {
18-
return { version: 1, createdAt: new Date().toISOString(), entries: {} };
22+
return createEmptyCache();
1923
}
2024

2125
try {
22-
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as CacheFile;
23-
if (parsed.version === 1 && parsed.entries) {
24-
return parsed;
26+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as Partial<CacheFile> & {
27+
version?: number;
28+
entries?: Record<string, CacheFile["entries"][string]>;
29+
queryEntries?: Record<string, string[]>;
30+
createdAt?: string;
31+
};
32+
33+
if (parsed.entries && typeof parsed.entries === "object") {
34+
return {
35+
version: 2,
36+
createdAt: parsed.createdAt ?? new Date().toISOString(),
37+
entries: parsed.entries,
38+
queryEntries: parsed.queryEntries && typeof parsed.queryEntries === "object" ? parsed.queryEntries : {},
39+
};
2540
}
2641
} catch (_error) {
2742
// ignore corrupt cache
2843
}
2944

30-
return { version: 1, createdAt: new Date().toISOString(), entries: {} };
45+
return createEmptyCache();
3146
}
3247

3348
export function saveCache(cache: CacheFile, cacheDirOverride?: string) {

src/output/formatters.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,12 +116,13 @@ export function printCacheSummary(cacheDirOverride?: string, options?: { json?:
116116
const cache = loadCache(cacheDirOverride);
117117
const advisoryCount = Object.entries(cache.entries).filter(([, value]) => Boolean(value)).length;
118118
const emptyCount = Object.entries(cache.entries).filter(([, value]) => value === null).length;
119+
const packageQueryCount = Object.keys(cache.queryEntries).length;
119120
const totalCount = advisoryCount + emptyCount;
120121

121-
if (totalCount === 0) return;
122+
if (totalCount === 0 && packageQueryCount === 0) return;
122123

123124
console.log(
124-
chalk.gray(`Cache: ${advisoryCount} advisory detail record${advisoryCount === 1 ? "" : "s"}`) +
125+
chalk.gray(`Cache: ${packageQueryCount} package match record${packageQueryCount === 1 ? "" : "s"}, ${advisoryCount} advisory detail record${advisoryCount === 1 ? "" : "s"}`) +
125126
(emptyCount > 0
126127
? chalk.gray(`, ${emptyCount} empty lookup${emptyCount === 1 ? "" : "s"}`)
127128
: ""),
@@ -144,4 +145,4 @@ export function sortFindingsForOutput(findings: Finding[]): Finding[] {
144145
if (sevDelta !== 0) return sevDelta;
145146
return a.pkg.name.localeCompare(b.pkg.name);
146147
});
147-
}
148+
}

src/scanner.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ export function createAdvisorySource(options?: { osvUrl?: string }): AdvisorySou
1212
return new OsvAdvisorySource(options?.osvUrl);
1313
}
1414

15+
function getPackageCacheKey(pkg: PackageRef): string {
16+
return `${pkg.ecosystem}:${pkg.name}@${pkg.version}`;
17+
}
18+
1519
export async function scanPackages(
1620
packages: PackageRef[],
1721
batchSize: number,
@@ -22,12 +26,27 @@ export async function scanPackages(
2226

2327
const spinner = createSpinner("Scanning dependencies against OSV...", options);
2428
const advisorySource = createAdvisorySource({ osvUrl: options.osvUrl });
29+
const cache = loadCache(cacheDirOverride);
2530

2631
try {
27-
const chunks = chunk(packages, batchSize);
2832
const results: Array<{ pkg: PackageRef; vulnIds: string[] }> = [];
33+
const uncachedPackages: PackageRef[] = [];
2934

3035
if (!offline) {
36+
for (const pkg of packages) {
37+
const cacheKey = getPackageCacheKey(pkg);
38+
if (cacheKey in cache.queryEntries) {
39+
const vulnIds = cache.queryEntries[cacheKey] ?? [];
40+
if (vulnIds.length > 0) {
41+
results.push({ pkg, vulnIds });
42+
}
43+
continue;
44+
}
45+
46+
uncachedPackages.push(pkg);
47+
}
48+
49+
const chunks = chunk(uncachedPackages, batchSize);
3150
for (let i = 0; i < chunks.length; i++) {
3251
spinner.update(`Scanning OSV batch ${i + 1}/${chunks.length}...`);
3352
const chunkItems = chunks[i];
@@ -38,18 +57,22 @@ export async function scanPackages(
3857
const pkg = chunkItems[j];
3958
const row = rows[j];
4059
const vulnIds = (row?.vulnerabilities ?? []).map(v => v.id).filter(Boolean);
60+
cache.queryEntries[getPackageCacheKey(pkg)] = vulnIds;
4161
if (vulnIds.length > 0) {
4262
results.push({ pkg, vulnIds });
4363
}
4464
}
4565
}
4666

47-
spinner.succeed(`Queried OSV in ${chunks.length} batch${chunks.length === 1 ? "" : "es"}`);
67+
if (chunks.length === 0) {
68+
spinner.succeed("Loaded package matches from cache");
69+
} else {
70+
spinner.succeed(`Queried OSV in ${chunks.length} batch${chunks.length === 1 ? "" : "es"}`);
71+
}
4872
} else {
4973
spinner.succeed("Offline mode enabled; package matching was skipped");
5074
}
5175

52-
const cache = loadCache(cacheDirOverride);
5376
const idSet = new Set(results.flatMap(r => r.vulnIds));
5477
const vulnMap = new Map<string, OsvVuln>();
5578

@@ -60,9 +83,11 @@ export async function scanPackages(
6083
for (let i = 0; i < ids.length; i++) {
6184
const id = ids[i];
6285
detailSpinner.update(`Fetching vulnerability details ${i + 1}/${ids.length}...`);
63-
const cached = cache.entries[id];
64-
if (cached) {
65-
vulnMap.set(id, cached);
86+
if (id in cache.entries) {
87+
const cached = cache.entries[id];
88+
if (cached) {
89+
vulnMap.set(id, cached);
90+
}
6691
continue;
6792
}
6893

src/types.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,10 @@ export type Finding = {
7272
};
7373

7474
export type CacheFile = {
75-
version: 1;
75+
version: 2;
7676
createdAt: string;
7777
entries: Record<string, OsvVuln | null>;
78+
queryEntries: Record<string, string[]>;
7879
};
7980

8081
export type Spinner = {
@@ -97,4 +98,4 @@ export type ParsedOptions = {
9798
minSeverity?: string;
9899
help?: boolean;
99100
osvUrl?: string;
100-
};
101+
};

0 commit comments

Comments
 (0)