@expressots/studio-agent 4.0.0-preview.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/agent.d.ts +127 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +1031 -0
- package/dist/agent.js.map +1 -0
- package/dist/discovery/index.d.ts +2 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +2 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/route-scanner.d.ts +35 -0
- package/dist/discovery/route-scanner.d.ts.map +1 -0
- package/dist/discovery/route-scanner.js +385 -0
- package/dist/discovery/route-scanner.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation/index.d.ts +2 -0
- package/dist/instrumentation/index.d.ts.map +1 -0
- package/dist/instrumentation/index.js +2 -0
- package/dist/instrumentation/index.js.map +1 -0
- package/dist/instrumentation/tracer.d.ts +40 -0
- package/dist/instrumentation/tracer.d.ts.map +1 -0
- package/dist/instrumentation/tracer.js +190 -0
- package/dist/instrumentation/tracer.js.map +1 -0
- package/dist/introspection/container-introspector.d.ts +81 -0
- package/dist/introspection/container-introspector.d.ts.map +1 -0
- package/dist/introspection/container-introspector.js +251 -0
- package/dist/introspection/container-introspector.js.map +1 -0
- package/dist/logging/log-capture.d.ts +58 -0
- package/dist/logging/log-capture.d.ts.map +1 -0
- package/dist/logging/log-capture.js +184 -0
- package/dist/logging/log-capture.js.map +1 -0
- package/dist/recording/index.d.ts +2 -0
- package/dist/recording/index.d.ts.map +1 -0
- package/dist/recording/index.js +2 -0
- package/dist/recording/index.js.map +1 -0
- package/dist/recording/request-recorder.d.ts +43 -0
- package/dist/recording/request-recorder.d.ts.map +1 -0
- package/dist/recording/request-recorder.js +373 -0
- package/dist/recording/request-recorder.js.map +1 -0
- package/dist/security/fix-resolver.d.ts +40 -0
- package/dist/security/fix-resolver.d.ts.map +1 -0
- package/dist/security/fix-resolver.js +283 -0
- package/dist/security/fix-resolver.js.map +1 -0
- package/dist/security/fix-runner.d.ts +60 -0
- package/dist/security/fix-runner.d.ts.map +1 -0
- package/dist/security/fix-runner.js +188 -0
- package/dist/security/fix-runner.js.map +1 -0
- package/dist/security/index.d.ts +140 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +460 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/lockfile-graph.d.ts +69 -0
- package/dist/security/lockfile-graph.d.ts.map +1 -0
- package/dist/security/lockfile-graph.js +245 -0
- package/dist/security/lockfile-graph.js.map +1 -0
- package/dist/security/npm-audit.d.ts +67 -0
- package/dist/security/npm-audit.d.ts.map +1 -0
- package/dist/security/npm-audit.js +320 -0
- package/dist/security/npm-audit.js.map +1 -0
- package/dist/security/osv-cache.d.ts +51 -0
- package/dist/security/osv-cache.d.ts.map +1 -0
- package/dist/security/osv-cache.js +99 -0
- package/dist/security/osv-cache.js.map +1 -0
- package/dist/security/osv-client.d.ts +47 -0
- package/dist/security/osv-client.d.ts.map +1 -0
- package/dist/security/osv-client.js +247 -0
- package/dist/security/osv-client.js.map +1 -0
- package/dist/security/posture-analyzer.d.ts +44 -0
- package/dist/security/posture-analyzer.d.ts.map +1 -0
- package/dist/security/posture-analyzer.js +397 -0
- package/dist/security/posture-analyzer.js.map +1 -0
- package/dist/security/reachability.d.ts +59 -0
- package/dist/security/reachability.d.ts.map +1 -0
- package/dist/security/reachability.js +302 -0
- package/dist/security/reachability.js.map +1 -0
- package/dist/security/score.d.ts +36 -0
- package/dist/security/score.d.ts.map +1 -0
- package/dist/security/score.js +94 -0
- package/dist/security/score.js.map +1 -0
- package/dist/types/index.d.ts +587 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `npm audit --json` runner.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the host project's `npm audit` in a child process, captures
|
|
5
|
+
* stdout, and normalises the result into `DependencyFinding[]` so the
|
|
6
|
+
* agent doesn't have to know about npm's wire format anywhere else.
|
|
7
|
+
*
|
|
8
|
+
* Design constraints:
|
|
9
|
+
* - **Async only.** This runs inside the host's process; blocking the
|
|
10
|
+
* event loop with `execSync` would freeze the host server.
|
|
11
|
+
* - **Bounded.** We cap stdout at 16 MB and impose a 30 s wall timeout
|
|
12
|
+
* to keep a misbehaving npm install from hanging Studio forever.
|
|
13
|
+
* - **Defensive.** `npm audit` writes useful JSON to stdout even when
|
|
14
|
+
* it exits 1 (the convention is "exit 1 means vulns were found").
|
|
15
|
+
* We treat exit codes 0 and 1 as success.
|
|
16
|
+
*
|
|
17
|
+
* The runner is decoupled from the engine: it returns a structured
|
|
18
|
+
* result plus a `state` value the engine maps onto `scanState.audit`.
|
|
19
|
+
*/
|
|
20
|
+
import { spawn } from 'node:child_process';
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
/** Maximum bytes of stdout we'll buffer from `npm audit`. */
|
|
24
|
+
const MAX_STDOUT_BYTES = 16 * 1024 * 1024; // 16 MB
|
|
25
|
+
/** How long we wait before killing the child process. */
|
|
26
|
+
const AUDIT_TIMEOUT_MS = 30_000;
|
|
27
|
+
/**
|
|
28
|
+
* Run `npm audit --json` in `cwd` and return findings. The function is
|
|
29
|
+
* resilient to npm not being installed, the project missing a
|
|
30
|
+
* lockfile, or audit returning malformed JSON — every error path
|
|
31
|
+
* yields a structured `NpmAuditResult` instead of throwing.
|
|
32
|
+
*/
|
|
33
|
+
export async function runNpmAudit(cwd) {
|
|
34
|
+
// Bail early if there's no lockfile — `npm audit` would just fail
|
|
35
|
+
// anyway, and we want to give the UI a clearer signal so it can
|
|
36
|
+
// render an empty state with instructions.
|
|
37
|
+
if (!hasLockfile(cwd)) {
|
|
38
|
+
return { state: 'missing-lockfile', findings: [], fixAvailability: new Map() };
|
|
39
|
+
}
|
|
40
|
+
let stdout = '';
|
|
41
|
+
let stderr = '';
|
|
42
|
+
try {
|
|
43
|
+
const result = await spawnAudit(cwd);
|
|
44
|
+
stdout = result.stdout;
|
|
45
|
+
stderr = result.stderr;
|
|
46
|
+
// npm audit exits 1 when it finds vulns, which is normal. Codes >=2
|
|
47
|
+
// signal a real failure (network, malformed package.json, etc.).
|
|
48
|
+
if (result.code !== null && result.code > 1) {
|
|
49
|
+
return {
|
|
50
|
+
state: 'error',
|
|
51
|
+
findings: [],
|
|
52
|
+
fixAvailability: new Map(),
|
|
53
|
+
error: extractShortError(stderr) ||
|
|
54
|
+
`npm audit exited with code ${result.code}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
state: 'error',
|
|
61
|
+
findings: [],
|
|
62
|
+
fixAvailability: new Map(),
|
|
63
|
+
error: err.message || 'failed to spawn npm audit',
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(stdout);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return {
|
|
72
|
+
state: 'error',
|
|
73
|
+
findings: [],
|
|
74
|
+
fixAvailability: new Map(),
|
|
75
|
+
error: 'npm audit produced invalid JSON',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
const findings = normaliseNpmAudit(parsed);
|
|
79
|
+
const fixAvailability = extractFixAvailability(parsed);
|
|
80
|
+
return { state: 'ok', findings, fixAvailability };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Walk the parsed audit report and produce a per-package `fixAvailable`
|
|
84
|
+
* map. Preserves npm's three-way semantics:
|
|
85
|
+
*
|
|
86
|
+
* - `false` → `{ kind: 'none' }` — no upstream fix exists.
|
|
87
|
+
* - `true` → `{ kind: 'auto' }` — `npm audit fix` can resolve it
|
|
88
|
+
* but no concrete pinned target.
|
|
89
|
+
* - object → `{ kind: 'specific', name, version, isSemVerMajor }`.
|
|
90
|
+
*/
|
|
91
|
+
function extractFixAvailability(report) {
|
|
92
|
+
const out = new Map();
|
|
93
|
+
const vulns = report.vulnerabilities ?? {};
|
|
94
|
+
for (const [pkgName, vuln] of Object.entries(vulns)) {
|
|
95
|
+
const fa = vuln.fixAvailable;
|
|
96
|
+
if (typeof fa === 'object' && fa !== null) {
|
|
97
|
+
out.set(pkgName, {
|
|
98
|
+
kind: 'specific',
|
|
99
|
+
name: fa.name,
|
|
100
|
+
version: fa.version,
|
|
101
|
+
isSemVerMajor: Boolean(fa.isSemVerMajor),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
else if (fa === true) {
|
|
105
|
+
out.set(pkgName, { kind: 'auto' });
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
out.set(pkgName, { kind: 'none' });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Translate the `npm audit --json` vulnerability map into the agent's
|
|
115
|
+
* canonical `DependencyFinding` shape.
|
|
116
|
+
*
|
|
117
|
+
* Two filtering rules de-noise the report:
|
|
118
|
+
*
|
|
119
|
+
* 1. We dedupe advisories by id within a package — a single CVE
|
|
120
|
+
* affecting multiple packages yields one finding per affected
|
|
121
|
+
* package, but a package isn't reported twice for the same CVE.
|
|
122
|
+
*
|
|
123
|
+
* 2. **Alias-only entries are skipped when an alias target has its
|
|
124
|
+
* own real advisory in the same report.** npm audit cascades
|
|
125
|
+
* "effects" up the dependency chain — every package that
|
|
126
|
+
* transitively depends on `path-to-regexp` shows up as a separate
|
|
127
|
+
* vulnerability entry with `via: ['path-to-regexp']`. Surfacing
|
|
128
|
+
* each link in that chain triples the finding count without
|
|
129
|
+
* adding signal; users only care about the package they actually
|
|
130
|
+
* need to upgrade (the real advisory's owner).
|
|
131
|
+
*/
|
|
132
|
+
function normaliseNpmAudit(report) {
|
|
133
|
+
const out = [];
|
|
134
|
+
const vulns = report.vulnerabilities ?? {};
|
|
135
|
+
// First pass: figure out which package names have *real* advisories
|
|
136
|
+
// (a `via` entry that's an object, not a string alias). Used below
|
|
137
|
+
// to drop alias-only cascades that point at one of these.
|
|
138
|
+
const packagesWithRealAdvisories = new Set();
|
|
139
|
+
for (const [pkgName, vuln] of Object.entries(vulns)) {
|
|
140
|
+
const hasReal = (vuln.via ?? []).some((v) => typeof v === 'object' && v !== null);
|
|
141
|
+
if (hasReal)
|
|
142
|
+
packagesWithRealAdvisories.add(pkgName);
|
|
143
|
+
}
|
|
144
|
+
for (const [pkgName, vuln] of Object.entries(vulns)) {
|
|
145
|
+
const advisories = (vuln.via ?? []).filter((v) => typeof v === 'object' && v !== null);
|
|
146
|
+
for (const adv of advisories) {
|
|
147
|
+
const id = buildFindingId(adv, pkgName);
|
|
148
|
+
out.push({
|
|
149
|
+
id,
|
|
150
|
+
package: pkgName,
|
|
151
|
+
installedVersion: vuln.range ?? adv.range ?? 'unknown',
|
|
152
|
+
fixedVersion: typeof vuln.fixAvailable === 'object' && vuln.fixAvailable
|
|
153
|
+
? vuln.fixAvailable.version
|
|
154
|
+
: undefined,
|
|
155
|
+
severity: parseSeverity(adv.severity ?? vuln.severity),
|
|
156
|
+
cvss: typeof adv.cvss?.score === 'number' && Number.isFinite(adv.cvss.score)
|
|
157
|
+
? adv.cvss.score
|
|
158
|
+
: undefined,
|
|
159
|
+
title: adv.title ?? `Vulnerability in ${pkgName}`,
|
|
160
|
+
summary: adv.title ?? '',
|
|
161
|
+
references: adv.url ? [adv.url] : [],
|
|
162
|
+
// npm audit reports `effects` as the chain of affected dependents;
|
|
163
|
+
// we surface it so users can see what brings the vulnerable
|
|
164
|
+
// package in. Most direct deps will have an empty effects array.
|
|
165
|
+
path: [pkgName, ...(vuln.effects ?? [])],
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
// Alias-only entries: surface as a finding ONLY if no alias target
|
|
169
|
+
// already has a real advisory in this report. Otherwise it's pure
|
|
170
|
+
// cascade noise — every transitive dependent of path-to-regexp
|
|
171
|
+
// shouldn't get its own row.
|
|
172
|
+
const aliasOnly = (vuln.via ?? []).filter((v) => typeof v === 'string');
|
|
173
|
+
if (advisories.length === 0 && aliasOnly.length > 0) {
|
|
174
|
+
const allAliasesCoveredByRealFindings = aliasOnly.every((a) => packagesWithRealAdvisories.has(a));
|
|
175
|
+
if (allAliasesCoveredByRealFindings)
|
|
176
|
+
continue;
|
|
177
|
+
out.push({
|
|
178
|
+
id: `npm-audit-${pkgName}-${vuln.severity ?? 'unknown'}`,
|
|
179
|
+
package: pkgName,
|
|
180
|
+
installedVersion: vuln.range ?? 'unknown',
|
|
181
|
+
fixedVersion: typeof vuln.fixAvailable === 'object' && vuln.fixAvailable
|
|
182
|
+
? vuln.fixAvailable.version
|
|
183
|
+
: undefined,
|
|
184
|
+
severity: parseSeverity(vuln.severity),
|
|
185
|
+
title: `Vulnerable transitive dependency via ${aliasOnly.join(', ')}`,
|
|
186
|
+
summary: '',
|
|
187
|
+
references: [],
|
|
188
|
+
path: [pkgName, ...aliasOnly],
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Build a stable advisory id. Prefer the GHSA / CVE embedded in the
|
|
196
|
+
* advisory URL — that's what we'll dedupe against OSV later. Falls
|
|
197
|
+
* back to a hash-equivalent synthetic id otherwise.
|
|
198
|
+
*/
|
|
199
|
+
function buildFindingId(adv, pkgName) {
|
|
200
|
+
const url = adv.url ?? '';
|
|
201
|
+
const ghsa = url.match(/(GHSA-[a-z0-9-]+)/i);
|
|
202
|
+
if (ghsa)
|
|
203
|
+
return ghsa[1].toUpperCase();
|
|
204
|
+
const cve = url.match(/(CVE-\d{4}-\d+)/i);
|
|
205
|
+
if (cve)
|
|
206
|
+
return cve[1].toUpperCase();
|
|
207
|
+
if (typeof adv.source === 'number') {
|
|
208
|
+
return `npm-${adv.source}-${pkgName}`;
|
|
209
|
+
}
|
|
210
|
+
// Last resort: deterministic synthetic id keyed by title+pkg so reruns
|
|
211
|
+
// produce the same id across scans.
|
|
212
|
+
return `npm-${pkgName}-${(adv.title ?? 'unknown').slice(0, 40).replace(/\s+/g, '-')}`;
|
|
213
|
+
}
|
|
214
|
+
function parseSeverity(input) {
|
|
215
|
+
switch ((input ?? '').toLowerCase()) {
|
|
216
|
+
case 'critical':
|
|
217
|
+
return 'CRITICAL';
|
|
218
|
+
case 'high':
|
|
219
|
+
return 'HIGH';
|
|
220
|
+
case 'moderate':
|
|
221
|
+
case 'medium':
|
|
222
|
+
return 'MEDIUM';
|
|
223
|
+
case 'low':
|
|
224
|
+
return 'LOW';
|
|
225
|
+
case 'info':
|
|
226
|
+
case 'informational':
|
|
227
|
+
return 'INFO';
|
|
228
|
+
default:
|
|
229
|
+
return 'LOW';
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/** Does the project at `cwd` have an npm lockfile? */
|
|
233
|
+
function hasLockfile(cwd) {
|
|
234
|
+
return (fs.existsSync(path.join(cwd, 'package-lock.json')) ||
|
|
235
|
+
fs.existsSync(path.join(cwd, 'npm-shrinkwrap.json')));
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Spawn `npm audit --json`, captures both streams, enforces the
|
|
239
|
+
* timeout, and resolves once the child exits (or we kill it).
|
|
240
|
+
*/
|
|
241
|
+
function spawnAudit(cwd) {
|
|
242
|
+
return new Promise((resolve, reject) => {
|
|
243
|
+
// On Windows the actual binary is `npm.cmd`. Spawn through the shell
|
|
244
|
+
// so platform differences don't matter. We don't pass any user input
|
|
245
|
+
// through the shell — only constant flags — so this is safe.
|
|
246
|
+
const child = spawn('npm', ['audit', '--json'], {
|
|
247
|
+
cwd,
|
|
248
|
+
shell: process.platform === 'win32',
|
|
249
|
+
// Inherit env so npm picks up the user's registry config / proxy.
|
|
250
|
+
env: process.env,
|
|
251
|
+
});
|
|
252
|
+
let stdout = '';
|
|
253
|
+
let stderr = '';
|
|
254
|
+
let bytes = 0;
|
|
255
|
+
child.stdout.on('data', (chunk) => {
|
|
256
|
+
bytes += chunk.length;
|
|
257
|
+
if (bytes > MAX_STDOUT_BYTES) {
|
|
258
|
+
// Truncate rather than ENOMEM. The first 16 MB of audit output
|
|
259
|
+
// is far more than any real project produces — if we hit this
|
|
260
|
+
// limit it's almost certainly a runaway.
|
|
261
|
+
if (stdout.length < MAX_STDOUT_BYTES) {
|
|
262
|
+
stdout += chunk.toString('utf-8');
|
|
263
|
+
stdout = stdout.slice(0, MAX_STDOUT_BYTES);
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
stdout += chunk.toString('utf-8');
|
|
268
|
+
});
|
|
269
|
+
child.stderr.on('data', (chunk) => {
|
|
270
|
+
// npm warning lines can be plentiful but are short; cap conservatively.
|
|
271
|
+
if (stderr.length < 32 * 1024) {
|
|
272
|
+
stderr += chunk.toString('utf-8');
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
let settled = false;
|
|
276
|
+
const settle = (result) => {
|
|
277
|
+
if (settled)
|
|
278
|
+
return;
|
|
279
|
+
settled = true;
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
resolve(result);
|
|
282
|
+
};
|
|
283
|
+
const timer = setTimeout(() => {
|
|
284
|
+
if (settled)
|
|
285
|
+
return;
|
|
286
|
+
try {
|
|
287
|
+
child.kill('SIGTERM');
|
|
288
|
+
}
|
|
289
|
+
catch {
|
|
290
|
+
// already gone
|
|
291
|
+
}
|
|
292
|
+
// Give it 250ms to actually die before resolving with what we have.
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
settle({ stdout, stderr, code: null });
|
|
295
|
+
}, 250);
|
|
296
|
+
}, AUDIT_TIMEOUT_MS);
|
|
297
|
+
child.on('error', (err) => {
|
|
298
|
+
if (settled)
|
|
299
|
+
return;
|
|
300
|
+
settled = true;
|
|
301
|
+
clearTimeout(timer);
|
|
302
|
+
reject(err);
|
|
303
|
+
});
|
|
304
|
+
child.on('close', (code) => {
|
|
305
|
+
settle({ stdout, stderr, code });
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Pull the first informative line out of `npm audit` stderr for display.
|
|
311
|
+
* npm prints a lot of `npm warn …` noise we don't want to surface.
|
|
312
|
+
*/
|
|
313
|
+
function extractShortError(stderr) {
|
|
314
|
+
const lines = stderr
|
|
315
|
+
.split('\n')
|
|
316
|
+
.map((l) => l.trim())
|
|
317
|
+
.filter((l) => l.length > 0 && !/^npm warn/i.test(l));
|
|
318
|
+
return lines[0] ?? '';
|
|
319
|
+
}
|
|
320
|
+
//# sourceMappingURL=npm-audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"npm-audit.js","sourceRoot":"","sources":["../../src/security/npm-audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk cache for OSV.dev advisory lookups.
|
|
3
|
+
*
|
|
4
|
+
* The OSV batch endpoint is generous but not infinite — repeating a
|
|
5
|
+
* scan in a project with 500+ deps every few minutes would burn quota
|
|
6
|
+
* fast. We cache results in `.studio/security-cache.json` keyed by
|
|
7
|
+
* `(ecosystem, package, version)` with a 6h TTL, so subsequent scans
|
|
8
|
+
* inside the TTL do zero network work for stable deps.
|
|
9
|
+
*
|
|
10
|
+
* The cache is best-effort: any I/O / parse error degrades to a
|
|
11
|
+
* cache-miss rather than failing the scan.
|
|
12
|
+
*/
|
|
13
|
+
export interface OsvAdvisory {
|
|
14
|
+
id: string;
|
|
15
|
+
aliases: string[];
|
|
16
|
+
summary: string;
|
|
17
|
+
details?: string;
|
|
18
|
+
severity?: number;
|
|
19
|
+
references: string[];
|
|
20
|
+
fixedVersion?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class OsvCache {
|
|
23
|
+
private readonly filePath;
|
|
24
|
+
private data;
|
|
25
|
+
private loaded;
|
|
26
|
+
private dirty;
|
|
27
|
+
/**
|
|
28
|
+
* Throttle disk writes — many cache puts inside one scan would
|
|
29
|
+
* otherwise produce many sequential writes. We coalesce into a
|
|
30
|
+
* single fsync at the end of a scan via `flush()`.
|
|
31
|
+
*/
|
|
32
|
+
constructor(dbPath: string);
|
|
33
|
+
/** Load cache from disk; safe to call repeatedly. */
|
|
34
|
+
load(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Look up advisories for a package@version. Returns null on miss /
|
|
37
|
+
* expired entry; callers should then fall back to a network query.
|
|
38
|
+
*/
|
|
39
|
+
get(ecosystem: string, name: string, version: string): OsvAdvisory[] | null;
|
|
40
|
+
/** Store advisories for a package@version. Flush() persists to disk. */
|
|
41
|
+
put(ecosystem: string, name: string, version: string, advisories: OsvAdvisory[]): void;
|
|
42
|
+
/**
|
|
43
|
+
* Persist any pending writes. Safe to call when there's nothing to
|
|
44
|
+
* flush. Failures are swallowed because the cache is an optimisation
|
|
45
|
+
* — losing it just means the next scan pays one more OSV roundtrip.
|
|
46
|
+
*/
|
|
47
|
+
flush(): void;
|
|
48
|
+
/** Drop everything. Exposed for tests / future "clear cache" UI. */
|
|
49
|
+
clear(): void;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=osv-cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"osv-cache.d.ts","sourceRoot":"","sources":["../../src/security/osv-cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAmBH,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAWD,qBAAa,QAAQ;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,IAAI,CAAsD;IAClE,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,KAAK,CAAS;IACtB;;;;OAIG;gBAES,MAAM,EAAE,MAAM;IAM1B,qDAAqD;IACrD,IAAI,IAAI,IAAI;IAeZ;;;OAGG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,GAAG,IAAI;IAQ3E,wEAAwE;IACxE,GAAG,CACD,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,WAAW,EAAE,GACxB,IAAI;IASP;;;;OAIG;IACH,KAAK,IAAI,IAAI;IAcb,oEAAoE;IACpE,KAAK,IAAI,IAAI;CAId"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-disk cache for OSV.dev advisory lookups.
|
|
3
|
+
*
|
|
4
|
+
* The OSV batch endpoint is generous but not infinite — repeating a
|
|
5
|
+
* scan in a project with 500+ deps every few minutes would burn quota
|
|
6
|
+
* fast. We cache results in `.studio/security-cache.json` keyed by
|
|
7
|
+
* `(ecosystem, package, version)` with a 6h TTL, so subsequent scans
|
|
8
|
+
* inside the TTL do zero network work for stable deps.
|
|
9
|
+
*
|
|
10
|
+
* The cache is best-effort: any I/O / parse error degrades to a
|
|
11
|
+
* cache-miss rather than failing the scan.
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
const TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
16
|
+
const CACHE_VERSION = 1;
|
|
17
|
+
function cacheKey(ecosystem, name, version) {
|
|
18
|
+
return `${ecosystem}|${name}|${version}`;
|
|
19
|
+
}
|
|
20
|
+
export class OsvCache {
|
|
21
|
+
filePath;
|
|
22
|
+
data = { version: CACHE_VERSION, entries: {} };
|
|
23
|
+
loaded = false;
|
|
24
|
+
dirty = false;
|
|
25
|
+
/**
|
|
26
|
+
* Throttle disk writes — many cache puts inside one scan would
|
|
27
|
+
* otherwise produce many sequential writes. We coalesce into a
|
|
28
|
+
* single fsync at the end of a scan via `flush()`.
|
|
29
|
+
*/
|
|
30
|
+
constructor(dbPath) {
|
|
31
|
+
// Co-locate with the studio db file so users only need to gitignore
|
|
32
|
+
// one folder (`.studio/`) to opt out of committing tooling state.
|
|
33
|
+
this.filePath = path.join(path.dirname(dbPath), 'security-cache.json');
|
|
34
|
+
}
|
|
35
|
+
/** Load cache from disk; safe to call repeatedly. */
|
|
36
|
+
load() {
|
|
37
|
+
if (this.loaded)
|
|
38
|
+
return;
|
|
39
|
+
this.loaded = true;
|
|
40
|
+
try {
|
|
41
|
+
const raw = fs.readFileSync(this.filePath, 'utf-8');
|
|
42
|
+
const parsed = JSON.parse(raw);
|
|
43
|
+
if (parsed?.version === CACHE_VERSION && parsed.entries) {
|
|
44
|
+
this.data = parsed;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Missing file or corrupt JSON — start fresh, don't crash the scan.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Look up advisories for a package@version. Returns null on miss /
|
|
53
|
+
* expired entry; callers should then fall back to a network query.
|
|
54
|
+
*/
|
|
55
|
+
get(ecosystem, name, version) {
|
|
56
|
+
this.load();
|
|
57
|
+
const entry = this.data.entries[cacheKey(ecosystem, name, version)];
|
|
58
|
+
if (!entry)
|
|
59
|
+
return null;
|
|
60
|
+
if (Date.now() - entry.storedAt > TTL_MS)
|
|
61
|
+
return null;
|
|
62
|
+
return entry.advisories;
|
|
63
|
+
}
|
|
64
|
+
/** Store advisories for a package@version. Flush() persists to disk. */
|
|
65
|
+
put(ecosystem, name, version, advisories) {
|
|
66
|
+
this.load();
|
|
67
|
+
this.data.entries[cacheKey(ecosystem, name, version)] = {
|
|
68
|
+
storedAt: Date.now(),
|
|
69
|
+
advisories,
|
|
70
|
+
};
|
|
71
|
+
this.dirty = true;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Persist any pending writes. Safe to call when there's nothing to
|
|
75
|
+
* flush. Failures are swallowed because the cache is an optimisation
|
|
76
|
+
* — losing it just means the next scan pays one more OSV roundtrip.
|
|
77
|
+
*/
|
|
78
|
+
flush() {
|
|
79
|
+
if (!this.dirty)
|
|
80
|
+
return;
|
|
81
|
+
try {
|
|
82
|
+
const dir = path.dirname(this.filePath);
|
|
83
|
+
if (!fs.existsSync(dir)) {
|
|
84
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
fs.writeFileSync(this.filePath, JSON.stringify(this.data));
|
|
87
|
+
this.dirty = false;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// best-effort
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Drop everything. Exposed for tests / future "clear cache" UI. */
|
|
94
|
+
clear() {
|
|
95
|
+
this.data = { version: CACHE_VERSION, entries: {} };
|
|
96
|
+
this.dirty = true;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=osv-cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"osv-cache.js","sourceRoot":"","sources":["../../src/security/osv-cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSV.dev client — supply-chain advisory lookups for installed packages.
|
|
3
|
+
*
|
|
4
|
+
* Why OSV: it's an open vulnerability database maintained by Google,
|
|
5
|
+
* exposes a programmatic JSON API, doesn't require auth, and aggregates
|
|
6
|
+
* GHSA, npm, OSS-Fuzz and other sources behind one query shape. It's
|
|
7
|
+
* also the source `npm audit` increasingly delegates to internally, so
|
|
8
|
+
* we get the same data without scraping anything.
|
|
9
|
+
*
|
|
10
|
+
* This client batches per-scan: one POST to `/v1/querybatch` covering
|
|
11
|
+
* every direct dependency. Results that aren't already in cache are
|
|
12
|
+
* persisted via `OsvCache` so subsequent scans inside the TTL skip the
|
|
13
|
+
* network entirely.
|
|
14
|
+
*
|
|
15
|
+
* Network failures degrade gracefully: we return an empty findings
|
|
16
|
+
* array so the engine can still ship a report using only `npm audit`.
|
|
17
|
+
*/
|
|
18
|
+
import type { DependencyFinding } from '../types/index.js';
|
|
19
|
+
import { OsvCache } from './osv-cache.js';
|
|
20
|
+
export interface PackageQuery {
|
|
21
|
+
name: string;
|
|
22
|
+
version: string;
|
|
23
|
+
}
|
|
24
|
+
export declare class OsvClient {
|
|
25
|
+
private readonly cache;
|
|
26
|
+
constructor(cache: OsvCache);
|
|
27
|
+
/**
|
|
28
|
+
* Look up advisories for every (package, version) pair and return
|
|
29
|
+
* normalised findings. Cache hits are served without touching the
|
|
30
|
+
* network; the rest are batched into one (or more, if very large)
|
|
31
|
+
* POSTs to OSV.
|
|
32
|
+
*/
|
|
33
|
+
lookup(packages: PackageQuery[]): Promise<DependencyFinding[]>;
|
|
34
|
+
/**
|
|
35
|
+
* POST a batch of queries to OSV's `/v1/querybatch`. Splits oversize
|
|
36
|
+
* input into multiple requests to stay under the per-call limit.
|
|
37
|
+
*/
|
|
38
|
+
private fetchAdvisoryIds;
|
|
39
|
+
/**
|
|
40
|
+
* Hydrate advisory id list into full records. OSV requires one GET
|
|
41
|
+
* per id (no batch endpoint for full records yet) — we run them in
|
|
42
|
+
* parallel with a hard concurrency cap so we don't open hundreds of
|
|
43
|
+
* sockets at once.
|
|
44
|
+
*/
|
|
45
|
+
private fetchFullAdvisories;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=osv-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"osv-client.d.ts","sourceRoot":"","sources":["../../src/security/osv-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAY,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,QAAQ,EAAoB,MAAM,gBAAgB,CAAC;AAQ5D,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAyBD,qBAAa,SAAS;IACR,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,QAAQ;IAE5C;;;;;OAKG;IACG,MAAM,CAAC,QAAQ,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAqDpE;;;OAGG;YACW,gBAAgB;IAsC9B;;;;;OAKG;YACW,mBAAmB;CA2BlC"}
|