trawly 0.0.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/LICENSE +17 -0
- package/README.md +155 -0
- package/SECURITY.md +18 -0
- package/dist/cli.js +1316 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +94 -0
- package/dist/index.js +393 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
// src/scanner.ts
|
|
2
|
+
import { existsSync, statSync } from "fs";
|
|
3
|
+
import { resolve as resolve2, join } from "path";
|
|
4
|
+
|
|
5
|
+
// src/extractors/npm-package-lock.ts
|
|
6
|
+
import { readFileSync } from "fs";
|
|
7
|
+
import { resolve } from "path";
|
|
8
|
+
function parseNpmPackageLock(filePath) {
|
|
9
|
+
const absolute = resolve(filePath);
|
|
10
|
+
const raw = readFileSync(absolute, "utf8");
|
|
11
|
+
let parsed;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(raw);
|
|
14
|
+
} catch (err) {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`Failed to parse ${absolute}: ${err.message}`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
if (parsed.lockfileVersion !== 2 && parsed.lockfileVersion !== 3) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Unsupported npm lockfileVersion ${String(
|
|
22
|
+
parsed.lockfileVersion
|
|
23
|
+
)} in ${absolute}. Only v2 and v3 are supported.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
const packages = parsed.packages;
|
|
27
|
+
if (!packages || typeof packages !== "object") {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`Lockfile ${absolute} has no "packages" map; cannot extract installed versions.`
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
const directDeps = collectDirectDependencyNames(packages[""] ?? {});
|
|
33
|
+
const instances = [];
|
|
34
|
+
for (const [path, entry] of Object.entries(packages)) {
|
|
35
|
+
if (path === "") continue;
|
|
36
|
+
if (entry.link) continue;
|
|
37
|
+
const name = packagePathToName(path);
|
|
38
|
+
if (!name) continue;
|
|
39
|
+
if (!entry.version) continue;
|
|
40
|
+
instances.push({
|
|
41
|
+
name,
|
|
42
|
+
version: entry.version,
|
|
43
|
+
ecosystem: "npm",
|
|
44
|
+
path,
|
|
45
|
+
direct: directDeps.has(name) && isTopLevelInstance(path),
|
|
46
|
+
dev: Boolean(entry.dev || entry.devOptional),
|
|
47
|
+
optional: Boolean(entry.optional || entry.devOptional),
|
|
48
|
+
resolved: entry.resolved,
|
|
49
|
+
integrity: entry.integrity
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return instances;
|
|
53
|
+
}
|
|
54
|
+
function collectDirectDependencyNames(rootEntry) {
|
|
55
|
+
const names = /* @__PURE__ */ new Set();
|
|
56
|
+
for (const key of [
|
|
57
|
+
"dependencies",
|
|
58
|
+
"devDependencies",
|
|
59
|
+
"optionalDependencies",
|
|
60
|
+
"peerDependencies"
|
|
61
|
+
]) {
|
|
62
|
+
const block = rootEntry[key];
|
|
63
|
+
if (!block) continue;
|
|
64
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
65
|
+
}
|
|
66
|
+
return names;
|
|
67
|
+
}
|
|
68
|
+
function packagePathToName(path) {
|
|
69
|
+
const marker = "node_modules/";
|
|
70
|
+
const idx = path.lastIndexOf(marker);
|
|
71
|
+
if (idx === -1) return null;
|
|
72
|
+
const tail = path.slice(idx + marker.length);
|
|
73
|
+
if (!tail) return null;
|
|
74
|
+
if (tail.startsWith("@")) {
|
|
75
|
+
const firstSlash = tail.indexOf("/");
|
|
76
|
+
if (firstSlash === -1) return null;
|
|
77
|
+
const secondSlash = tail.indexOf("/", firstSlash + 1);
|
|
78
|
+
return secondSlash === -1 ? tail : tail.slice(0, secondSlash);
|
|
79
|
+
}
|
|
80
|
+
const next = tail.indexOf("/");
|
|
81
|
+
return next === -1 ? tail : tail.slice(0, next);
|
|
82
|
+
}
|
|
83
|
+
function isTopLevelInstance(path) {
|
|
84
|
+
const first = path.indexOf("node_modules/");
|
|
85
|
+
if (first === -1) return false;
|
|
86
|
+
return path.indexOf("node_modules/", first + 1) === -1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/sources/osv.ts
|
|
90
|
+
var OSV_QUERYBATCH_URL = "https://api.osv.dev/v1/querybatch";
|
|
91
|
+
var OSV_VULN_URL = "https://api.osv.dev/v1/vulns";
|
|
92
|
+
var QUERY_CHUNK_SIZE = 500;
|
|
93
|
+
var REQUEST_TIMEOUT_MS = 15e3;
|
|
94
|
+
var MAX_RETRIES = 2;
|
|
95
|
+
function dedupeForQuery(packages) {
|
|
96
|
+
const seen = /* @__PURE__ */ new Set();
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const pkg of packages) {
|
|
99
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
100
|
+
if (seen.has(key)) continue;
|
|
101
|
+
seen.add(key);
|
|
102
|
+
out.push({ name: pkg.name, version: pkg.version });
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
async function queryOsv(packages, deps = {}) {
|
|
107
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
108
|
+
const unique = dedupeForQuery(packages);
|
|
109
|
+
if (unique.length === 0) return [];
|
|
110
|
+
const idsByPackage = /* @__PURE__ */ new Map();
|
|
111
|
+
for (const chunk of chunked(unique, QUERY_CHUNK_SIZE)) {
|
|
112
|
+
const body = {
|
|
113
|
+
queries: chunk.map((q) => ({
|
|
114
|
+
package: { ecosystem: "npm", name: q.name },
|
|
115
|
+
version: q.version
|
|
116
|
+
}))
|
|
117
|
+
};
|
|
118
|
+
const res = await postJson(
|
|
119
|
+
fetchImpl,
|
|
120
|
+
OSV_QUERYBATCH_URL,
|
|
121
|
+
body
|
|
122
|
+
);
|
|
123
|
+
res.results.forEach((result, i) => {
|
|
124
|
+
const q = chunk[i];
|
|
125
|
+
if (!q) return;
|
|
126
|
+
const key = `${q.name}@${q.version}`;
|
|
127
|
+
if (!result.vulns || result.vulns.length === 0) return;
|
|
128
|
+
const ids = idsByPackage.get(key) ?? /* @__PURE__ */ new Set();
|
|
129
|
+
for (const v of result.vulns) ids.add(v.id);
|
|
130
|
+
idsByPackage.set(key, ids);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const ids of idsByPackage.values()) {
|
|
135
|
+
for (const id of ids) allIds.add(id);
|
|
136
|
+
}
|
|
137
|
+
const detailsById = /* @__PURE__ */ new Map();
|
|
138
|
+
for (const id of allIds) {
|
|
139
|
+
try {
|
|
140
|
+
const detail = await getJson(
|
|
141
|
+
fetchImpl,
|
|
142
|
+
`${OSV_VULN_URL}/${encodeURIComponent(id)}`
|
|
143
|
+
);
|
|
144
|
+
detailsById.set(id, detail);
|
|
145
|
+
} catch {
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const findings = [];
|
|
149
|
+
for (const pkg of packages) {
|
|
150
|
+
const key = `${pkg.name}@${pkg.version}`;
|
|
151
|
+
const ids = idsByPackage.get(key);
|
|
152
|
+
if (!ids) continue;
|
|
153
|
+
for (const id of ids) {
|
|
154
|
+
const detail = detailsById.get(id);
|
|
155
|
+
findings.push(buildFinding(pkg, id, detail));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return findings;
|
|
159
|
+
}
|
|
160
|
+
function buildFinding(pkg, id, detail) {
|
|
161
|
+
const severity = detail ? parseSeverity(detail) : "unknown";
|
|
162
|
+
const summary = detail?.summary ?? detail?.details ?? id;
|
|
163
|
+
return {
|
|
164
|
+
id,
|
|
165
|
+
source: "osv",
|
|
166
|
+
type: "vulnerability",
|
|
167
|
+
severity,
|
|
168
|
+
packageName: pkg.name,
|
|
169
|
+
installedVersion: pkg.version,
|
|
170
|
+
summary: truncate(summary, 240),
|
|
171
|
+
url: pickAdvisoryUrl(detail) ?? `https://osv.dev/vulnerability/${id}`,
|
|
172
|
+
fixedVersions: detail ? collectFixedVersions(detail, pkg.name) : [],
|
|
173
|
+
affectedPaths: [pkg.path]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
function parseSeverity(detail) {
|
|
177
|
+
const dbSpecific = detail.database_specific?.severity?.toLowerCase();
|
|
178
|
+
if (dbSpecific === "critical" || dbSpecific === "high" || dbSpecific === "moderate" || dbSpecific === "low") {
|
|
179
|
+
return dbSpecific;
|
|
180
|
+
}
|
|
181
|
+
if (dbSpecific === "medium") return "moderate";
|
|
182
|
+
const cvss = detail.severity?.find((s) => s.type?.startsWith("CVSS_"));
|
|
183
|
+
if (cvss) {
|
|
184
|
+
const score = parseCvssScore(cvss.score);
|
|
185
|
+
if (score === void 0) return "unknown";
|
|
186
|
+
if (score >= 9) return "critical";
|
|
187
|
+
if (score >= 7) return "high";
|
|
188
|
+
if (score >= 4) return "moderate";
|
|
189
|
+
if (score > 0) return "low";
|
|
190
|
+
}
|
|
191
|
+
return "unknown";
|
|
192
|
+
}
|
|
193
|
+
function parseCvssScore(vector) {
|
|
194
|
+
const direct = Number.parseFloat(vector);
|
|
195
|
+
if (!Number.isNaN(direct) && vector.trim() !== "") return direct;
|
|
196
|
+
return void 0;
|
|
197
|
+
}
|
|
198
|
+
function pickAdvisoryUrl(detail) {
|
|
199
|
+
if (!detail?.references) return void 0;
|
|
200
|
+
const advisory = detail.references.find((r) => r.type === "ADVISORY");
|
|
201
|
+
return advisory?.url ?? detail.references[0]?.url;
|
|
202
|
+
}
|
|
203
|
+
function collectFixedVersions(detail, packageName) {
|
|
204
|
+
const out = /* @__PURE__ */ new Set();
|
|
205
|
+
for (const aff of detail.affected ?? []) {
|
|
206
|
+
if (aff.package?.name && aff.package.name !== packageName) continue;
|
|
207
|
+
for (const range of aff.ranges ?? []) {
|
|
208
|
+
for (const event of range.events ?? []) {
|
|
209
|
+
if (event.fixed) out.add(event.fixed);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return [...out];
|
|
214
|
+
}
|
|
215
|
+
function* chunked(items, size) {
|
|
216
|
+
for (let i = 0; i < items.length; i += size) {
|
|
217
|
+
yield items.slice(i, i + size);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function postJson(fetchImpl, url, body) {
|
|
221
|
+
return withRetry(async () => {
|
|
222
|
+
const controller = new AbortController();
|
|
223
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
224
|
+
try {
|
|
225
|
+
const res = await fetchImpl(url, {
|
|
226
|
+
method: "POST",
|
|
227
|
+
headers: { "content-type": "application/json" },
|
|
228
|
+
body: JSON.stringify(body),
|
|
229
|
+
signal: controller.signal
|
|
230
|
+
});
|
|
231
|
+
if (!res.ok) {
|
|
232
|
+
throw new HttpError(`OSV ${res.status}: ${res.statusText}`, res.status);
|
|
233
|
+
}
|
|
234
|
+
return await res.json();
|
|
235
|
+
} finally {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async function getJson(fetchImpl, url) {
|
|
241
|
+
return withRetry(async () => {
|
|
242
|
+
const controller = new AbortController();
|
|
243
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetchImpl(url, { signal: controller.signal });
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
throw new HttpError(`OSV ${res.status}: ${res.statusText}`, res.status);
|
|
248
|
+
}
|
|
249
|
+
return await res.json();
|
|
250
|
+
} finally {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
var HttpError = class extends Error {
|
|
256
|
+
constructor(message, status) {
|
|
257
|
+
super(message);
|
|
258
|
+
this.status = status;
|
|
259
|
+
}
|
|
260
|
+
status;
|
|
261
|
+
};
|
|
262
|
+
async function withRetry(fn) {
|
|
263
|
+
let lastErr;
|
|
264
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
265
|
+
try {
|
|
266
|
+
return await fn();
|
|
267
|
+
} catch (err) {
|
|
268
|
+
lastErr = err;
|
|
269
|
+
if (!isRetryable(err) || attempt === MAX_RETRIES) break;
|
|
270
|
+
await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
throw lastErr;
|
|
274
|
+
}
|
|
275
|
+
function isRetryable(err) {
|
|
276
|
+
if (err instanceof HttpError) return err.status >= 500;
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
279
|
+
function truncate(s, max) {
|
|
280
|
+
if (s.length <= max) return s;
|
|
281
|
+
return `${s.slice(0, max - 1)}\u2026`;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// src/types.ts
|
|
285
|
+
var SEVERITY_RANK = {
|
|
286
|
+
critical: 4,
|
|
287
|
+
high: 3,
|
|
288
|
+
moderate: 2,
|
|
289
|
+
low: 1,
|
|
290
|
+
unknown: 0
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/scanner.ts
|
|
294
|
+
async function scanProject(options = {}) {
|
|
295
|
+
const cwd = resolve2(options.cwd ?? process.cwd());
|
|
296
|
+
const lockfilePath = options.lockfile ? resolve2(cwd, options.lockfile) : detectLockfile(cwd);
|
|
297
|
+
if (!lockfilePath) {
|
|
298
|
+
throw new ScanInputError(
|
|
299
|
+
`No npm lockfile found in ${cwd}. Pass --lockfile or run in a directory with package-lock.json.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return scanLockfile({
|
|
303
|
+
lockfilePath,
|
|
304
|
+
includeDev: options.includeDev,
|
|
305
|
+
prodOnly: options.prodOnly,
|
|
306
|
+
fetchImpl: options.fetchImpl
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
async function scanLockfile(options) {
|
|
310
|
+
const { lockfilePath } = options;
|
|
311
|
+
if (!existsSync(lockfilePath)) {
|
|
312
|
+
throw new ScanInputError(`Lockfile does not exist: ${lockfilePath}`);
|
|
313
|
+
}
|
|
314
|
+
const stat = statSync(lockfilePath);
|
|
315
|
+
if (!stat.isFile()) {
|
|
316
|
+
throw new ScanInputError(`Lockfile path is not a file: ${lockfilePath}`);
|
|
317
|
+
}
|
|
318
|
+
const allInstances = parseNpmPackageLock(lockfilePath);
|
|
319
|
+
const instances = filterInstances(allInstances, options);
|
|
320
|
+
const errors = [];
|
|
321
|
+
let findings = [];
|
|
322
|
+
try {
|
|
323
|
+
findings = await queryOsv(instances, { fetchImpl: options.fetchImpl });
|
|
324
|
+
} catch (err) {
|
|
325
|
+
errors.push({
|
|
326
|
+
message: "Failed to query OSV advisory database",
|
|
327
|
+
cause: err.message
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
findings.sort(compareFindings);
|
|
331
|
+
return {
|
|
332
|
+
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
333
|
+
packagesScanned: instances.length,
|
|
334
|
+
findings,
|
|
335
|
+
summary: summarize(findings),
|
|
336
|
+
errors
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
var ScanInputError = class extends Error {
|
|
340
|
+
constructor(message) {
|
|
341
|
+
super(message);
|
|
342
|
+
this.name = "ScanInputError";
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
function detectLockfile(cwd) {
|
|
346
|
+
const candidate = join(cwd, "package-lock.json");
|
|
347
|
+
return existsSync(candidate) ? candidate : void 0;
|
|
348
|
+
}
|
|
349
|
+
function filterInstances(instances, options) {
|
|
350
|
+
const includeDev = options.prodOnly ? false : options.includeDev !== false;
|
|
351
|
+
if (includeDev) return instances;
|
|
352
|
+
return instances.filter((p) => !p.dev);
|
|
353
|
+
}
|
|
354
|
+
function compareFindings(a, b) {
|
|
355
|
+
const sev = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];
|
|
356
|
+
if (sev !== 0) return sev;
|
|
357
|
+
if (a.packageName !== b.packageName) {
|
|
358
|
+
return a.packageName.localeCompare(b.packageName);
|
|
359
|
+
}
|
|
360
|
+
if (a.installedVersion !== b.installedVersion) {
|
|
361
|
+
return a.installedVersion.localeCompare(b.installedVersion);
|
|
362
|
+
}
|
|
363
|
+
return a.id.localeCompare(b.id);
|
|
364
|
+
}
|
|
365
|
+
function summarize(findings) {
|
|
366
|
+
const summary = {
|
|
367
|
+
critical: 0,
|
|
368
|
+
high: 0,
|
|
369
|
+
moderate: 0,
|
|
370
|
+
low: 0,
|
|
371
|
+
unknown: 0
|
|
372
|
+
};
|
|
373
|
+
for (const f of findings) summary[f.severity]++;
|
|
374
|
+
return summary;
|
|
375
|
+
}
|
|
376
|
+
function meetsThreshold(findings, threshold) {
|
|
377
|
+
if (threshold === "none") return false;
|
|
378
|
+
const min = SEVERITY_RANK[threshold];
|
|
379
|
+
return findings.some((f) => SEVERITY_RANK[f.severity] >= min);
|
|
380
|
+
}
|
|
381
|
+
export {
|
|
382
|
+
SEVERITY_RANK,
|
|
383
|
+
ScanInputError,
|
|
384
|
+
compareFindings,
|
|
385
|
+
dedupeForQuery,
|
|
386
|
+
meetsThreshold,
|
|
387
|
+
parseNpmPackageLock,
|
|
388
|
+
queryOsv,
|
|
389
|
+
scanLockfile,
|
|
390
|
+
scanProject,
|
|
391
|
+
summarize
|
|
392
|
+
};
|
|
393
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scanner.ts","../src/extractors/npm-package-lock.ts","../src/sources/osv.ts","../src/types.ts"],"sourcesContent":["import { existsSync, statSync } from \"node:fs\";\nimport { resolve, join } from \"node:path\";\nimport { parseNpmPackageLock } from \"./extractors/npm-package-lock.js\";\nimport { queryOsv } from \"./sources/osv.js\";\nimport { SEVERITY_RANK } from \"./types.js\";\nimport type {\n Finding,\n PackageInstance,\n ScanError,\n ScanLockfileOptions,\n ScanProjectOptions,\n ScanResult,\n Severity,\n} from \"./types.js\";\n\nexport async function scanProject(\n options: ScanProjectOptions = {},\n): Promise<ScanResult> {\n const cwd = resolve(options.cwd ?? process.cwd());\n const lockfilePath = options.lockfile\n ? resolve(cwd, options.lockfile)\n : detectLockfile(cwd);\n\n if (!lockfilePath) {\n throw new ScanInputError(\n `No npm lockfile found in ${cwd}. Pass --lockfile or run in a directory with package-lock.json.`,\n );\n }\n\n return scanLockfile({\n lockfilePath,\n includeDev: options.includeDev,\n prodOnly: options.prodOnly,\n fetchImpl: options.fetchImpl,\n });\n}\n\nexport async function scanLockfile(\n options: ScanLockfileOptions,\n): Promise<ScanResult> {\n const { lockfilePath } = options;\n if (!existsSync(lockfilePath)) {\n throw new ScanInputError(`Lockfile does not exist: ${lockfilePath}`);\n }\n const stat = statSync(lockfilePath);\n if (!stat.isFile()) {\n throw new ScanInputError(`Lockfile path is not a file: ${lockfilePath}`);\n }\n\n const allInstances = parseNpmPackageLock(lockfilePath);\n const instances = filterInstances(allInstances, options);\n const errors: ScanError[] = [];\n\n let findings: Finding[] = [];\n try {\n findings = await queryOsv(instances, { fetchImpl: options.fetchImpl });\n } catch (err) {\n errors.push({\n message: \"Failed to query OSV advisory database\",\n cause: (err as Error).message,\n });\n }\n\n findings.sort(compareFindings);\n\n return {\n scannedAt: new Date().toISOString(),\n packagesScanned: instances.length,\n findings,\n summary: summarize(findings),\n errors,\n };\n}\n\nexport class ScanInputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"ScanInputError\";\n }\n}\n\nfunction detectLockfile(cwd: string): string | undefined {\n const candidate = join(cwd, \"package-lock.json\");\n return existsSync(candidate) ? candidate : undefined;\n}\n\nfunction filterInstances(\n instances: PackageInstance[],\n options: { includeDev?: boolean; prodOnly?: boolean },\n): PackageInstance[] {\n const includeDev = options.prodOnly ? false : options.includeDev !== false;\n if (includeDev) return instances;\n return instances.filter((p) => !p.dev);\n}\n\nexport function compareFindings(a: Finding, b: Finding): number {\n const sev = SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity];\n if (sev !== 0) return sev;\n if (a.packageName !== b.packageName) {\n return a.packageName.localeCompare(b.packageName);\n }\n if (a.installedVersion !== b.installedVersion) {\n return a.installedVersion.localeCompare(b.installedVersion);\n }\n return a.id.localeCompare(b.id);\n}\n\nexport function summarize(findings: Finding[]): Record<Severity, number> {\n const summary: Record<Severity, number> = {\n critical: 0,\n high: 0,\n moderate: 0,\n low: 0,\n unknown: 0,\n };\n for (const f of findings) summary[f.severity]++;\n return summary;\n}\n\n/**\n * Returns true when any finding meets or exceeds the given severity threshold.\n * \"none\" means never fail.\n */\nexport function meetsThreshold(\n findings: Finding[],\n threshold: Severity | \"none\",\n): boolean {\n if (threshold === \"none\") return false;\n const min = SEVERITY_RANK[threshold];\n return findings.some((f) => SEVERITY_RANK[f.severity] >= min);\n}\n","import { readFileSync } from \"node:fs\";\nimport { resolve } from \"node:path\";\nimport type { PackageInstance } from \"../types.js\";\n\ninterface NpmLockEntry {\n version?: string;\n resolved?: string;\n integrity?: string;\n dev?: boolean;\n devOptional?: boolean;\n optional?: boolean;\n peer?: boolean;\n link?: boolean;\n // Present on the root (\"\") entry only.\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n optionalDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\ninterface NpmLockfile {\n name?: string;\n lockfileVersion?: number;\n packages?: Record<string, NpmLockEntry>;\n}\n\n/**\n * Parse an npm `package-lock.json` (v2 or v3) and return one\n * PackageInstance per node in the `packages` map.\n *\n * The empty-string key represents the root project and is skipped.\n */\nexport function parseNpmPackageLock(filePath: string): PackageInstance[] {\n const absolute = resolve(filePath);\n const raw = readFileSync(absolute, \"utf8\");\n let parsed: NpmLockfile;\n try {\n parsed = JSON.parse(raw) as NpmLockfile;\n } catch (err) {\n throw new Error(\n `Failed to parse ${absolute}: ${(err as Error).message}`,\n );\n }\n\n if (parsed.lockfileVersion !== 2 && parsed.lockfileVersion !== 3) {\n throw new Error(\n `Unsupported npm lockfileVersion ${String(\n parsed.lockfileVersion,\n )} in ${absolute}. Only v2 and v3 are supported.`,\n );\n }\n\n const packages = parsed.packages;\n if (!packages || typeof packages !== \"object\") {\n throw new Error(\n `Lockfile ${absolute} has no \"packages\" map; cannot extract installed versions.`,\n );\n }\n\n const directDeps = collectDirectDependencyNames(packages[\"\"] ?? {});\n const instances: PackageInstance[] = [];\n\n for (const [path, entry] of Object.entries(packages)) {\n if (path === \"\") continue;\n if (entry.link) continue; // workspace symlink, not a real install\n const name = packagePathToName(path);\n if (!name) continue;\n if (!entry.version) continue;\n\n instances.push({\n name,\n version: entry.version,\n ecosystem: \"npm\",\n path,\n direct: directDeps.has(name) && isTopLevelInstance(path),\n dev: Boolean(entry.dev || entry.devOptional),\n optional: Boolean(entry.optional || entry.devOptional),\n resolved: entry.resolved,\n integrity: entry.integrity,\n });\n }\n\n return instances;\n}\n\nfunction collectDirectDependencyNames(rootEntry: NpmLockEntry): Set<string> {\n const names = new Set<string>();\n for (const key of [\n \"dependencies\",\n \"devDependencies\",\n \"optionalDependencies\",\n \"peerDependencies\",\n ] as const) {\n const block = rootEntry[key];\n if (!block) continue;\n for (const name of Object.keys(block)) names.add(name);\n }\n return names;\n}\n\n/**\n * \"node_modules/foo\" -> \"foo\"\n * \"node_modules/@scope/bar\" -> \"@scope/bar\"\n * \"node_modules/foo/node_modules/bar\" -> \"bar\"\n */\nexport function packagePathToName(path: string): string | null {\n const marker = \"node_modules/\";\n const idx = path.lastIndexOf(marker);\n if (idx === -1) return null;\n const tail = path.slice(idx + marker.length);\n if (!tail) return null;\n if (tail.startsWith(\"@\")) {\n const firstSlash = tail.indexOf(\"/\");\n if (firstSlash === -1) return null;\n const secondSlash = tail.indexOf(\"/\", firstSlash + 1);\n return secondSlash === -1 ? tail : tail.slice(0, secondSlash);\n }\n const next = tail.indexOf(\"/\");\n return next === -1 ? tail : tail.slice(0, next);\n}\n\n/** A direct-install path has exactly one `node_modules/` segment. */\nfunction isTopLevelInstance(path: string): boolean {\n const first = path.indexOf(\"node_modules/\");\n if (first === -1) return false;\n return path.indexOf(\"node_modules/\", first + 1) === -1;\n}\n","import type { Finding, PackageInstance, Severity } from \"../types.js\";\n\nconst OSV_QUERYBATCH_URL = \"https://api.osv.dev/v1/querybatch\";\nconst OSV_VULN_URL = \"https://api.osv.dev/v1/vulns\";\nconst QUERY_CHUNK_SIZE = 500;\nconst REQUEST_TIMEOUT_MS = 15_000;\nconst MAX_RETRIES = 2;\n\ninterface OsvQueryBatchResponse {\n results: Array<{ vulns?: Array<{ id: string; modified?: string }> }>;\n}\n\ninterface OsvSeverity {\n type: string;\n score: string;\n}\n\ninterface OsvAffectedRange {\n type: string;\n events: Array<{ introduced?: string; fixed?: string; last_affected?: string }>;\n}\n\ninterface OsvAffectedPackage {\n package?: { ecosystem?: string; name?: string };\n ranges?: OsvAffectedRange[];\n versions?: string[];\n}\n\ninterface OsvVulnDetail {\n id: string;\n summary?: string;\n details?: string;\n references?: Array<{ type?: string; url?: string }>;\n severity?: OsvSeverity[];\n database_specific?: { severity?: string };\n affected?: OsvAffectedPackage[];\n}\n\nexport interface OsvQueryDeps {\n fetchImpl?: typeof fetch;\n}\n\ninterface UniquePackage {\n name: string;\n version: string;\n}\n\n/**\n * Build the deduplicated list of unique name@version pairs to query OSV with.\n */\nexport function dedupeForQuery(\n packages: PackageInstance[],\n): UniquePackage[] {\n const seen = new Set<string>();\n const out: UniquePackage[] = [];\n for (const pkg of packages) {\n const key = `${pkg.name}@${pkg.version}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push({ name: pkg.name, version: pkg.version });\n }\n return out;\n}\n\n/**\n * Query OSV for the given installed packages and return one Finding per\n * (advisory, affected package instance) pair.\n */\nexport async function queryOsv(\n packages: PackageInstance[],\n deps: OsvQueryDeps = {},\n): Promise<Finding[]> {\n const fetchImpl = deps.fetchImpl ?? fetch;\n const unique = dedupeForQuery(packages);\n if (unique.length === 0) return [];\n\n const idsByPackage = new Map<string, Set<string>>();\n for (const chunk of chunked(unique, QUERY_CHUNK_SIZE)) {\n const body = {\n queries: chunk.map((q) => ({\n package: { ecosystem: \"npm\", name: q.name },\n version: q.version,\n })),\n };\n const res = await postJson<OsvQueryBatchResponse>(\n fetchImpl,\n OSV_QUERYBATCH_URL,\n body,\n );\n res.results.forEach((result, i) => {\n const q = chunk[i];\n if (!q) return;\n const key = `${q.name}@${q.version}`;\n if (!result.vulns || result.vulns.length === 0) return;\n const ids = idsByPackage.get(key) ?? new Set<string>();\n for (const v of result.vulns) ids.add(v.id);\n idsByPackage.set(key, ids);\n });\n }\n\n const allIds = new Set<string>();\n for (const ids of idsByPackage.values()) {\n for (const id of ids) allIds.add(id);\n }\n\n const detailsById = new Map<string, OsvVulnDetail>();\n for (const id of allIds) {\n try {\n const detail = await getJson<OsvVulnDetail>(\n fetchImpl,\n `${OSV_VULN_URL}/${encodeURIComponent(id)}`,\n );\n detailsById.set(id, detail);\n } catch {\n // Skip missing/broken records; we still have the id reported below.\n }\n }\n\n const findings: Finding[] = [];\n for (const pkg of packages) {\n const key = `${pkg.name}@${pkg.version}`;\n const ids = idsByPackage.get(key);\n if (!ids) continue;\n for (const id of ids) {\n const detail = detailsById.get(id);\n findings.push(buildFinding(pkg, id, detail));\n }\n }\n return findings;\n}\n\nfunction buildFinding(\n pkg: PackageInstance,\n id: string,\n detail: OsvVulnDetail | undefined,\n): Finding {\n const severity = detail ? parseSeverity(detail) : \"unknown\";\n const summary = detail?.summary ?? detail?.details ?? id;\n return {\n id,\n source: \"osv\",\n type: \"vulnerability\",\n severity,\n packageName: pkg.name,\n installedVersion: pkg.version,\n summary: truncate(summary, 240),\n url: pickAdvisoryUrl(detail) ?? `https://osv.dev/vulnerability/${id}`,\n fixedVersions: detail ? collectFixedVersions(detail, pkg.name) : [],\n affectedPaths: [pkg.path],\n };\n}\n\nexport function parseSeverity(detail: OsvVulnDetail): Severity {\n // GHSA records expose a normalized severity in database_specific.severity.\n const dbSpecific = detail.database_specific?.severity?.toLowerCase();\n if (\n dbSpecific === \"critical\" ||\n dbSpecific === \"high\" ||\n dbSpecific === \"moderate\" ||\n dbSpecific === \"low\"\n ) {\n return dbSpecific;\n }\n if (dbSpecific === \"medium\") return \"moderate\";\n\n const cvss = detail.severity?.find((s) => s.type?.startsWith(\"CVSS_\"));\n if (cvss) {\n const score = parseCvssScore(cvss.score);\n if (score === undefined) return \"unknown\";\n if (score >= 9.0) return \"critical\";\n if (score >= 7.0) return \"high\";\n if (score >= 4.0) return \"moderate\";\n if (score > 0) return \"low\";\n }\n return \"unknown\";\n}\n\nfunction parseCvssScore(vector: string): number | undefined {\n const direct = Number.parseFloat(vector);\n if (!Number.isNaN(direct) && vector.trim() !== \"\") return direct;\n // Some entries store the full CVSS vector string; we don't compute it here.\n return undefined;\n}\n\nfunction pickAdvisoryUrl(detail: OsvVulnDetail | undefined): string | undefined {\n if (!detail?.references) return undefined;\n const advisory = detail.references.find((r) => r.type === \"ADVISORY\");\n return advisory?.url ?? detail.references[0]?.url;\n}\n\nexport function collectFixedVersions(\n detail: OsvVulnDetail,\n packageName: string,\n): string[] {\n const out = new Set<string>();\n for (const aff of detail.affected ?? []) {\n if (aff.package?.name && aff.package.name !== packageName) continue;\n for (const range of aff.ranges ?? []) {\n for (const event of range.events ?? []) {\n if (event.fixed) out.add(event.fixed);\n }\n }\n }\n return [...out];\n}\n\nfunction* chunked<T>(items: T[], size: number): Generator<T[]> {\n for (let i = 0; i < items.length; i += size) {\n yield items.slice(i, i + size);\n }\n}\n\nasync function postJson<T>(\n fetchImpl: typeof fetch,\n url: string,\n body: unknown,\n): Promise<T> {\n return withRetry(async () => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n try {\n const res = await fetchImpl(url, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(body),\n signal: controller.signal,\n });\n if (!res.ok) {\n throw new HttpError(`OSV ${res.status}: ${res.statusText}`, res.status);\n }\n return (await res.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\nasync function getJson<T>(fetchImpl: typeof fetch, url: string): Promise<T> {\n return withRetry(async () => {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n try {\n const res = await fetchImpl(url, { signal: controller.signal });\n if (!res.ok) {\n throw new HttpError(`OSV ${res.status}: ${res.statusText}`, res.status);\n }\n return (await res.json()) as T;\n } finally {\n clearTimeout(timer);\n }\n });\n}\n\nclass HttpError extends Error {\n constructor(message: string, public readonly status: number) {\n super(message);\n }\n}\n\nasync function withRetry<T>(fn: () => Promise<T>): Promise<T> {\n let lastErr: unknown;\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastErr = err;\n if (!isRetryable(err) || attempt === MAX_RETRIES) break;\n await new Promise((r) => setTimeout(r, 250 * 2 ** attempt));\n }\n }\n throw lastErr;\n}\n\nfunction isRetryable(err: unknown): boolean {\n if (err instanceof HttpError) return err.status >= 500;\n // AbortError (timeout) and network errors are retryable.\n return true;\n}\n\nfunction truncate(s: string, max: number): string {\n if (s.length <= max) return s;\n return `${s.slice(0, max - 1)}…`;\n}\n","export type Severity = \"critical\" | \"high\" | \"moderate\" | \"low\" | \"unknown\";\n\nexport type Ecosystem = \"npm\";\n\nexport type FindingType =\n | \"vulnerability\"\n | \"malware\"\n | \"risk-signal\"\n | \"integrity\";\n\nexport const SEVERITY_RANK: Record<Severity, number> = {\n critical: 4,\n high: 3,\n moderate: 2,\n low: 1,\n unknown: 0,\n};\n\nexport interface PackageInstance {\n name: string;\n version: string;\n ecosystem: Ecosystem;\n /** Path within the lockfile's `packages` map (e.g. \"node_modules/foo\"). */\n path: string;\n direct: boolean;\n dev: boolean;\n optional: boolean;\n resolved?: string;\n integrity?: string;\n}\n\nexport interface Finding {\n id: string;\n source: \"osv\";\n type: FindingType;\n severity: Severity;\n packageName: string;\n installedVersion: string;\n summary: string;\n url?: string;\n fixedVersions: string[];\n affectedPaths: string[];\n}\n\nexport interface ScanError {\n message: string;\n cause?: string;\n}\n\nexport interface ScanResult {\n scannedAt: string;\n packagesScanned: number;\n findings: Finding[];\n summary: Record<Severity, number>;\n errors: ScanError[];\n}\n\nexport interface ScanProjectOptions {\n cwd?: string;\n lockfile?: string;\n includeDev?: boolean;\n prodOnly?: boolean;\n cache?: boolean;\n fetchImpl?: typeof fetch;\n}\n\nexport interface ScanLockfileOptions {\n lockfilePath: string;\n includeDev?: boolean;\n prodOnly?: boolean;\n fetchImpl?: typeof fetch;\n}\n\nexport type FailOnLevel = Severity | \"none\";\n"],"mappings":"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","names":["resolve","resolve"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trawly",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Dependency sanity scanner for npm projects. Scans installed package versions against OSV and reports known vulnerabilities.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"trawly": "./dist/cli.js"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE",
|
|
25
|
+
"SECURITY.md"
|
|
26
|
+
],
|
|
27
|
+
"scripts": {
|
|
28
|
+
"build": "tsup",
|
|
29
|
+
"dev": "tsup --watch",
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"scan": "node --import tsx/esm src/cli.ts scan",
|
|
34
|
+
"prepublishOnly": "npm run build"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"security",
|
|
38
|
+
"vulnerability",
|
|
39
|
+
"scanner",
|
|
40
|
+
"npm",
|
|
41
|
+
"osv",
|
|
42
|
+
"supply-chain",
|
|
43
|
+
"dependency",
|
|
44
|
+
"audit"
|
|
45
|
+
],
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"commander": "^12.1.0",
|
|
48
|
+
"kleur": "^4.1.5",
|
|
49
|
+
"zod": "^3.23.8"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^20.14.0",
|
|
53
|
+
"tsup": "^8.3.0",
|
|
54
|
+
"tsx": "^4.19.0",
|
|
55
|
+
"typescript": "^5.6.0",
|
|
56
|
+
"vitest": "^2.1.0"
|
|
57
|
+
}
|
|
58
|
+
}
|