@lockhawk/core 0.2.5 → 0.2.7
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 +7 -7
- package/dist/index.d.ts +9 -11
- package/dist/index.js +48 -2474
- package/package.json +14 -5
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,2490 +1,64 @@
|
|
|
1
|
-
// src/types.ts
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
function pkgKey(name, version) {
|
|
14
|
-
return `${name}@${version}`;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// src/lockfiles/normalize.ts
|
|
18
|
-
import { resolve } from "path";
|
|
19
|
-
|
|
20
|
-
// src/lockfiles/detect.ts
|
|
21
|
-
import { existsSync, readFileSync, statSync } from "fs";
|
|
22
|
-
import { join } from "path";
|
|
23
|
-
var LOCKFILES = [
|
|
24
|
-
{ filename: "pnpm-lock.yaml", manager: "pnpm" },
|
|
25
|
-
{ filename: "yarn.lock", manager: "yarn" },
|
|
26
|
-
{ filename: "package-lock.json", manager: "npm" },
|
|
27
|
-
{ filename: "npm-shrinkwrap.json", manager: "npm" }
|
|
28
|
-
];
|
|
29
|
-
function detectLockfile(dir) {
|
|
30
|
-
for (const { filename, manager } of LOCKFILES) {
|
|
31
|
-
const path = join(dir, filename);
|
|
32
|
-
if (existsSync(path) && statSync(path).isFile()) {
|
|
33
|
-
return { manager, path, filename };
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return void 0;
|
|
37
|
-
}
|
|
38
|
-
function readRootManifest(dir) {
|
|
39
|
-
const path = join(dir, "package.json");
|
|
40
|
-
let raw = {};
|
|
41
|
-
if (existsSync(path)) {
|
|
42
|
-
try {
|
|
43
|
-
raw = JSON.parse(readFileSync(path, "utf8"));
|
|
44
|
-
} catch {
|
|
45
|
-
raw = {};
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const asMap = (v) => v && typeof v === "object" ? v : {};
|
|
49
|
-
return {
|
|
50
|
-
name: typeof raw.name === "string" ? raw.name : "project",
|
|
51
|
-
version: typeof raw.version === "string" ? raw.version : void 0,
|
|
52
|
-
dependencies: asMap(raw.dependencies),
|
|
53
|
-
devDependencies: asMap(raw.devDependencies),
|
|
54
|
-
optionalDependencies: asMap(raw.optionalDependencies),
|
|
55
|
-
peerDependencies: asMap(raw.peerDependencies)
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/lockfiles/raw.ts
|
|
60
|
-
function reachable(start, nodes) {
|
|
61
|
-
const seen = /* @__PURE__ */ new Set();
|
|
62
|
-
const queue = [];
|
|
63
|
-
for (const k of start) {
|
|
64
|
-
if (!seen.has(k)) {
|
|
65
|
-
seen.add(k);
|
|
66
|
-
queue.push(k);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
for (let i = 0; i < queue.length; i++) {
|
|
70
|
-
const node = nodes[queue[i]];
|
|
71
|
-
if (!node) continue;
|
|
72
|
-
for (const dep of node.dependencies) {
|
|
73
|
-
if (!seen.has(dep)) {
|
|
74
|
-
seen.add(dep);
|
|
75
|
-
queue.push(dep);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return seen;
|
|
80
|
-
}
|
|
81
|
-
function buildDependencyGraph(raw) {
|
|
82
|
-
const prodReach = reachable(raw.directProd, raw.nodes);
|
|
83
|
-
const optionalReach = reachable(raw.directOptional, raw.nodes);
|
|
84
|
-
const devReach = reachable(raw.directDev, raw.nodes);
|
|
85
|
-
const direct = /* @__PURE__ */ new Set([...raw.directProd, ...raw.directOptional, ...raw.directDev]);
|
|
86
|
-
const nodes = {};
|
|
87
|
-
for (const [key, rn] of Object.entries(raw.nodes)) {
|
|
88
|
-
let scope;
|
|
89
|
-
if (prodReach.has(key)) scope = "prod";
|
|
90
|
-
else if (optionalReach.has(key)) scope = "optional";
|
|
91
|
-
else if (devReach.has(key)) scope = "dev";
|
|
92
|
-
else scope = "prod";
|
|
93
|
-
nodes[key] = {
|
|
94
|
-
key,
|
|
95
|
-
name: rn.name,
|
|
96
|
-
version: rn.version,
|
|
97
|
-
scope,
|
|
98
|
-
direct: direct.has(key),
|
|
99
|
-
dependencies: dedupe(rn.dependencies),
|
|
100
|
-
...rn.unscannable ? { unscannable: rn.unscannable } : {}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
return {
|
|
104
|
-
manager: raw.manager,
|
|
105
|
-
lockfilePath: raw.lockfilePath,
|
|
106
|
-
lockfileVersion: raw.lockfileVersion,
|
|
107
|
-
root: raw.root,
|
|
108
|
-
nodes,
|
|
109
|
-
directKeys: [...direct].filter((k) => nodes[k] !== void 0)
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
function dedupe(keys) {
|
|
113
|
-
return keys.length > 1 ? [...new Set(keys)] : keys;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// src/lockfiles/npm.ts
|
|
117
|
-
import { readFileSync as readFileSync2 } from "fs";
|
|
118
|
-
import { dirname } from "path";
|
|
119
|
-
function parseNpm(lockfilePath) {
|
|
120
|
-
const data = JSON.parse(readFileSync2(lockfilePath, "utf8"));
|
|
121
|
-
if (data.packages) {
|
|
122
|
-
return parsePackages(data, lockfilePath);
|
|
123
|
-
}
|
|
124
|
-
return parseLegacy(data, lockfilePath);
|
|
125
|
-
}
|
|
126
|
-
function nameFromPath(p) {
|
|
127
|
-
const marker = "node_modules/";
|
|
128
|
-
const idx = p.lastIndexOf(marker);
|
|
129
|
-
return idx === -1 ? p : p.slice(idx + marker.length);
|
|
130
|
-
}
|
|
131
|
-
function parsePackages(data, lockfilePath) {
|
|
132
|
-
const packages = data.packages ?? {};
|
|
133
|
-
const rootEntry = packages[""] ?? {};
|
|
134
|
-
const root = {
|
|
135
|
-
name: rootEntry.name ?? data.name ?? "project",
|
|
136
|
-
version: rootEntry.version ?? data.version
|
|
137
|
-
};
|
|
138
|
-
const nodes = /* @__PURE__ */ Object.create(null);
|
|
139
|
-
const unscannable = [];
|
|
140
|
-
const pathToKey = /* @__PURE__ */ Object.create(null);
|
|
141
|
-
for (const [p, entry] of Object.entries(packages)) {
|
|
142
|
-
if (p === "" || entry.link) continue;
|
|
143
|
-
const name = entry.name ?? nameFromPath(p);
|
|
144
|
-
const version = entry.version;
|
|
145
|
-
if (!version) continue;
|
|
146
|
-
const key = pkgKey(name, version);
|
|
147
|
-
pathToKey[p] = key;
|
|
148
|
-
if (!nodes[key]) {
|
|
149
|
-
const node = { key, name, version, dependencies: [] };
|
|
150
|
-
if (!p.includes("node_modules")) {
|
|
151
|
-
node.unscannable = { reason: "local workspace package" };
|
|
152
|
-
unscannable.push({ name, version, reason: "local workspace package" });
|
|
153
|
-
}
|
|
154
|
-
nodes[key] = node;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
const resolveDep = (fromPath, depName) => {
|
|
158
|
-
let prefix = fromPath;
|
|
159
|
-
for (; ; ) {
|
|
160
|
-
const candidate = `${prefix ? `${prefix}/` : ""}node_modules/${depName}`;
|
|
161
|
-
const entry = packages[candidate];
|
|
162
|
-
if (entry) {
|
|
163
|
-
if (entry.link && typeof entry.resolved === "string") return entry.resolved;
|
|
164
|
-
return candidate;
|
|
165
|
-
}
|
|
166
|
-
if (!prefix) return void 0;
|
|
167
|
-
const i = prefix.lastIndexOf("/node_modules/");
|
|
168
|
-
prefix = i === -1 ? "" : prefix.slice(0, i);
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
const directProd = /* @__PURE__ */ new Set();
|
|
172
|
-
const directDev = /* @__PURE__ */ new Set();
|
|
173
|
-
const directOptional = /* @__PURE__ */ new Set();
|
|
174
|
-
for (const [p, entry] of Object.entries(packages)) {
|
|
175
|
-
if (entry.link) continue;
|
|
176
|
-
const fromKey = p === "" ? void 0 : pathToKey[p];
|
|
177
|
-
const isLocalRoot = p === "" || !p.includes("node_modules");
|
|
178
|
-
const addEdges = (names, bucket) => {
|
|
179
|
-
for (const depName of names) {
|
|
180
|
-
const targetPath = resolveDep(p, depName);
|
|
181
|
-
if (!targetPath) continue;
|
|
182
|
-
const targetKey = pathToKey[targetPath];
|
|
183
|
-
if (!targetKey) continue;
|
|
184
|
-
if (fromKey && fromKey !== targetKey) nodes[fromKey].dependencies.push(targetKey);
|
|
185
|
-
if (bucket) bucket.add(targetKey);
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
|
-
addEdges(Object.keys(entry.dependencies ?? {}), isLocalRoot ? directProd : void 0);
|
|
189
|
-
addEdges(
|
|
190
|
-
Object.keys(entry.optionalDependencies ?? {}),
|
|
191
|
-
isLocalRoot ? directOptional : void 0
|
|
192
|
-
);
|
|
193
|
-
if (isLocalRoot) {
|
|
194
|
-
addEdges(Object.keys(entry.devDependencies ?? {}), directDev);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
manager: "npm",
|
|
199
|
-
lockfilePath,
|
|
200
|
-
lockfileVersion: data.lockfileVersion,
|
|
201
|
-
root,
|
|
202
|
-
nodes,
|
|
203
|
-
directProd: [...directProd],
|
|
204
|
-
directDev: [...directDev],
|
|
205
|
-
directOptional: [...directOptional],
|
|
206
|
-
unscannable
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
function parseLegacy(data, lockfilePath) {
|
|
210
|
-
const manifest = readRootManifest(dirname(lockfilePath));
|
|
211
|
-
const root = { name: data.name ?? manifest.name, version: data.version ?? manifest.version };
|
|
212
|
-
const nodes = /* @__PURE__ */ Object.create(null);
|
|
213
|
-
const buildLevel = (deps) => {
|
|
214
|
-
const m2 = /* @__PURE__ */ new Map();
|
|
215
|
-
for (const [name, info] of Object.entries(deps ?? {})) {
|
|
216
|
-
if (info?.version) m2.set(name, info.version);
|
|
217
|
-
}
|
|
218
|
-
return m2;
|
|
219
|
-
};
|
|
220
|
-
const resolveFromChain = (name, chain) => {
|
|
221
|
-
for (let i = chain.length - 1; i >= 0; i--) {
|
|
222
|
-
const v = chain[i].get(name);
|
|
223
|
-
if (v) return v;
|
|
224
|
-
}
|
|
225
|
-
return void 0;
|
|
226
|
-
};
|
|
227
|
-
const visit = (deps, ancestors) => {
|
|
228
|
-
const level = buildLevel(deps);
|
|
229
|
-
const chain = [...ancestors, level];
|
|
230
|
-
for (const [name, info] of Object.entries(deps ?? {})) {
|
|
231
|
-
const version = info?.version;
|
|
232
|
-
if (!version) continue;
|
|
233
|
-
const key = pkgKey(name, version);
|
|
234
|
-
if (!nodes[key]) nodes[key] = { key, name, version, dependencies: [] };
|
|
235
|
-
const ownChain = [...chain, buildLevel(info.dependencies)];
|
|
236
|
-
for (const reqName of Object.keys(info.requires ?? {})) {
|
|
237
|
-
const v = resolveFromChain(reqName, ownChain);
|
|
238
|
-
if (v) {
|
|
239
|
-
const targetKey = pkgKey(reqName, v);
|
|
240
|
-
if (targetKey !== key) nodes[key].dependencies.push(targetKey);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
if (info.dependencies) visit(info.dependencies, chain);
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
visit(data.dependencies, []);
|
|
247
|
-
const topLevel = buildLevel(data.dependencies);
|
|
248
|
-
const directProd = [];
|
|
249
|
-
const directDev = [];
|
|
250
|
-
const directOptional = [];
|
|
251
|
-
const classify = (names, out) => {
|
|
252
|
-
for (const name of names) {
|
|
253
|
-
const v = topLevel.get(name);
|
|
254
|
-
if (v) out.push(pkgKey(name, v));
|
|
255
|
-
}
|
|
256
|
-
};
|
|
257
|
-
classify(Object.keys(manifest.dependencies), directProd);
|
|
258
|
-
classify(Object.keys(manifest.optionalDependencies), directOptional);
|
|
259
|
-
classify(Object.keys(manifest.devDependencies), directDev);
|
|
260
|
-
return {
|
|
261
|
-
manager: "npm",
|
|
262
|
-
lockfilePath,
|
|
263
|
-
lockfileVersion: data.lockfileVersion,
|
|
264
|
-
root,
|
|
265
|
-
nodes,
|
|
266
|
-
directProd,
|
|
267
|
-
directDev,
|
|
268
|
-
directOptional,
|
|
269
|
-
unscannable: []
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// src/lockfiles/yarn.ts
|
|
274
|
-
import { readFileSync as readFileSync3 } from "fs";
|
|
275
|
-
import { dirname as dirname2 } from "path";
|
|
276
|
-
import { createRequire } from "module";
|
|
277
|
-
import yaml from "js-yaml";
|
|
278
|
-
var require2 = createRequire(import.meta.url);
|
|
279
|
-
var yarnLockfile = require2("@yarnpkg/lockfile");
|
|
280
|
-
function parseYarn(lockfilePath) {
|
|
281
|
-
const content = readFileSync3(lockfilePath, "utf8");
|
|
282
|
-
const manifest = readRootManifest(dirname2(lockfilePath));
|
|
283
|
-
const isBerry = content.includes("__metadata");
|
|
284
|
-
const { descriptorToVersion, entryByVersion, lockfileVersion } = isBerry ? parseBerry(content) : parseClassic(content);
|
|
285
|
-
return assemble(
|
|
286
|
-
lockfilePath,
|
|
287
|
-
manifest,
|
|
288
|
-
descriptorToVersion,
|
|
289
|
-
entryByVersion,
|
|
290
|
-
lockfileVersion,
|
|
291
|
-
isBerry
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
function splitDescriptor(descriptor) {
|
|
295
|
-
const at = descriptor.lastIndexOf("@");
|
|
296
|
-
if (at <= 0) return void 0;
|
|
297
|
-
return { name: descriptor.slice(0, at), range: descriptor.slice(at + 1) };
|
|
298
|
-
}
|
|
299
|
-
function parseClassic(content) {
|
|
300
|
-
const parsed = yarnLockfile.parse(content);
|
|
301
|
-
const descriptorToVersion = /* @__PURE__ */ new Map();
|
|
302
|
-
const entryByVersion = /* @__PURE__ */ new Map();
|
|
303
|
-
for (const [rawKey, entry] of Object.entries(parsed.object)) {
|
|
304
|
-
if (!entry.version) continue;
|
|
305
|
-
for (const descriptor of rawKey.split(/,\s*/)) {
|
|
306
|
-
const split = splitDescriptor(descriptor);
|
|
307
|
-
if (split) descriptorToVersion.set(`${split.name}@${split.range}`, entry.version);
|
|
308
|
-
if (split) entryByVersion.set(pkgKey(split.name, entry.version), entry);
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
return { descriptorToVersion, entryByVersion, lockfileVersion: 1 };
|
|
312
|
-
}
|
|
313
|
-
function parseBerry(content) {
|
|
314
|
-
const doc = yaml.load(content) ?? {};
|
|
315
|
-
const descriptorToVersion = /* @__PURE__ */ new Map();
|
|
316
|
-
const entryByVersion = /* @__PURE__ */ new Map();
|
|
317
|
-
let lockfileVersion = "berry";
|
|
318
|
-
for (const [rawKey, entry] of Object.entries(doc)) {
|
|
319
|
-
if (rawKey === "__metadata") {
|
|
320
|
-
const v = entry.version;
|
|
321
|
-
if (v !== void 0) lockfileVersion = v;
|
|
322
|
-
continue;
|
|
323
|
-
}
|
|
324
|
-
if (!entry.version) continue;
|
|
325
|
-
for (const descriptor of rawKey.split(/,\s*/)) {
|
|
326
|
-
const split = splitDescriptor(descriptor);
|
|
327
|
-
if (!split) continue;
|
|
328
|
-
descriptorToVersion.set(`${split.name}@${split.range}`, entry.version);
|
|
329
|
-
entryByVersion.set(pkgKey(split.name, entry.version), entry);
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
return { descriptorToVersion, entryByVersion, lockfileVersion };
|
|
333
|
-
}
|
|
334
|
-
function assemble(lockfilePath, manifest, descriptorToVersion, entryByVersion, lockfileVersion, isBerry) {
|
|
335
|
-
const resolve3 = (name, range) => {
|
|
336
|
-
const candidates = isBerry ? [`${name}@${range}`, `${name}@npm:${range}`] : [`${name}@${range}`];
|
|
337
|
-
for (const c of candidates) {
|
|
338
|
-
const v = descriptorToVersion.get(c);
|
|
339
|
-
if (v) return v;
|
|
340
|
-
}
|
|
341
|
-
return void 0;
|
|
342
|
-
};
|
|
343
|
-
const nodes = /* @__PURE__ */ Object.create(null);
|
|
344
|
-
for (const [key, entry] of entryByVersion) {
|
|
345
|
-
const at = key.lastIndexOf("@");
|
|
346
|
-
const name = key.slice(0, at);
|
|
347
|
-
const version = key.slice(at + 1);
|
|
348
|
-
if (!nodes[key]) nodes[key] = { key, name, version, dependencies: [] };
|
|
349
|
-
const addEdges = (deps) => {
|
|
350
|
-
for (const [depName, depRange] of Object.entries(deps ?? {})) {
|
|
351
|
-
const v = resolve3(depName, depRange);
|
|
352
|
-
if (!v) continue;
|
|
353
|
-
const targetKey = pkgKey(depName, v);
|
|
354
|
-
if (targetKey !== key) nodes[key].dependencies.push(targetKey);
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
addEdges(entry.dependencies);
|
|
358
|
-
addEdges(entry.optionalDependencies);
|
|
359
|
-
}
|
|
360
|
-
const toDirect = (deps) => {
|
|
361
|
-
const out = [];
|
|
362
|
-
for (const [name, range] of Object.entries(deps)) {
|
|
363
|
-
const v = resolve3(name, range);
|
|
364
|
-
if (v) out.push(pkgKey(name, v));
|
|
365
|
-
}
|
|
366
|
-
return out;
|
|
367
|
-
};
|
|
368
|
-
return {
|
|
369
|
-
manager: "yarn",
|
|
370
|
-
lockfilePath,
|
|
371
|
-
lockfileVersion,
|
|
372
|
-
root: { name: manifest.name, version: manifest.version },
|
|
373
|
-
nodes,
|
|
374
|
-
directProd: toDirect(manifest.dependencies),
|
|
375
|
-
directDev: toDirect(manifest.devDependencies),
|
|
376
|
-
directOptional: toDirect(manifest.optionalDependencies),
|
|
377
|
-
unscannable: []
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// src/lockfiles/pnpm.ts
|
|
382
|
-
import { readFileSync as readFileSync4 } from "fs";
|
|
383
|
-
import { dirname as dirname3 } from "path";
|
|
384
|
-
import yaml2 from "js-yaml";
|
|
385
|
-
function parsePnpm(lockfilePath) {
|
|
386
|
-
const doc = yaml2.load(readFileSync4(lockfilePath, "utf8")) ?? {};
|
|
387
|
-
const manifest = readRootManifest(dirname3(lockfilePath));
|
|
388
|
-
const major = majorVersion(doc.lockfileVersion);
|
|
389
|
-
const nodes = /* @__PURE__ */ Object.create(null);
|
|
390
|
-
for (const key of Object.keys(doc.packages ?? {})) {
|
|
391
|
-
const parsed = parseKey(key, major);
|
|
392
|
-
if (!parsed) continue;
|
|
393
|
-
const nodeKey = pkgKey(parsed.name, parsed.version);
|
|
394
|
-
if (!nodes[nodeKey]) {
|
|
395
|
-
nodes[nodeKey] = {
|
|
396
|
-
key: nodeKey,
|
|
397
|
-
name: parsed.name,
|
|
398
|
-
version: parsed.version,
|
|
399
|
-
dependencies: []
|
|
400
|
-
};
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const edgeSource = major >= 9 ? doc.snapshots ?? {} : doc.packages ?? {};
|
|
404
|
-
for (const [key, entry] of Object.entries(edgeSource)) {
|
|
405
|
-
const parsed = parseKey(key, major);
|
|
406
|
-
if (!parsed) continue;
|
|
407
|
-
const fromKey = pkgKey(parsed.name, parsed.version);
|
|
408
|
-
if (!nodes[fromKey]) {
|
|
409
|
-
nodes[fromKey] = {
|
|
410
|
-
key: fromKey,
|
|
411
|
-
name: parsed.name,
|
|
412
|
-
version: parsed.version,
|
|
413
|
-
dependencies: []
|
|
414
|
-
};
|
|
415
|
-
}
|
|
416
|
-
const addEdges = (deps) => {
|
|
417
|
-
for (const [depName, depValue] of Object.entries(deps ?? {})) {
|
|
418
|
-
const targetKey = pkgKey(depName, stripVersion(depValue, major));
|
|
419
|
-
if (targetKey !== fromKey && nodes[targetKey]) nodes[fromKey].dependencies.push(targetKey);
|
|
420
|
-
}
|
|
421
|
-
};
|
|
422
|
-
addEdges(entry.dependencies);
|
|
423
|
-
addEdges(entry.optionalDependencies);
|
|
424
|
-
}
|
|
425
|
-
const importers = doc.importers ?? {
|
|
426
|
-
".": {
|
|
427
|
-
dependencies: doc.dependencies,
|
|
428
|
-
devDependencies: doc.devDependencies,
|
|
429
|
-
optionalDependencies: doc.optionalDependencies
|
|
430
|
-
}
|
|
431
|
-
};
|
|
432
|
-
const directProd = /* @__PURE__ */ new Set();
|
|
433
|
-
const directDev = /* @__PURE__ */ new Set();
|
|
434
|
-
const directOptional = /* @__PURE__ */ new Set();
|
|
435
|
-
const collect = (deps, into) => {
|
|
436
|
-
for (const [name, val] of Object.entries(deps ?? {})) {
|
|
437
|
-
const version = importerVersion(val, major);
|
|
438
|
-
if (!version) continue;
|
|
439
|
-
const key = pkgKey(name, version);
|
|
440
|
-
if (nodes[key]) into.add(key);
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
for (const imp of Object.values(importers)) {
|
|
444
|
-
collect(imp.dependencies, directProd);
|
|
445
|
-
collect(imp.optionalDependencies, directOptional);
|
|
446
|
-
collect(imp.devDependencies, directDev);
|
|
447
|
-
}
|
|
448
|
-
return {
|
|
449
|
-
manager: "pnpm",
|
|
450
|
-
lockfilePath,
|
|
451
|
-
lockfileVersion: doc.lockfileVersion,
|
|
452
|
-
root: { name: manifest.name, version: manifest.version },
|
|
453
|
-
nodes,
|
|
454
|
-
directProd: [...directProd],
|
|
455
|
-
directDev: [...directDev],
|
|
456
|
-
directOptional: [...directOptional],
|
|
457
|
-
unscannable: []
|
|
458
|
-
};
|
|
459
|
-
}
|
|
460
|
-
function majorVersion(v) {
|
|
461
|
-
const n = parseFloat(String(v ?? "9"));
|
|
462
|
-
return Number.isNaN(n) ? 9 : Math.floor(n);
|
|
463
|
-
}
|
|
464
|
-
function parseKey(key, major) {
|
|
465
|
-
const k = key.startsWith("/") ? key.slice(1) : key;
|
|
466
|
-
if (major < 6) {
|
|
467
|
-
const lastSlash = k.lastIndexOf("/");
|
|
468
|
-
if (lastSlash <= 0) return void 0;
|
|
469
|
-
const name2 = k.slice(0, lastSlash);
|
|
470
|
-
const version2 = stripVersion(k.slice(lastSlash + 1), major);
|
|
471
|
-
return version2 ? { name: name2, version: version2 } : void 0;
|
|
472
|
-
}
|
|
473
|
-
const at = k.lastIndexOf("@");
|
|
474
|
-
if (at <= 0) return void 0;
|
|
475
|
-
const name = k.slice(0, at);
|
|
476
|
-
const version = stripVersion(k.slice(at + 1), major);
|
|
477
|
-
return version ? { name, version } : void 0;
|
|
478
|
-
}
|
|
479
|
-
function stripVersion(value, major) {
|
|
480
|
-
let v = value;
|
|
481
|
-
if (major < 6) {
|
|
482
|
-
const us = v.indexOf("_");
|
|
483
|
-
if (us !== -1) v = v.slice(0, us);
|
|
484
|
-
}
|
|
485
|
-
const paren = v.indexOf("(");
|
|
486
|
-
if (paren !== -1) v = v.slice(0, paren);
|
|
487
|
-
return v;
|
|
488
|
-
}
|
|
489
|
-
function importerVersion(val, major) {
|
|
490
|
-
let ref;
|
|
491
|
-
if (typeof val === "string") ref = val;
|
|
492
|
-
else if (val && typeof val === "object" && typeof val.version === "string") {
|
|
493
|
-
ref = val.version;
|
|
494
|
-
}
|
|
495
|
-
if (!ref) return void 0;
|
|
496
|
-
if (ref.startsWith("link:") || ref.startsWith("file:") || ref.startsWith("workspace:"))
|
|
497
|
-
return void 0;
|
|
498
|
-
return stripVersion(ref, major);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
// src/lockfiles/normalize.ts
|
|
502
|
-
var LockfileError = class extends Error {
|
|
503
|
-
constructor(message) {
|
|
504
|
-
super(message);
|
|
505
|
-
this.name = "LockfileError";
|
|
506
|
-
}
|
|
507
|
-
};
|
|
508
|
-
function parseLockfile(detected) {
|
|
509
|
-
try {
|
|
510
|
-
switch (detected.manager) {
|
|
511
|
-
case "npm":
|
|
512
|
-
return parseNpm(detected.path);
|
|
513
|
-
case "yarn":
|
|
514
|
-
return parseYarn(detected.path);
|
|
515
|
-
case "pnpm":
|
|
516
|
-
return parsePnpm(detected.path);
|
|
517
|
-
}
|
|
518
|
-
} catch (err) {
|
|
519
|
-
throw new LockfileError(
|
|
520
|
-
`Failed to parse ${detected.filename}: ${err instanceof Error ? err.message : String(err)}`
|
|
521
|
-
);
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
function loadDependencyGraph(dir) {
|
|
525
|
-
const detected = detectLockfile(resolve(dir));
|
|
526
|
-
if (!detected) {
|
|
527
|
-
throw new LockfileError(
|
|
528
|
-
`No lockfile found in ${dir}. Expected one of: package-lock.json, yarn.lock, pnpm-lock.yaml.`
|
|
529
|
-
);
|
|
530
|
-
}
|
|
531
|
-
return buildDependencyGraph(parseLockfile(detected));
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// src/graph/paths.ts
|
|
535
|
-
function rootLabel(graph) {
|
|
536
|
-
return graph.root.version ? `${graph.root.name}@${graph.root.version}` : graph.root.name;
|
|
537
|
-
}
|
|
538
|
-
function shortestPath(graph, targetKey) {
|
|
539
|
-
if (!graph.nodes[targetKey]) return null;
|
|
540
|
-
const VIRTUAL_ROOT = "\0root";
|
|
541
|
-
const prev = /* @__PURE__ */ new Map();
|
|
542
|
-
const visited = /* @__PURE__ */ new Set([VIRTUAL_ROOT]);
|
|
543
|
-
const queue = [VIRTUAL_ROOT];
|
|
544
|
-
for (let i = 0; i < queue.length; i++) {
|
|
545
|
-
const current = queue[i];
|
|
546
|
-
const neighbors = current === VIRTUAL_ROOT ? graph.directKeys : graph.nodes[current]?.dependencies ?? [];
|
|
547
|
-
for (const next of neighbors) {
|
|
548
|
-
if (visited.has(next)) continue;
|
|
549
|
-
visited.add(next);
|
|
550
|
-
prev.set(next, current);
|
|
551
|
-
if (next === targetKey) return reconstruct(graph, prev, targetKey, VIRTUAL_ROOT);
|
|
552
|
-
queue.push(next);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
return [rootLabel(graph), targetKey];
|
|
556
|
-
}
|
|
557
|
-
function reconstruct(graph, prev, target, virtualRoot) {
|
|
558
|
-
const path = [];
|
|
559
|
-
let cursor = target;
|
|
560
|
-
while (cursor !== void 0 && cursor !== virtualRoot) {
|
|
561
|
-
path.unshift(cursor);
|
|
562
|
-
cursor = prev.get(cursor);
|
|
563
|
-
}
|
|
564
|
-
path.unshift(rootLabel(graph));
|
|
565
|
-
return path;
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// src/match/ranges.ts
|
|
569
|
-
import semver from "semver";
|
|
570
|
-
function isVersionAffected(version, affected) {
|
|
571
|
-
if (affected.versions?.includes(version)) return true;
|
|
572
|
-
const installed = semver.parse(version, { loose: true });
|
|
573
|
-
if (!installed) return false;
|
|
574
|
-
for (const range of affected.ranges ?? []) {
|
|
575
|
-
if (range.type === "GIT") continue;
|
|
576
|
-
if (rangeAffects(installed, range.events)) return true;
|
|
577
|
-
}
|
|
578
|
-
return false;
|
|
579
|
-
}
|
|
580
|
-
function rangeAffects(installed, events) {
|
|
581
|
-
const boundaries = [];
|
|
582
|
-
for (const event of events) {
|
|
583
|
-
if (event.introduced !== void 0) {
|
|
584
|
-
boundaries.push({
|
|
585
|
-
kind: "introduced",
|
|
586
|
-
version: event.introduced === "0" ? null : parse(event.introduced)
|
|
587
|
-
});
|
|
588
|
-
} else if (event.fixed !== void 0) {
|
|
589
|
-
boundaries.push({ kind: "fixed", version: parse(event.fixed) });
|
|
590
|
-
} else if (event.last_affected !== void 0) {
|
|
591
|
-
boundaries.push({ kind: "last_affected", version: parse(event.last_affected) });
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
boundaries.sort((a, b) => {
|
|
595
|
-
if (a.version === null) return b.version === null ? 0 : -1;
|
|
596
|
-
if (b.version === null) return 1;
|
|
597
|
-
return a.version.compare(b.version);
|
|
598
|
-
});
|
|
599
|
-
let affected = false;
|
|
600
|
-
for (const boundary of boundaries) {
|
|
601
|
-
if (boundary.version === null) {
|
|
602
|
-
affected = true;
|
|
603
|
-
continue;
|
|
604
|
-
}
|
|
605
|
-
const cmp = installed.compare(boundary.version);
|
|
606
|
-
switch (boundary.kind) {
|
|
607
|
-
case "introduced":
|
|
608
|
-
if (cmp >= 0) affected = true;
|
|
609
|
-
break;
|
|
610
|
-
case "fixed":
|
|
611
|
-
if (cmp >= 0) affected = false;
|
|
612
|
-
break;
|
|
613
|
-
case "last_affected":
|
|
614
|
-
if (cmp > 0) affected = false;
|
|
615
|
-
break;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
return affected;
|
|
619
|
-
}
|
|
620
|
-
function parse(version) {
|
|
621
|
-
return semver.parse(version, { loose: true }) ?? semver.coerce(version);
|
|
622
|
-
}
|
|
623
|
-
function vulnerabilityAffects(name, version, vuln) {
|
|
624
|
-
if (vuln.withdrawn) return { affected: false, fixedVersions: [] };
|
|
625
|
-
let affected = false;
|
|
626
|
-
const fixed = /* @__PURE__ */ new Set();
|
|
627
|
-
for (const entry of vuln.affected ?? []) {
|
|
628
|
-
const ecosystem = entry.package?.ecosystem;
|
|
629
|
-
if (ecosystem && ecosystem !== "npm") continue;
|
|
630
|
-
if (entry.package?.name && entry.package.name !== name) continue;
|
|
631
|
-
if (!isVersionAffected(version, entry)) continue;
|
|
632
|
-
affected = true;
|
|
633
|
-
for (const range of entry.ranges ?? []) {
|
|
634
|
-
for (const event of range.events) {
|
|
635
|
-
if (event.fixed) fixed.add(event.fixed);
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
return { affected, fixedVersions: [...fixed] };
|
|
640
|
-
}
|
|
641
|
-
function nearestFix(installed, fixedVersions) {
|
|
642
|
-
const current = semver.parse(installed, { loose: true });
|
|
643
|
-
const candidates = fixedVersions.map((v) => semver.parse(v, { loose: true }) ?? semver.coerce(v)).filter((v) => v !== null).filter((v) => current ? v.compare(current) > 0 : true).sort((a, b) => a.compare(b));
|
|
644
|
-
return candidates[0]?.version;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// src/match/dedupe.ts
|
|
648
|
-
function tokensOf(vuln) {
|
|
649
|
-
return [vuln.id, ...vuln.aliases ?? []];
|
|
650
|
-
}
|
|
651
|
-
function canonicalId(ids) {
|
|
652
|
-
const list = [...ids];
|
|
653
|
-
return list.find((id) => id.startsWith("CVE-")) ?? list.find((id) => id.startsWith("GHSA-")) ?? list[0];
|
|
654
|
-
}
|
|
655
|
-
function dedupeVulnerabilities(vulns) {
|
|
656
|
-
if (vulns.length <= 1) return vulns;
|
|
657
|
-
const parent = /* @__PURE__ */ new Map();
|
|
658
|
-
const find = (x) => {
|
|
659
|
-
let root = x;
|
|
660
|
-
while (parent.get(root) !== void 0 && parent.get(root) !== root) root = parent.get(root);
|
|
661
|
-
parent.set(x, root);
|
|
662
|
-
return root;
|
|
663
|
-
};
|
|
664
|
-
const union = (a, b) => {
|
|
665
|
-
parent.set(find(a), find(b));
|
|
666
|
-
};
|
|
667
|
-
for (const vuln of vulns) {
|
|
668
|
-
const tokens = tokensOf(vuln);
|
|
669
|
-
if (!parent.has(tokens[0])) parent.set(tokens[0], tokens[0]);
|
|
670
|
-
for (const token of tokens.slice(1)) {
|
|
671
|
-
if (!parent.has(token)) parent.set(token, token);
|
|
672
|
-
union(tokens[0], token);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
const groups = /* @__PURE__ */ new Map();
|
|
676
|
-
for (const vuln of vulns) {
|
|
677
|
-
const root = find(vuln.id);
|
|
678
|
-
const group = groups.get(root);
|
|
679
|
-
if (group) group.push(vuln);
|
|
680
|
-
else groups.set(root, [vuln]);
|
|
681
|
-
}
|
|
682
|
-
return [...groups.values()].map(mergeGroup);
|
|
683
|
-
}
|
|
684
|
-
function mergeGroup(group) {
|
|
685
|
-
if (group.length === 1) return group[0];
|
|
686
|
-
const allIds = /* @__PURE__ */ new Set();
|
|
687
|
-
const references = /* @__PURE__ */ new Map();
|
|
688
|
-
const severity = [];
|
|
689
|
-
const affected = [];
|
|
690
|
-
let summary;
|
|
691
|
-
let details;
|
|
692
|
-
let database_specific;
|
|
693
|
-
for (const vuln of group) {
|
|
694
|
-
for (const token of tokensOf(vuln)) allIds.add(token);
|
|
695
|
-
for (const ref of vuln.references ?? []) references.set(ref.url, ref);
|
|
696
|
-
for (const sev of vuln.severity ?? []) severity.push(sev);
|
|
697
|
-
for (const aff of vuln.affected ?? []) affected.push(aff);
|
|
698
|
-
summary ??= vuln.summary;
|
|
699
|
-
details ??= vuln.details;
|
|
700
|
-
database_specific ??= vuln.database_specific;
|
|
701
|
-
}
|
|
702
|
-
const id = canonicalId(allIds);
|
|
703
|
-
return {
|
|
704
|
-
id,
|
|
705
|
-
aliases: [...allIds].filter((token) => token !== id),
|
|
706
|
-
summary,
|
|
707
|
-
details,
|
|
708
|
-
references: [...references.values()],
|
|
709
|
-
severity,
|
|
710
|
-
affected,
|
|
711
|
-
database_specific
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
// src/match/findings.ts
|
|
716
|
-
import semver2 from "semver";
|
|
717
|
-
|
|
718
|
-
// src/score/cvss4-data.ts
|
|
719
|
-
var CVSS4_LOOKUP = {
|
|
720
|
-
"100000": 9.8,
|
|
721
|
-
"100001": 9.5,
|
|
722
|
-
"100010": 9.4,
|
|
723
|
-
"100011": 8.7,
|
|
724
|
-
"100020": 9.1,
|
|
725
|
-
"100021": 8.1,
|
|
726
|
-
"100100": 9.4,
|
|
727
|
-
"100101": 8.9,
|
|
728
|
-
"100110": 8.6,
|
|
729
|
-
"100111": 7.4,
|
|
730
|
-
"100120": 7.7,
|
|
731
|
-
"100121": 6.4,
|
|
732
|
-
"100200": 8.7,
|
|
733
|
-
"100201": 7.5,
|
|
734
|
-
"100210": 7.4,
|
|
735
|
-
"100211": 6.3,
|
|
736
|
-
"100220": 6.3,
|
|
737
|
-
"100221": 4.9,
|
|
738
|
-
"101000": 9.4,
|
|
739
|
-
"101001": 8.9,
|
|
740
|
-
"101010": 8.8,
|
|
741
|
-
"101011": 7.7,
|
|
742
|
-
"101020": 7.6,
|
|
743
|
-
"101021": 6.7,
|
|
744
|
-
"101100": 8.6,
|
|
745
|
-
"101101": 7.6,
|
|
746
|
-
"101110": 7.4,
|
|
747
|
-
"101111": 5.8,
|
|
748
|
-
"101120": 5.9,
|
|
749
|
-
"101121": 5,
|
|
750
|
-
"101200": 7.2,
|
|
751
|
-
"101201": 5.7,
|
|
752
|
-
"101210": 5.7,
|
|
753
|
-
"101211": 5.2,
|
|
754
|
-
"101220": 5.2,
|
|
755
|
-
"101221": 2.5,
|
|
756
|
-
"102001": 8.3,
|
|
757
|
-
"102011": 7,
|
|
758
|
-
"102021": 5.4,
|
|
759
|
-
"102101": 6.5,
|
|
760
|
-
"102111": 5.8,
|
|
761
|
-
"102121": 2.6,
|
|
762
|
-
"102201": 5.3,
|
|
763
|
-
"102211": 2.1,
|
|
764
|
-
"102221": 1.3,
|
|
765
|
-
"110000": 9.5,
|
|
766
|
-
"110001": 9,
|
|
767
|
-
"110010": 8.8,
|
|
768
|
-
"110011": 7.6,
|
|
769
|
-
"110020": 7.6,
|
|
770
|
-
"110021": 7,
|
|
771
|
-
"110100": 9,
|
|
772
|
-
"110101": 7.7,
|
|
773
|
-
"110110": 7.5,
|
|
774
|
-
"110111": 6.2,
|
|
775
|
-
"110120": 6.1,
|
|
776
|
-
"110121": 5.3,
|
|
777
|
-
"110200": 7.7,
|
|
778
|
-
"110201": 6.6,
|
|
779
|
-
"110210": 6.8,
|
|
780
|
-
"110211": 5.9,
|
|
781
|
-
"110220": 5.2,
|
|
782
|
-
"110221": 3,
|
|
783
|
-
"111000": 8.9,
|
|
784
|
-
"111001": 7.8,
|
|
785
|
-
"111010": 7.6,
|
|
786
|
-
"111011": 6.7,
|
|
787
|
-
"111020": 6.2,
|
|
788
|
-
"111021": 5.8,
|
|
789
|
-
"111100": 7.4,
|
|
790
|
-
"111101": 5.9,
|
|
791
|
-
"111110": 5.7,
|
|
792
|
-
"111111": 5.7,
|
|
793
|
-
"111120": 4.7,
|
|
794
|
-
"111121": 2.3,
|
|
795
|
-
"111200": 6.1,
|
|
796
|
-
"111201": 5.2,
|
|
797
|
-
"111210": 5.7,
|
|
798
|
-
"111211": 2.9,
|
|
799
|
-
"111220": 2.4,
|
|
800
|
-
"111221": 1.6,
|
|
801
|
-
"112001": 7.1,
|
|
802
|
-
"112011": 5.9,
|
|
803
|
-
"112021": 3,
|
|
804
|
-
"112101": 5.8,
|
|
805
|
-
"112111": 2.6,
|
|
806
|
-
"112121": 1.5,
|
|
807
|
-
"112201": 2.3,
|
|
808
|
-
"112211": 1.3,
|
|
809
|
-
"112221": 0.6,
|
|
810
|
-
"200000": 9.3,
|
|
811
|
-
"200001": 8.7,
|
|
812
|
-
"200010": 8.6,
|
|
813
|
-
"200011": 7.2,
|
|
814
|
-
"200020": 7.5,
|
|
815
|
-
"200021": 5.8,
|
|
816
|
-
"200100": 8.6,
|
|
817
|
-
"200101": 7.4,
|
|
818
|
-
"200110": 7.4,
|
|
819
|
-
"200111": 6.1,
|
|
820
|
-
"200120": 5.6,
|
|
821
|
-
"200121": 3.4,
|
|
822
|
-
"200200": 7,
|
|
823
|
-
"200201": 5.4,
|
|
824
|
-
"200210": 5.2,
|
|
825
|
-
"200211": 4,
|
|
826
|
-
"200220": 4,
|
|
827
|
-
"200221": 2.2,
|
|
828
|
-
"201000": 8.5,
|
|
829
|
-
"201001": 7.5,
|
|
830
|
-
"201010": 7.4,
|
|
831
|
-
"201011": 5.5,
|
|
832
|
-
"201020": 6.2,
|
|
833
|
-
"201021": 5.1,
|
|
834
|
-
"201100": 7.2,
|
|
835
|
-
"201101": 5.7,
|
|
836
|
-
"201110": 5.5,
|
|
837
|
-
"201111": 4.1,
|
|
838
|
-
"201120": 4.6,
|
|
839
|
-
"201121": 1.9,
|
|
840
|
-
"201200": 5.3,
|
|
841
|
-
"201201": 3.6,
|
|
842
|
-
"201210": 3.4,
|
|
843
|
-
"201211": 1.9,
|
|
844
|
-
"201220": 1.9,
|
|
845
|
-
"201221": 0.8,
|
|
846
|
-
"202001": 6.4,
|
|
847
|
-
"202011": 5.1,
|
|
848
|
-
"202021": 2,
|
|
849
|
-
"202101": 4.7,
|
|
850
|
-
"202111": 2.1,
|
|
851
|
-
"202121": 1.1,
|
|
852
|
-
"202201": 2.4,
|
|
853
|
-
"202211": 0.9,
|
|
854
|
-
"202221": 0.4,
|
|
855
|
-
"210000": 8.8,
|
|
856
|
-
"210001": 7.5,
|
|
857
|
-
"210010": 7.3,
|
|
858
|
-
"210011": 5.3,
|
|
859
|
-
"210020": 6,
|
|
860
|
-
"210021": 5,
|
|
861
|
-
"210100": 7.3,
|
|
862
|
-
"210101": 5.5,
|
|
863
|
-
"210110": 5.9,
|
|
864
|
-
"210111": 4,
|
|
865
|
-
"210120": 4.1,
|
|
866
|
-
"210121": 2,
|
|
867
|
-
"210200": 5.4,
|
|
868
|
-
"210201": 4.3,
|
|
869
|
-
"210210": 4.5,
|
|
870
|
-
"210211": 2.2,
|
|
871
|
-
"210220": 2,
|
|
872
|
-
"210221": 1.1,
|
|
873
|
-
"211000": 7.5,
|
|
874
|
-
"211001": 5.5,
|
|
875
|
-
"211010": 5.8,
|
|
876
|
-
"211011": 4.5,
|
|
877
|
-
"211020": 4,
|
|
878
|
-
"211021": 2.1,
|
|
879
|
-
"211100": 6.1,
|
|
880
|
-
"211101": 5.1,
|
|
881
|
-
"211110": 4.8,
|
|
882
|
-
"211111": 1.8,
|
|
883
|
-
"211120": 2,
|
|
884
|
-
"211121": 0.9,
|
|
885
|
-
"211200": 4.6,
|
|
886
|
-
"211201": 1.8,
|
|
887
|
-
"211210": 1.7,
|
|
888
|
-
"211211": 0.7,
|
|
889
|
-
"211220": 0.8,
|
|
890
|
-
"211221": 0.2,
|
|
891
|
-
"212001": 5.3,
|
|
892
|
-
"212011": 2.4,
|
|
893
|
-
"212021": 1.4,
|
|
894
|
-
"212101": 2.4,
|
|
895
|
-
"212111": 1.2,
|
|
896
|
-
"212121": 0.5,
|
|
897
|
-
"212201": 1,
|
|
898
|
-
"212211": 0.3,
|
|
899
|
-
"212221": 0.1,
|
|
900
|
-
"000000": 10,
|
|
901
|
-
"000001": 9.9,
|
|
902
|
-
"000010": 9.8,
|
|
903
|
-
"000011": 9.5,
|
|
904
|
-
"000020": 9.5,
|
|
905
|
-
"000021": 9.2,
|
|
906
|
-
"000100": 10,
|
|
907
|
-
"000101": 9.6,
|
|
908
|
-
"000110": 9.3,
|
|
909
|
-
"000111": 8.7,
|
|
910
|
-
"000120": 9.1,
|
|
911
|
-
"000121": 8.1,
|
|
912
|
-
"000200": 9.3,
|
|
913
|
-
"000201": 9,
|
|
914
|
-
"000210": 8.9,
|
|
915
|
-
"000211": 8,
|
|
916
|
-
"000220": 8.1,
|
|
917
|
-
"000221": 6.8,
|
|
918
|
-
"001000": 9.8,
|
|
919
|
-
"001001": 9.5,
|
|
920
|
-
"001010": 9.5,
|
|
921
|
-
"001011": 9.2,
|
|
922
|
-
"001020": 9,
|
|
923
|
-
"001021": 8.4,
|
|
924
|
-
"001100": 9.3,
|
|
925
|
-
"001101": 9.2,
|
|
926
|
-
"001110": 8.9,
|
|
927
|
-
"001111": 8.1,
|
|
928
|
-
"001120": 8.1,
|
|
929
|
-
"001121": 6.5,
|
|
930
|
-
"001200": 8.8,
|
|
931
|
-
"001201": 8,
|
|
932
|
-
"001210": 7.8,
|
|
933
|
-
"001211": 7,
|
|
934
|
-
"001220": 6.9,
|
|
935
|
-
"001221": 4.8,
|
|
936
|
-
"002001": 9.2,
|
|
937
|
-
"002011": 8.2,
|
|
938
|
-
"002021": 7.2,
|
|
939
|
-
"002101": 7.9,
|
|
940
|
-
"002111": 6.9,
|
|
941
|
-
"002121": 5,
|
|
942
|
-
"002201": 6.9,
|
|
943
|
-
"002211": 5.5,
|
|
944
|
-
"002221": 2.7,
|
|
945
|
-
"010000": 9.9,
|
|
946
|
-
"010001": 9.7,
|
|
947
|
-
"010010": 9.5,
|
|
948
|
-
"010011": 9.2,
|
|
949
|
-
"010020": 9.2,
|
|
950
|
-
"010021": 8.5,
|
|
951
|
-
"010100": 9.5,
|
|
952
|
-
"010101": 9.1,
|
|
953
|
-
"010110": 9,
|
|
954
|
-
"010111": 8.3,
|
|
955
|
-
"010120": 8.4,
|
|
956
|
-
"010121": 7.1,
|
|
957
|
-
"010200": 9.2,
|
|
958
|
-
"010201": 8.1,
|
|
959
|
-
"010210": 8.2,
|
|
960
|
-
"010211": 7.1,
|
|
961
|
-
"010220": 7.2,
|
|
962
|
-
"010221": 5.3,
|
|
963
|
-
"011000": 9.5,
|
|
964
|
-
"011001": 9.3,
|
|
965
|
-
"011010": 9.2,
|
|
966
|
-
"011011": 8.5,
|
|
967
|
-
"011020": 8.5,
|
|
968
|
-
"011021": 7.3,
|
|
969
|
-
"011100": 9.2,
|
|
970
|
-
"011101": 8.2,
|
|
971
|
-
"011110": 8,
|
|
972
|
-
"011111": 7.2,
|
|
973
|
-
"011120": 7,
|
|
974
|
-
"011121": 5.9,
|
|
975
|
-
"011200": 8.4,
|
|
976
|
-
"011201": 7,
|
|
977
|
-
"011210": 7.1,
|
|
978
|
-
"011211": 5.2,
|
|
979
|
-
"011220": 5,
|
|
980
|
-
"011221": 3,
|
|
981
|
-
"012001": 8.6,
|
|
982
|
-
"012011": 7.5,
|
|
983
|
-
"012021": 5.2,
|
|
984
|
-
"012101": 7.1,
|
|
985
|
-
"012111": 5.2,
|
|
986
|
-
"012121": 2.9,
|
|
987
|
-
"012201": 6.3,
|
|
988
|
-
"012211": 2.9,
|
|
989
|
-
"012221": 1.7
|
|
990
|
-
};
|
|
991
|
-
var CVSS4_MAX_COMPOSED = {
|
|
992
|
-
eq1: {
|
|
993
|
-
"0": ["AV:N/PR:N/UI:N/"],
|
|
994
|
-
"1": ["AV:A/PR:N/UI:N/", "AV:N/PR:L/UI:N/", "AV:N/PR:N/UI:P/"],
|
|
995
|
-
"2": ["AV:P/PR:N/UI:N/", "AV:A/PR:L/UI:P/"]
|
|
996
|
-
},
|
|
997
|
-
eq2: {
|
|
998
|
-
"0": ["AC:L/AT:N/"],
|
|
999
|
-
"1": ["AC:H/AT:N/", "AC:L/AT:P/"]
|
|
1000
|
-
},
|
|
1001
|
-
eq3: {
|
|
1002
|
-
"0": {
|
|
1003
|
-
"0": ["VC:H/VI:H/VA:H/CR:H/IR:H/AR:H/"],
|
|
1004
|
-
"1": ["VC:H/VI:H/VA:L/CR:M/IR:M/AR:H/", "VC:H/VI:H/VA:H/CR:M/IR:M/AR:M/"]
|
|
1005
|
-
},
|
|
1006
|
-
"1": {
|
|
1007
|
-
"0": ["VC:L/VI:H/VA:H/CR:H/IR:H/AR:H/", "VC:H/VI:L/VA:H/CR:H/IR:H/AR:H/"],
|
|
1008
|
-
"1": [
|
|
1009
|
-
"VC:L/VI:H/VA:L/CR:H/IR:M/AR:H/",
|
|
1010
|
-
"VC:L/VI:H/VA:H/CR:H/IR:M/AR:M/",
|
|
1011
|
-
"VC:H/VI:L/VA:H/CR:M/IR:H/AR:M/",
|
|
1012
|
-
"VC:H/VI:L/VA:L/CR:M/IR:H/AR:H/",
|
|
1013
|
-
"VC:L/VI:L/VA:H/CR:H/IR:H/AR:M/"
|
|
1014
|
-
]
|
|
1015
|
-
},
|
|
1016
|
-
"2": {
|
|
1017
|
-
"1": ["VC:L/VI:L/VA:L/CR:H/IR:H/AR:H/"]
|
|
1018
|
-
}
|
|
1019
|
-
},
|
|
1020
|
-
eq4: {
|
|
1021
|
-
"0": ["SC:H/SI:S/SA:S/"],
|
|
1022
|
-
"1": ["SC:H/SI:H/SA:H/"],
|
|
1023
|
-
"2": ["SC:L/SI:L/SA:L/"]
|
|
1024
|
-
},
|
|
1025
|
-
eq5: {
|
|
1026
|
-
"0": ["E:A/"],
|
|
1027
|
-
"1": ["E:P/"],
|
|
1028
|
-
"2": ["E:U/"]
|
|
1029
|
-
}
|
|
1030
|
-
};
|
|
1031
|
-
var CVSS4_MAX_SEVERITY = {
|
|
1032
|
-
eq1: {
|
|
1033
|
-
"0": 1,
|
|
1034
|
-
"1": 4,
|
|
1035
|
-
"2": 5
|
|
1036
|
-
},
|
|
1037
|
-
eq2: {
|
|
1038
|
-
"0": 1,
|
|
1039
|
-
"1": 2
|
|
1040
|
-
},
|
|
1041
|
-
eq3eq6: {
|
|
1042
|
-
"0": {
|
|
1043
|
-
"0": 7,
|
|
1044
|
-
"1": 6
|
|
1045
|
-
},
|
|
1046
|
-
"1": {
|
|
1047
|
-
"0": 8,
|
|
1048
|
-
"1": 8
|
|
1049
|
-
},
|
|
1050
|
-
"2": {
|
|
1051
|
-
"1": 10
|
|
1052
|
-
}
|
|
1053
|
-
},
|
|
1054
|
-
eq4: {
|
|
1055
|
-
"0": 6,
|
|
1056
|
-
"1": 5,
|
|
1057
|
-
"2": 4
|
|
1058
|
-
},
|
|
1059
|
-
eq5: {
|
|
1060
|
-
"0": 1,
|
|
1061
|
-
"1": 1,
|
|
1062
|
-
"2": 1
|
|
1063
|
-
}
|
|
1064
|
-
};
|
|
1065
|
-
|
|
1066
|
-
// src/score/cvss4.ts
|
|
1067
|
-
var BASE_METRICS = ["AV", "AC", "AT", "PR", "UI", "VC", "VI", "VA", "SC", "SI", "SA"];
|
|
1068
|
-
var AV_levels = { N: 0, A: 0.1, L: 0.2, P: 0.3 };
|
|
1069
|
-
var PR_levels = { N: 0, L: 0.1, H: 0.2 };
|
|
1070
|
-
var UI_levels = { N: 0, P: 0.1, A: 0.2 };
|
|
1071
|
-
var AC_levels = { L: 0, H: 0.1 };
|
|
1072
|
-
var AT_levels = { N: 0, P: 0.1 };
|
|
1073
|
-
var VC_levels = { H: 0, L: 0.1, N: 0.2 };
|
|
1074
|
-
var VI_levels = { H: 0, L: 0.1, N: 0.2 };
|
|
1075
|
-
var VA_levels = { H: 0, L: 0.1, N: 0.2 };
|
|
1076
|
-
var SC_levels = { H: 0.1, L: 0.2, N: 0.3 };
|
|
1077
|
-
var SI_levels = { S: 0, H: 0.1, L: 0.2, N: 0.3 };
|
|
1078
|
-
var SA_levels = { S: 0, H: 0.1, L: 0.2, N: 0.3 };
|
|
1079
|
-
var CR_levels = { H: 0, M: 0.1, L: 0.2 };
|
|
1080
|
-
var IR_levels = { H: 0, M: 0.1, L: 0.2 };
|
|
1081
|
-
var AR_levels = { H: 0, M: 0.1, L: 0.2 };
|
|
1082
|
-
function scoreV4(vector) {
|
|
1083
|
-
const sel = parseV4(vector);
|
|
1084
|
-
if (!sel) return null;
|
|
1085
|
-
const macro = macroVector(sel);
|
|
1086
|
-
if (CVSS4_LOOKUP[macro] === void 0) return null;
|
|
1087
|
-
return cvssScore(sel, macro);
|
|
1088
|
-
}
|
|
1089
|
-
function parseV4(vector) {
|
|
1090
|
-
const parts = vector.split("/");
|
|
1091
|
-
if (parts[0] !== "CVSS:4.0") return null;
|
|
1092
|
-
const sel = {};
|
|
1093
|
-
for (const part of parts.slice(1)) {
|
|
1094
|
-
const [k, v] = part.split(":");
|
|
1095
|
-
if (k && v) sel[k] = v;
|
|
1096
|
-
}
|
|
1097
|
-
for (const metric of BASE_METRICS) if (!(metric in sel)) return null;
|
|
1098
|
-
for (const metric of ["E", "CR", "IR", "AR"]) if (!(metric in sel)) sel[metric] = "X";
|
|
1099
|
-
return sel;
|
|
1100
|
-
}
|
|
1101
|
-
function m(sel, metric) {
|
|
1102
|
-
const selected = sel[metric];
|
|
1103
|
-
if (metric === "E" && selected === "X") return "A";
|
|
1104
|
-
if ((metric === "CR" || metric === "IR" || metric === "AR") && selected === "X") return "H";
|
|
1105
|
-
const modified = sel[`M${metric}`];
|
|
1106
|
-
if (modified !== void 0 && modified !== "X") return modified;
|
|
1107
|
-
return selected ?? "";
|
|
1108
|
-
}
|
|
1109
|
-
function macroVector(sel) {
|
|
1110
|
-
const av = m(sel, "AV");
|
|
1111
|
-
const pr = m(sel, "PR");
|
|
1112
|
-
const ui = m(sel, "UI");
|
|
1113
|
-
let eq1 = "2";
|
|
1114
|
-
if (av === "N" && pr === "N" && ui === "N") eq1 = "0";
|
|
1115
|
-
else if ((av === "N" || pr === "N" || ui === "N") && av !== "P") eq1 = "1";
|
|
1116
|
-
const eq2 = m(sel, "AC") === "L" && m(sel, "AT") === "N" ? "0" : "1";
|
|
1117
|
-
const vc = m(sel, "VC");
|
|
1118
|
-
const vi = m(sel, "VI");
|
|
1119
|
-
const va = m(sel, "VA");
|
|
1120
|
-
let eq3 = "2";
|
|
1121
|
-
if (vc === "H" && vi === "H") eq3 = "0";
|
|
1122
|
-
else if (vc === "H" || vi === "H" || va === "H") eq3 = "1";
|
|
1123
|
-
const msi = m(sel, "MSI");
|
|
1124
|
-
const msa = m(sel, "MSA");
|
|
1125
|
-
let eq4 = "2";
|
|
1126
|
-
if (msi === "S" || msa === "S") eq4 = "0";
|
|
1127
|
-
else if (m(sel, "SC") === "H" || m(sel, "SI") === "H" || m(sel, "SA") === "H") eq4 = "1";
|
|
1128
|
-
const e = m(sel, "E");
|
|
1129
|
-
const eq5 = e === "A" ? "0" : e === "P" ? "1" : "2";
|
|
1130
|
-
const cr = m(sel, "CR");
|
|
1131
|
-
const ir = m(sel, "IR");
|
|
1132
|
-
const ar = m(sel, "AR");
|
|
1133
|
-
const eq6 = cr === "H" && vc === "H" || ir === "H" && vi === "H" || ar === "H" && va === "H" ? "0" : "1";
|
|
1134
|
-
return `${eq1}${eq2}${eq3}${eq4}${eq5}${eq6}`;
|
|
1135
|
-
}
|
|
1136
|
-
function lvl(map, key) {
|
|
1137
|
-
return map[key] ?? 0;
|
|
1138
|
-
}
|
|
1139
|
-
function eqMaxes(macro, eq) {
|
|
1140
|
-
const group = CVSS4_MAX_COMPOSED[`eq${eq}`];
|
|
1141
|
-
return group[macro[eq - 1]] ?? [];
|
|
1142
|
-
}
|
|
1143
|
-
function extractValueMetric(metric, str) {
|
|
1144
|
-
const start = str.indexOf(metric) + metric.length + 1;
|
|
1145
|
-
const rest = str.slice(start);
|
|
1146
|
-
const slash = rest.indexOf("/");
|
|
1147
|
-
return slash > 0 ? rest.substring(0, slash) : rest;
|
|
1148
|
-
}
|
|
1149
|
-
function cvssScore(sel, macro) {
|
|
1150
|
-
if (BASE_METRICS.slice(5).every((metric) => m(sel, metric) === "N")) return 0;
|
|
1151
|
-
let value = CVSS4_LOOKUP[macro];
|
|
1152
|
-
const eq1 = Number(macro[0]);
|
|
1153
|
-
const eq3 = Number(macro[2]);
|
|
1154
|
-
const eq6 = Number(macro[5]);
|
|
1155
|
-
const lower = (parts) => CVSS4_LOOKUP[parts.join("")] ?? NaN;
|
|
1156
|
-
const d = macro.split("").map(Number);
|
|
1157
|
-
const scoreEq1Lower = lower([d[0] + 1, d[1], d[2], d[3], d[4], d[5]]);
|
|
1158
|
-
const scoreEq2Lower = lower([d[0], d[1] + 1, d[2], d[3], d[4], d[5]]);
|
|
1159
|
-
let scoreEq3Eq6Lower;
|
|
1160
|
-
if (eq3 === 1 && eq6 === 1) scoreEq3Eq6Lower = lower([d[0], d[1], d[2] + 1, d[3], d[4], d[5]]);
|
|
1161
|
-
else if (eq3 === 0 && eq6 === 1)
|
|
1162
|
-
scoreEq3Eq6Lower = lower([d[0], d[1], d[2] + 1, d[3], d[4], d[5]]);
|
|
1163
|
-
else if (eq3 === 1 && eq6 === 0)
|
|
1164
|
-
scoreEq3Eq6Lower = lower([d[0], d[1], d[2], d[3], d[4], d[5] + 1]);
|
|
1165
|
-
else if (eq3 === 0 && eq6 === 0) {
|
|
1166
|
-
const left = lower([d[0], d[1], d[2], d[3], d[4], d[5] + 1]);
|
|
1167
|
-
const right = lower([d[0], d[1], d[2] + 1, d[3], d[4], d[5]]);
|
|
1168
|
-
scoreEq3Eq6Lower = left > right ? left : right;
|
|
1169
|
-
} else scoreEq3Eq6Lower = lower([d[0], d[1], d[2] + 1, d[3], d[4], d[5] + 1]);
|
|
1170
|
-
const scoreEq4Lower = lower([d[0], d[1], d[2], d[3] + 1, d[4], d[5]]);
|
|
1171
|
-
const scoreEq5Lower = lower([d[0], d[1], d[2], d[3], d[4] + 1, d[5]]);
|
|
1172
|
-
const eq1Maxes = eqMaxes(macro, 1);
|
|
1173
|
-
const eq2Maxes = eqMaxes(macro, 2);
|
|
1174
|
-
const eq3Eq6Maxes = CVSS4_MAX_COMPOSED.eq3[macro[2]][macro[5]];
|
|
1175
|
-
const eq4Maxes = eqMaxes(macro, 4);
|
|
1176
|
-
const eq5Maxes = eqMaxes(macro, 5);
|
|
1177
|
-
const maxVectors = [];
|
|
1178
|
-
for (const a of eq1Maxes)
|
|
1179
|
-
for (const b of eq2Maxes)
|
|
1180
|
-
for (const c of eq3Eq6Maxes)
|
|
1181
|
-
for (const e of eq4Maxes) for (const f of eq5Maxes) maxVectors.push(a + b + c + e + f);
|
|
1182
|
-
let sdAV = 0, sdPR = 0, sdUI = 0, sdAC = 0, sdAT = 0, sdVC = 0, sdVI = 0, sdVA = 0, sdSC = 0, sdSI = 0, sdSA = 0, sdCR = 0, sdIR = 0, sdAR = 0;
|
|
1183
|
-
for (const maxVector of maxVectors) {
|
|
1184
|
-
sdAV = lvl(AV_levels, m(sel, "AV")) - lvl(AV_levels, extractValueMetric("AV", maxVector));
|
|
1185
|
-
sdPR = lvl(PR_levels, m(sel, "PR")) - lvl(PR_levels, extractValueMetric("PR", maxVector));
|
|
1186
|
-
sdUI = lvl(UI_levels, m(sel, "UI")) - lvl(UI_levels, extractValueMetric("UI", maxVector));
|
|
1187
|
-
sdAC = lvl(AC_levels, m(sel, "AC")) - lvl(AC_levels, extractValueMetric("AC", maxVector));
|
|
1188
|
-
sdAT = lvl(AT_levels, m(sel, "AT")) - lvl(AT_levels, extractValueMetric("AT", maxVector));
|
|
1189
|
-
sdVC = lvl(VC_levels, m(sel, "VC")) - lvl(VC_levels, extractValueMetric("VC", maxVector));
|
|
1190
|
-
sdVI = lvl(VI_levels, m(sel, "VI")) - lvl(VI_levels, extractValueMetric("VI", maxVector));
|
|
1191
|
-
sdVA = lvl(VA_levels, m(sel, "VA")) - lvl(VA_levels, extractValueMetric("VA", maxVector));
|
|
1192
|
-
sdSC = lvl(SC_levels, m(sel, "SC")) - lvl(SC_levels, extractValueMetric("SC", maxVector));
|
|
1193
|
-
sdSI = lvl(SI_levels, m(sel, "SI")) - lvl(SI_levels, extractValueMetric("SI", maxVector));
|
|
1194
|
-
sdSA = lvl(SA_levels, m(sel, "SA")) - lvl(SA_levels, extractValueMetric("SA", maxVector));
|
|
1195
|
-
sdCR = lvl(CR_levels, m(sel, "CR")) - lvl(CR_levels, extractValueMetric("CR", maxVector));
|
|
1196
|
-
sdIR = lvl(IR_levels, m(sel, "IR")) - lvl(IR_levels, extractValueMetric("IR", maxVector));
|
|
1197
|
-
sdAR = lvl(AR_levels, m(sel, "AR")) - lvl(AR_levels, extractValueMetric("AR", maxVector));
|
|
1198
|
-
const distances = [
|
|
1199
|
-
sdAV,
|
|
1200
|
-
sdPR,
|
|
1201
|
-
sdUI,
|
|
1202
|
-
sdAC,
|
|
1203
|
-
sdAT,
|
|
1204
|
-
sdVC,
|
|
1205
|
-
sdVI,
|
|
1206
|
-
sdVA,
|
|
1207
|
-
sdSC,
|
|
1208
|
-
sdSI,
|
|
1209
|
-
sdSA,
|
|
1210
|
-
sdCR,
|
|
1211
|
-
sdIR,
|
|
1212
|
-
sdAR
|
|
1213
|
-
];
|
|
1214
|
-
if (distances.some((x) => x < 0)) continue;
|
|
1215
|
-
break;
|
|
1216
|
-
}
|
|
1217
|
-
const csdEq1 = sdAV + sdPR + sdUI;
|
|
1218
|
-
const csdEq2 = sdAC + sdAT;
|
|
1219
|
-
const csdEq3Eq6 = sdVC + sdVI + sdVA + sdCR + sdIR + sdAR;
|
|
1220
|
-
const csdEq4 = sdSC + sdSI + sdSA;
|
|
1221
|
-
const step = 0.1;
|
|
1222
|
-
const availEq1 = value - scoreEq1Lower;
|
|
1223
|
-
const availEq2 = value - scoreEq2Lower;
|
|
1224
|
-
const availEq3Eq6 = value - scoreEq3Eq6Lower;
|
|
1225
|
-
const availEq4 = value - scoreEq4Lower;
|
|
1226
|
-
const availEq5 = value - scoreEq5Lower;
|
|
1227
|
-
const maxSev = CVSS4_MAX_SEVERITY;
|
|
1228
|
-
const maxSevEq1 = maxSev.eq1[String(eq1)] * step;
|
|
1229
|
-
const maxSevEq2 = maxSev.eq2[macro[1]] * step;
|
|
1230
|
-
const maxSevEq3Eq6 = maxSev.eq3eq6[String(eq3)][String(eq6)] * step;
|
|
1231
|
-
const maxSevEq4 = maxSev.eq4[macro[3]] * step;
|
|
1232
|
-
let n = 0;
|
|
1233
|
-
let nsEq1 = 0, nsEq2 = 0, nsEq3Eq6 = 0, nsEq4 = 0;
|
|
1234
|
-
if (!Number.isNaN(availEq1)) {
|
|
1235
|
-
n++;
|
|
1236
|
-
nsEq1 = availEq1 * (csdEq1 / maxSevEq1);
|
|
1237
|
-
}
|
|
1238
|
-
if (!Number.isNaN(availEq2)) {
|
|
1239
|
-
n++;
|
|
1240
|
-
nsEq2 = availEq2 * (csdEq2 / maxSevEq2);
|
|
1241
|
-
}
|
|
1242
|
-
if (!Number.isNaN(availEq3Eq6)) {
|
|
1243
|
-
n++;
|
|
1244
|
-
nsEq3Eq6 = availEq3Eq6 * (csdEq3Eq6 / maxSevEq3Eq6);
|
|
1245
|
-
}
|
|
1246
|
-
if (!Number.isNaN(availEq4)) {
|
|
1247
|
-
n++;
|
|
1248
|
-
nsEq4 = availEq4 * (csdEq4 / maxSevEq4);
|
|
1249
|
-
}
|
|
1250
|
-
if (!Number.isNaN(availEq5)) {
|
|
1251
|
-
n++;
|
|
1252
|
-
}
|
|
1253
|
-
const meanDistance = n === 0 ? 0 : (nsEq1 + nsEq2 + nsEq3Eq6 + nsEq4) / n;
|
|
1254
|
-
value -= meanDistance;
|
|
1255
|
-
if (value < 0) value = 0;
|
|
1256
|
-
if (value > 10) value = 10;
|
|
1257
|
-
return Math.round(value * 10) / 10;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
// src/score/cvss.ts
|
|
1261
|
-
var AV = { N: 0.85, A: 0.62, L: 0.55, P: 0.2 };
|
|
1262
|
-
var AC = { L: 0.77, H: 0.44 };
|
|
1263
|
-
var UI = { N: 0.85, R: 0.62 };
|
|
1264
|
-
var PR_UNCHANGED = { N: 0.85, L: 0.62, H: 0.27 };
|
|
1265
|
-
var PR_CHANGED = { N: 0.85, L: 0.68, H: 0.5 };
|
|
1266
|
-
var CIA = { H: 0.56, L: 0.22, N: 0 };
|
|
1267
|
-
function levelFromScore(score) {
|
|
1268
|
-
if (score <= 0) return "none";
|
|
1269
|
-
if (score < 4) return "low";
|
|
1270
|
-
if (score < 7) return "medium";
|
|
1271
|
-
if (score < 9) return "high";
|
|
1272
|
-
return "critical";
|
|
1273
|
-
}
|
|
1274
|
-
function levelFromLabel(label) {
|
|
1275
|
-
switch (label.trim().toUpperCase()) {
|
|
1276
|
-
case "CRITICAL":
|
|
1277
|
-
return "critical";
|
|
1278
|
-
case "HIGH":
|
|
1279
|
-
return "high";
|
|
1280
|
-
case "MODERATE":
|
|
1281
|
-
case "MEDIUM":
|
|
1282
|
-
return "medium";
|
|
1283
|
-
case "LOW":
|
|
1284
|
-
return "low";
|
|
1285
|
-
case "NONE":
|
|
1286
|
-
return "none";
|
|
1287
|
-
default:
|
|
1288
|
-
return "unknown";
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
function roundUp(input) {
|
|
1292
|
-
const scaled = Math.round(input * 1e5);
|
|
1293
|
-
if (scaled % 1e4 === 0) return scaled / 1e5;
|
|
1294
|
-
return (Math.floor(scaled / 1e4) + 1) / 10;
|
|
1295
|
-
}
|
|
1296
|
-
function scoreFromVector(vector) {
|
|
1297
|
-
const parts = vector.split("/");
|
|
1298
|
-
const header = parts[0] ?? "";
|
|
1299
|
-
if (header === "CVSS:4.0") {
|
|
1300
|
-
const score2 = scoreV4(vector);
|
|
1301
|
-
return score2 === null ? null : { score: score2, level: levelFromScore(score2), version: header };
|
|
1302
|
-
}
|
|
1303
|
-
if (!/^CVSS:3\.[01]$/.test(header)) return null;
|
|
1304
|
-
const m2 = {};
|
|
1305
|
-
for (const part of parts.slice(1)) {
|
|
1306
|
-
const [k, v] = part.split(":");
|
|
1307
|
-
if (k && v) m2[k] = v;
|
|
1308
|
-
}
|
|
1309
|
-
const av = AV[m2.AV ?? ""];
|
|
1310
|
-
const ac = AC[m2.AC ?? ""];
|
|
1311
|
-
const ui = UI[m2.UI ?? ""];
|
|
1312
|
-
const scopeChanged = m2.S === "C";
|
|
1313
|
-
const pr = (scopeChanged ? PR_CHANGED : PR_UNCHANGED)[m2.PR ?? ""];
|
|
1314
|
-
const c = CIA[m2.C ?? ""];
|
|
1315
|
-
const i = CIA[m2.I ?? ""];
|
|
1316
|
-
const a = CIA[m2.A ?? ""];
|
|
1317
|
-
if ([av, ac, ui, pr, c, i, a].some((x) => x === void 0)) return null;
|
|
1318
|
-
const iss = 1 - (1 - c) * (1 - i) * (1 - a);
|
|
1319
|
-
const impact = scopeChanged ? 7.52 * (iss - 0.029) - 3.25 * Math.pow(iss - 0.02, 15) : 6.42 * iss;
|
|
1320
|
-
const exploitability = 8.22 * av * ac * pr * ui;
|
|
1321
|
-
let score;
|
|
1322
|
-
if (impact <= 0) score = 0;
|
|
1323
|
-
else if (scopeChanged) score = roundUp(Math.min(1.08 * (impact + exploitability), 10));
|
|
1324
|
-
else score = roundUp(Math.min(impact + exploitability, 10));
|
|
1325
|
-
return { score, level: levelFromScore(score), version: header };
|
|
1326
|
-
}
|
|
1327
|
-
function resolveSeverity(vuln) {
|
|
1328
|
-
const vectors = [];
|
|
1329
|
-
for (const s of vuln.severity ?? []) if (s.score) vectors.push(s.score);
|
|
1330
|
-
for (const affected of vuln.affected ?? []) {
|
|
1331
|
-
for (const s of affected.severity ?? []) if (s.score) vectors.push(s.score);
|
|
1332
|
-
}
|
|
1333
|
-
let best = null;
|
|
1334
|
-
for (const vector of vectors) {
|
|
1335
|
-
const result = scoreFromVector(vector);
|
|
1336
|
-
if (result && (!best || result.score > best.score)) best = result;
|
|
1337
|
-
}
|
|
1338
|
-
if (best) {
|
|
1339
|
-
return {
|
|
1340
|
-
level: best.level,
|
|
1341
|
-
score: best.score,
|
|
1342
|
-
vector: vectorFor(best, vectors),
|
|
1343
|
-
cvssVersion: best.version,
|
|
1344
|
-
source: "cvss"
|
|
1345
|
-
};
|
|
1346
|
-
}
|
|
1347
|
-
const label = qualitativeLabel(vuln);
|
|
1348
|
-
if (label) return { level: levelFromLabel(label), source: "database" };
|
|
1349
|
-
return { level: "unknown", source: "unknown" };
|
|
1350
|
-
}
|
|
1351
|
-
function vectorFor(best, vectors) {
|
|
1352
|
-
return vectors.find((v) => v.startsWith(best.version));
|
|
1353
|
-
}
|
|
1354
|
-
function qualitativeLabel(vuln) {
|
|
1355
|
-
const fromRecord = (db) => {
|
|
1356
|
-
const sev = db?.severity;
|
|
1357
|
-
return typeof sev === "string" ? sev : void 0;
|
|
1358
|
-
};
|
|
1359
|
-
const top = fromRecord(vuln.database_specific);
|
|
1360
|
-
if (top) return top;
|
|
1361
|
-
for (const affected of vuln.affected ?? []) {
|
|
1362
|
-
const label = fromRecord(affected.database_specific);
|
|
1363
|
-
if (label) return label;
|
|
1364
|
-
}
|
|
1365
|
-
return void 0;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
// src/match/findings.ts
|
|
1369
|
-
function buildFindings(graph, candidatesFor, options = {}) {
|
|
1370
|
-
const findings = [];
|
|
1371
|
-
for (const node of Object.values(graph.nodes)) {
|
|
1372
|
-
if (node.unscannable) continue;
|
|
1373
|
-
if (options.prodOnly && node.scope === "dev") continue;
|
|
1374
|
-
const candidates = candidatesFor(node.name);
|
|
1375
|
-
if (candidates.length === 0) continue;
|
|
1376
|
-
const matched = candidates.filter(
|
|
1377
|
-
(vuln) => vulnerabilityAffects(node.name, node.version, vuln).affected
|
|
1378
|
-
);
|
|
1379
|
-
if (matched.length === 0) continue;
|
|
1380
|
-
const path = shortestPath(graph, node.key);
|
|
1381
|
-
for (const vuln of dedupeVulnerabilities(matched)) {
|
|
1382
|
-
if (isIgnored(vuln, options.ignore)) continue;
|
|
1383
|
-
const { fixedVersions } = vulnerabilityAffects(node.name, node.version, vuln);
|
|
1384
|
-
const sortedFixes = sortVersions(fixedVersions);
|
|
1385
|
-
const nearest = nearestFix(node.version, fixedVersions);
|
|
1386
|
-
findings.push({
|
|
1387
|
-
id: vuln.id,
|
|
1388
|
-
aliases: vuln.aliases ?? [],
|
|
1389
|
-
packageName: node.name,
|
|
1390
|
-
version: node.version,
|
|
1391
|
-
scope: node.scope,
|
|
1392
|
-
direct: node.direct,
|
|
1393
|
-
severity: resolveSeverity(vuln),
|
|
1394
|
-
summary: vuln.summary ?? vuln.id,
|
|
1395
|
-
details: vuln.details,
|
|
1396
|
-
references: (vuln.references ?? []).map((ref) => ref.url),
|
|
1397
|
-
fixedVersions: sortedFixes,
|
|
1398
|
-
recommendation: buildRecommendation(node.name, node.direct, nearest, graph.manager),
|
|
1399
|
-
dependencyPaths: path ? [path] : [],
|
|
1400
|
-
source: "osv"
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
return findings;
|
|
1405
|
-
}
|
|
1406
|
-
function isIgnored(vuln, ignore) {
|
|
1407
|
-
if (!ignore || ignore.size === 0) return false;
|
|
1408
|
-
if (ignore.has(vuln.id)) return true;
|
|
1409
|
-
return (vuln.aliases ?? []).some((alias) => ignore.has(alias));
|
|
1410
|
-
}
|
|
1411
|
-
function sortVersions(versions) {
|
|
1412
|
-
return [...new Set(versions)].sort((a, b) => {
|
|
1413
|
-
const pa = semver2.parse(a, { loose: true });
|
|
1414
|
-
const pb = semver2.parse(b, { loose: true });
|
|
1415
|
-
if (pa && pb) return pa.compare(pb);
|
|
1416
|
-
return a.localeCompare(b);
|
|
1417
|
-
});
|
|
1418
|
-
}
|
|
1419
|
-
var INSTALL_COMMAND = {
|
|
1420
|
-
npm: "npm install",
|
|
1421
|
-
yarn: "yarn add",
|
|
1422
|
-
pnpm: "pnpm add"
|
|
1423
|
-
};
|
|
1424
|
-
function buildRecommendation(name, direct, nearest, manager) {
|
|
1425
|
-
if (!nearest) {
|
|
1426
|
-
return direct ? "No fixed version is available yet. Watch the advisory, or evaluate an alternative package." : "No fixed version is available yet. Pressure the parent dependency to update, or override the version.";
|
|
1427
|
-
}
|
|
1428
|
-
if (direct) {
|
|
1429
|
-
return `Upgrade to ${name}@${nearest} or later (e.g. \`${INSTALL_COMMAND[manager]} ${name}@${nearest}\`).`;
|
|
1430
|
-
}
|
|
1431
|
-
return `Update the dependency that pulls in ${name} so it resolves to ${name}@${nearest} or later. If it cannot, force the resolution via an override.`;
|
|
1432
|
-
}
|
|
1433
|
-
|
|
1434
|
-
// src/osv/database.ts
|
|
1435
|
-
var OsvDatabase = class {
|
|
1436
|
-
byName = /* @__PURE__ */ new Map();
|
|
1437
|
-
ids = /* @__PURE__ */ new Set();
|
|
1438
|
-
add(vuln) {
|
|
1439
|
-
if (vuln.withdrawn) return;
|
|
1440
|
-
if (this.ids.has(vuln.id)) return;
|
|
1441
|
-
this.ids.add(vuln.id);
|
|
1442
|
-
const names = /* @__PURE__ */ new Set();
|
|
1443
|
-
for (const affected of vuln.affected ?? []) {
|
|
1444
|
-
const ecosystem = affected.package?.ecosystem;
|
|
1445
|
-
if (ecosystem && ecosystem !== "npm") continue;
|
|
1446
|
-
if (affected.package?.name) names.add(affected.package.name);
|
|
1447
|
-
}
|
|
1448
|
-
for (const name of names) {
|
|
1449
|
-
const list = this.byName.get(name);
|
|
1450
|
-
if (list) list.push(vuln);
|
|
1451
|
-
else this.byName.set(name, [vuln]);
|
|
1452
|
-
}
|
|
1453
|
-
}
|
|
1454
|
-
addAll(vulns) {
|
|
1455
|
-
for (const vuln of vulns) this.add(vuln);
|
|
1456
|
-
}
|
|
1457
|
-
/** Candidate advisories that name `packageName` (still need a version check). */
|
|
1458
|
-
vulnerabilitiesFor(packageName) {
|
|
1459
|
-
return this.byName.get(packageName) ?? [];
|
|
1460
|
-
}
|
|
1461
|
-
/** Number of distinct advisories indexed. */
|
|
1462
|
-
get size() {
|
|
1463
|
-
return this.ids.size;
|
|
1464
|
-
}
|
|
1465
|
-
};
|
|
1466
|
-
|
|
1467
|
-
// src/engine.ts
|
|
1468
|
-
import { resolve as resolve2 } from "path";
|
|
1469
|
-
|
|
1470
|
-
// src/cache/paths.ts
|
|
1471
|
-
import { join as join2 } from "path";
|
|
1472
|
-
import envPaths from "env-paths";
|
|
1473
|
-
function resolveCacheDir(override) {
|
|
1474
|
-
if (override) return override;
|
|
1475
|
-
if (process.env.LOCKHAWK_CACHE) return process.env.LOCKHAWK_CACHE;
|
|
1476
|
-
return envPaths("lockhawk", { suffix: "" }).cache;
|
|
1477
|
-
}
|
|
1478
|
-
function offlineDbDir(cacheDir) {
|
|
1479
|
-
return join2(cacheDir, "osv-db", "npm");
|
|
1480
|
-
}
|
|
1481
|
-
var SHARD_BUCKETS = 4096;
|
|
1482
|
-
function shardBucket(packageName) {
|
|
1483
|
-
let hash = 2166136261;
|
|
1484
|
-
for (let i = 0; i < packageName.length; i++) {
|
|
1485
|
-
hash ^= packageName.charCodeAt(i);
|
|
1486
|
-
hash = Math.imul(hash, 16777619);
|
|
1487
|
-
}
|
|
1488
|
-
return (hash >>> 0) % SHARD_BUCKETS;
|
|
1489
|
-
}
|
|
1490
|
-
function offlineMetaPath(cacheDir) {
|
|
1491
|
-
return join2(offlineDbDir(cacheDir), "meta.json");
|
|
1492
|
-
}
|
|
1493
|
-
function queryCachePath(cacheDir, id) {
|
|
1494
|
-
return join2(cacheDir, "query-cache", `${encodeURIComponent(id)}.json`);
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
// src/osv/client.ts
|
|
1498
|
-
import { mkdir, readFile, writeFile } from "fs/promises";
|
|
1499
|
-
import { dirname as dirname4 } from "path";
|
|
1500
|
-
import pLimit from "p-limit";
|
|
1501
|
-
import pRetry, { AbortError } from "p-retry";
|
|
1502
|
-
var OSV_API = "https://api.osv.dev/v1";
|
|
1503
|
-
var BATCH_SIZE = 1e3;
|
|
1504
|
-
var OsvClient = class {
|
|
1505
|
-
constructor(opts) {
|
|
1506
|
-
this.opts = opts;
|
|
1507
|
-
this.limit = pLimit(opts.concurrency ?? 5);
|
|
1508
|
-
}
|
|
1509
|
-
opts;
|
|
1510
|
-
limit;
|
|
1511
|
-
/** Fetch advisories naming the given packages, keyed by package name. */
|
|
1512
|
-
async fetchAdvisories(packages) {
|
|
1513
|
-
const names = [...new Set(packages.map((p) => p.name))];
|
|
1514
|
-
const idsByName = await this.queryBatch(names);
|
|
1515
|
-
const allIds = /* @__PURE__ */ new Set();
|
|
1516
|
-
for (const ids of idsByName.values()) for (const id of ids) allIds.add(id);
|
|
1517
|
-
const records = await this.hydrate([...allIds]);
|
|
1518
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1519
|
-
for (const [name, ids] of idsByName) {
|
|
1520
|
-
const list = [];
|
|
1521
|
-
for (const id of ids) {
|
|
1522
|
-
const record = records.get(id);
|
|
1523
|
-
if (record) list.push(record);
|
|
1524
|
-
}
|
|
1525
|
-
if (list.length) byName.set(name, list);
|
|
1526
|
-
}
|
|
1527
|
-
return byName;
|
|
1528
|
-
}
|
|
1529
|
-
async queryBatch(names) {
|
|
1530
|
-
const idsByName = /* @__PURE__ */ new Map();
|
|
1531
|
-
for (let i = 0; i < names.length; i += BATCH_SIZE) {
|
|
1532
|
-
const chunk = names.slice(i, i + BATCH_SIZE);
|
|
1533
|
-
const body = {
|
|
1534
|
-
queries: chunk.map((name) => ({ package: { name, ecosystem: "npm" } }))
|
|
1535
|
-
};
|
|
1536
|
-
const data = await this.request(`${OSV_API}/querybatch`, body);
|
|
1537
|
-
for (let j = 0; j < chunk.length; j++) {
|
|
1538
|
-
const name = chunk[j];
|
|
1539
|
-
const result = data.results[j];
|
|
1540
|
-
if (!result) continue;
|
|
1541
|
-
const set = idsByName.get(name) ?? /* @__PURE__ */ new Set();
|
|
1542
|
-
for (const vuln of result.vulns ?? []) set.add(vuln.id);
|
|
1543
|
-
let pageToken = result.next_page_token;
|
|
1544
|
-
while (pageToken) {
|
|
1545
|
-
const page = await this.request(`${OSV_API}/query`, {
|
|
1546
|
-
package: { name, ecosystem: "npm" },
|
|
1547
|
-
page_token: pageToken
|
|
1548
|
-
});
|
|
1549
|
-
for (const vuln of page.vulns ?? []) set.add(vuln.id);
|
|
1550
|
-
pageToken = page.next_page_token;
|
|
1551
|
-
}
|
|
1552
|
-
if (set.size) idsByName.set(name, set);
|
|
1553
|
-
}
|
|
1554
|
-
}
|
|
1555
|
-
return idsByName;
|
|
1556
|
-
}
|
|
1557
|
-
async hydrate(ids) {
|
|
1558
|
-
const records = /* @__PURE__ */ new Map();
|
|
1559
|
-
await Promise.all(
|
|
1560
|
-
ids.map(
|
|
1561
|
-
(id) => this.limit(async () => {
|
|
1562
|
-
const record = await this.getVulnerability(id);
|
|
1563
|
-
if (record) records.set(id, record);
|
|
1564
|
-
})
|
|
1565
|
-
)
|
|
1566
|
-
);
|
|
1567
|
-
return records;
|
|
1568
|
-
}
|
|
1569
|
-
async getVulnerability(id) {
|
|
1570
|
-
const cached = await this.readCache(id);
|
|
1571
|
-
if (cached) return cached;
|
|
1572
|
-
const record = await this.request(
|
|
1573
|
-
`${OSV_API}/vulns/${encodeURIComponent(id)}`
|
|
1574
|
-
);
|
|
1575
|
-
await this.writeCache(id, record);
|
|
1576
|
-
return record;
|
|
1577
|
-
}
|
|
1578
|
-
async request(url, body) {
|
|
1579
|
-
const retries = this.opts.retries ?? 3;
|
|
1580
|
-
return pRetry(
|
|
1581
|
-
async () => {
|
|
1582
|
-
const res = await fetch(url, {
|
|
1583
|
-
method: body ? "POST" : "GET",
|
|
1584
|
-
headers: body ? { "content-type": "application/json" } : void 0,
|
|
1585
|
-
body: body ? JSON.stringify(body) : void 0
|
|
1586
|
-
});
|
|
1587
|
-
if (res.ok) return await res.json();
|
|
1588
|
-
if (res.status === 429 || res.status >= 500) {
|
|
1589
|
-
throw new Error(`OSV ${res.status} for ${url}`);
|
|
1590
|
-
}
|
|
1591
|
-
throw new AbortError(`OSV ${res.status} for ${url}`);
|
|
1592
|
-
},
|
|
1593
|
-
{ retries }
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
|
-
async readCache(id) {
|
|
1597
|
-
if (this.opts.noCache) return void 0;
|
|
1598
|
-
try {
|
|
1599
|
-
const parsed = JSON.parse(await readFile(queryCachePath(this.opts.cacheDir, id), "utf8"));
|
|
1600
|
-
const ttlMs = (this.opts.cacheTtlHours ?? 24) * 36e5;
|
|
1601
|
-
if (Date.now() - parsed.fetchedAt > ttlMs) return void 0;
|
|
1602
|
-
return parsed.vuln;
|
|
1603
|
-
} catch {
|
|
1604
|
-
return void 0;
|
|
1605
|
-
}
|
|
1606
|
-
}
|
|
1607
|
-
async writeCache(id, vuln) {
|
|
1608
|
-
if (this.opts.noCache) return;
|
|
1609
|
-
try {
|
|
1610
|
-
const path = queryCachePath(this.opts.cacheDir, id);
|
|
1611
|
-
await mkdir(dirname4(path), { recursive: true });
|
|
1612
|
-
await writeFile(path, JSON.stringify({ fetchedAt: Date.now(), vuln }));
|
|
1613
|
-
} catch {
|
|
1614
|
-
}
|
|
1615
|
-
}
|
|
1616
|
-
};
|
|
1617
|
-
|
|
1618
|
-
// src/osv/offline-db.ts
|
|
1619
|
-
import { createWriteStream } from "fs";
|
|
1620
|
-
import { mkdir as mkdir2, mkdtemp, readFile as readFile2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
1621
|
-
import { tmpdir } from "os";
|
|
1622
|
-
import { join as join3 } from "path";
|
|
1623
|
-
import { Readable } from "stream";
|
|
1624
|
-
import { pipeline } from "stream/promises";
|
|
1625
|
-
import { gunzipSync, gzipSync } from "zlib";
|
|
1626
|
-
import yauzl from "yauzl";
|
|
1627
|
-
var OSV_NPM_ZIP_URL = "https://storage.googleapis.com/osv-vulnerabilities/npm/all.zip";
|
|
1628
|
-
var MAX_ENTRY_BYTES = 8 * 1024 * 1024;
|
|
1629
|
-
var OfflineDbMissingError = class extends Error {
|
|
1630
|
-
constructor() {
|
|
1631
|
-
super("No offline OSV database found. Run `lockhawk db update` first.");
|
|
1632
|
-
this.name = "OfflineDbMissingError";
|
|
1633
|
-
}
|
|
1634
|
-
};
|
|
1635
|
-
var IMF_FIXDATE = /^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/;
|
|
1636
|
-
var ENTITY_TAG = /^(?:W\/)?"[\x21\x23-\x7e]*"$/;
|
|
1637
|
-
function sanitizeHttpDate(value) {
|
|
1638
|
-
return value && IMF_FIXDATE.test(value) ? value : void 0;
|
|
1639
|
-
}
|
|
1640
|
-
function sanitizeETag(value) {
|
|
1641
|
-
return value && ENTITY_TAG.test(value) ? value : void 0;
|
|
1642
|
-
}
|
|
1643
|
-
async function readOfflineMeta(cacheDir) {
|
|
1644
|
-
try {
|
|
1645
|
-
return JSON.parse(await readFile2(offlineMetaPath(cacheDir), "utf8"));
|
|
1646
|
-
} catch {
|
|
1647
|
-
return void 0;
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
async function updateOfflineDatabase(opts) {
|
|
1651
|
-
const { cacheDir, nowIso, force, onProgress } = opts;
|
|
1652
|
-
const dir = offlineDbDir(cacheDir);
|
|
1653
|
-
await mkdir2(dir, { recursive: true });
|
|
1654
|
-
const previous = force ? void 0 : await readOfflineMeta(cacheDir);
|
|
1655
|
-
const headers = {};
|
|
1656
|
-
const etag = sanitizeETag(previous?.etag);
|
|
1657
|
-
const lastModified = sanitizeHttpDate(previous?.lastModified);
|
|
1658
|
-
if (etag) headers["If-None-Match"] = etag;
|
|
1659
|
-
if (lastModified) headers["If-Modified-Since"] = lastModified;
|
|
1660
|
-
onProgress?.("Fetching npm advisory database from OSV.dev\u2026");
|
|
1661
|
-
const res = await fetch(OSV_NPM_ZIP_URL, { headers });
|
|
1662
|
-
if (res.status === 304 && previous) {
|
|
1663
|
-
const meta2 = { ...previous, lastUpdated: nowIso };
|
|
1664
|
-
await writeFile2(offlineMetaPath(cacheDir), JSON.stringify(meta2, null, 2));
|
|
1665
|
-
return { status: "unchanged", meta: meta2 };
|
|
1666
|
-
}
|
|
1667
|
-
if (!res.ok || !res.body) {
|
|
1668
|
-
throw new Error(`OSV database download failed: HTTP ${res.status}`);
|
|
1669
|
-
}
|
|
1670
|
-
const tmp = await mkdtemp(join3(tmpdir(), "lockhawk-osv-"));
|
|
1671
|
-
const zipPath = join3(tmp, "all.zip");
|
|
1672
|
-
let records;
|
|
1673
|
-
try {
|
|
1674
|
-
await pipeline(
|
|
1675
|
-
Readable.fromWeb(res.body),
|
|
1676
|
-
createWriteStream(zipPath)
|
|
1677
|
-
);
|
|
1678
|
-
onProgress?.("Extracting advisories\u2026");
|
|
1679
|
-
records = await readZipRecords(zipPath);
|
|
1680
|
-
} finally {
|
|
1681
|
-
await rm(tmp, { recursive: true, force: true });
|
|
1682
|
-
}
|
|
1683
|
-
onProgress?.("Building package index\u2026");
|
|
1684
|
-
const byName = groupByPackage(records);
|
|
1685
|
-
await writeShards(cacheDir, byName);
|
|
1686
|
-
const meta = {
|
|
1687
|
-
ecosystem: "npm",
|
|
1688
|
-
source: "osv.dev",
|
|
1689
|
-
lastUpdated: nowIso,
|
|
1690
|
-
recordCount: records.length,
|
|
1691
|
-
packageCount: byName.size,
|
|
1692
|
-
etag: res.headers.get("etag") ?? void 0,
|
|
1693
|
-
lastModified: res.headers.get("last-modified") ?? void 0
|
|
1694
|
-
};
|
|
1695
|
-
await writeFile2(offlineMetaPath(cacheDir), JSON.stringify(meta, null, 2));
|
|
1696
|
-
return { status: "updated", meta };
|
|
1697
|
-
}
|
|
1698
|
-
function bucketDir(cacheDir) {
|
|
1699
|
-
return join3(offlineDbDir(cacheDir), "by-name");
|
|
1700
|
-
}
|
|
1701
|
-
function bucketPath(cacheDir, bucket) {
|
|
1702
|
-
return join3(bucketDir(cacheDir), `${bucket}.json.gz`);
|
|
1703
|
-
}
|
|
1704
|
-
async function loadAdvisoriesForPackages(cacheDir, names) {
|
|
1705
|
-
if (!await readOfflineMeta(cacheDir)) throw new OfflineDbMissingError();
|
|
1706
|
-
const namesByBucket = /* @__PURE__ */ new Map();
|
|
1707
|
-
for (const name of new Set(names)) {
|
|
1708
|
-
const bucket = shardBucket(name);
|
|
1709
|
-
const list = namesByBucket.get(bucket);
|
|
1710
|
-
if (list) list.push(name);
|
|
1711
|
-
else namesByBucket.set(bucket, [name]);
|
|
1712
|
-
}
|
|
1713
|
-
const result = /* @__PURE__ */ new Map();
|
|
1714
|
-
await Promise.all(
|
|
1715
|
-
[...namesByBucket.entries()].map(async ([bucket, bucketNames]) => {
|
|
1716
|
-
try {
|
|
1717
|
-
const data = JSON.parse(
|
|
1718
|
-
gunzipSync(await readFile2(bucketPath(cacheDir, bucket))).toString("utf8")
|
|
1719
|
-
);
|
|
1720
|
-
for (const name of bucketNames) {
|
|
1721
|
-
const records = data[name];
|
|
1722
|
-
if (records?.length) result.set(name, records);
|
|
1723
|
-
}
|
|
1724
|
-
} catch {
|
|
1725
|
-
}
|
|
1726
|
-
})
|
|
1727
|
-
);
|
|
1728
|
-
return result;
|
|
1729
|
-
}
|
|
1730
|
-
function groupByPackage(records) {
|
|
1731
|
-
const byName = /* @__PURE__ */ new Map();
|
|
1732
|
-
for (const record of records) {
|
|
1733
|
-
if (record.withdrawn) continue;
|
|
1734
|
-
const names = /* @__PURE__ */ new Set();
|
|
1735
|
-
for (const affected of record.affected ?? []) {
|
|
1736
|
-
const ecosystem = affected.package?.ecosystem;
|
|
1737
|
-
if (ecosystem && ecosystem !== "npm") continue;
|
|
1738
|
-
if (affected.package?.name) names.add(affected.package.name);
|
|
1739
|
-
}
|
|
1740
|
-
for (const name of names) {
|
|
1741
|
-
const list = byName.get(name);
|
|
1742
|
-
if (list) list.push(record);
|
|
1743
|
-
else byName.set(name, [record]);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
return byName;
|
|
1747
|
-
}
|
|
1748
|
-
async function writeShards(cacheDir, byName) {
|
|
1749
|
-
const dir = bucketDir(cacheDir);
|
|
1750
|
-
await rm(dir, { recursive: true, force: true });
|
|
1751
|
-
await mkdir2(dir, { recursive: true });
|
|
1752
|
-
const buckets = /* @__PURE__ */ new Map();
|
|
1753
|
-
for (const [name, records] of byName) {
|
|
1754
|
-
const bucket = shardBucket(name);
|
|
1755
|
-
const map = buckets.get(bucket) ?? {};
|
|
1756
|
-
map[name] = records;
|
|
1757
|
-
buckets.set(bucket, map);
|
|
1758
|
-
}
|
|
1759
|
-
const entries = [...buckets.entries()];
|
|
1760
|
-
const BATCH = 256;
|
|
1761
|
-
for (let i = 0; i < entries.length; i += BATCH) {
|
|
1762
|
-
await Promise.all(
|
|
1763
|
-
entries.slice(i, i + BATCH).map(
|
|
1764
|
-
([bucket, map]) => writeFile2(bucketPath(cacheDir, bucket), gzipSync(Buffer.from(JSON.stringify(map))))
|
|
1765
|
-
)
|
|
1766
|
-
);
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
function readZipRecords(zipPath) {
|
|
1770
|
-
return new Promise((resolve3, reject) => {
|
|
1771
|
-
const out = [];
|
|
1772
|
-
yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => {
|
|
1773
|
-
if (err || !zip) return reject(err ?? new Error("Failed to open OSV zip"));
|
|
1774
|
-
zip.on("entry", (entry) => {
|
|
1775
|
-
if (!entry.fileName.endsWith(".json")) return zip.readEntry();
|
|
1776
|
-
if (entry.uncompressedSize > MAX_ENTRY_BYTES) return zip.readEntry();
|
|
1777
|
-
zip.openReadStream(entry, (streamErr, stream) => {
|
|
1778
|
-
if (streamErr || !stream) return zip.readEntry();
|
|
1779
|
-
const chunks = [];
|
|
1780
|
-
stream.on("data", (chunk) => chunks.push(chunk));
|
|
1781
|
-
stream.on("error", () => zip.readEntry());
|
|
1782
|
-
stream.on("end", () => {
|
|
1783
|
-
try {
|
|
1784
|
-
out.push(JSON.parse(Buffer.concat(chunks).toString("utf8")));
|
|
1785
|
-
} catch {
|
|
1786
|
-
}
|
|
1787
|
-
zip.readEntry();
|
|
1788
|
-
});
|
|
1789
|
-
});
|
|
1790
|
-
});
|
|
1791
|
-
zip.on("end", () => resolve3(out));
|
|
1792
|
-
zip.on("error", reject);
|
|
1793
|
-
zip.readEntry();
|
|
1794
|
-
});
|
|
1795
|
-
});
|
|
1796
|
-
}
|
|
1797
|
-
|
|
1798
|
-
// src/osv/source.ts
|
|
1799
|
-
var HOUR_MS = 36e5;
|
|
1800
|
-
var OfflineSource = class {
|
|
1801
|
-
constructor(opts) {
|
|
1802
|
-
this.opts = opts;
|
|
1803
|
-
}
|
|
1804
|
-
opts;
|
|
1805
|
-
async prepare(packages) {
|
|
1806
|
-
const meta = await readOfflineMeta(this.opts.cacheDir);
|
|
1807
|
-
if (!meta) throw new OfflineDbMissingError();
|
|
1808
|
-
const map = await loadAdvisoriesForPackages(
|
|
1809
|
-
this.opts.cacheDir,
|
|
1810
|
-
packages.map((p) => p.name)
|
|
1811
|
-
);
|
|
1812
|
-
const ageHours = (Date.now() - Date.parse(meta.lastUpdated)) / HOUR_MS;
|
|
1813
|
-
const stale = ageHours > (this.opts.staleAfterHours ?? 24);
|
|
1814
|
-
const warnings = stale ? [
|
|
1815
|
-
`Offline database is ${Math.round(ageHours)}h old. Run \`lockhawk db update\` for current results.`
|
|
1816
|
-
] : [];
|
|
1817
|
-
return {
|
|
1818
|
-
candidatesFor: (name) => map.get(name) ?? [],
|
|
1819
|
-
database: {
|
|
1820
|
-
source: "offline",
|
|
1821
|
-
recordCount: meta.recordCount,
|
|
1822
|
-
lastUpdated: meta.lastUpdated,
|
|
1823
|
-
ageHours: Math.round(ageHours * 10) / 10,
|
|
1824
|
-
stale,
|
|
1825
|
-
warnings
|
|
1826
|
-
}
|
|
1827
|
-
};
|
|
1828
|
-
}
|
|
1829
|
-
};
|
|
1830
|
-
var OnlineSource = class {
|
|
1831
|
-
constructor(opts) {
|
|
1832
|
-
this.opts = opts;
|
|
1833
|
-
}
|
|
1834
|
-
opts;
|
|
1835
|
-
async prepare(packages) {
|
|
1836
|
-
const client = new OsvClient({
|
|
1837
|
-
cacheDir: this.opts.cacheDir,
|
|
1838
|
-
concurrency: this.opts.concurrency,
|
|
1839
|
-
noCache: this.opts.noCache,
|
|
1840
|
-
cacheTtlHours: this.opts.cacheTtlHours
|
|
1841
|
-
});
|
|
1842
|
-
try {
|
|
1843
|
-
const map = await client.fetchAdvisories(packages);
|
|
1844
|
-
return {
|
|
1845
|
-
candidatesFor: (name) => map.get(name) ?? [],
|
|
1846
|
-
database: { source: "online", stale: false, warnings: [] }
|
|
1847
|
-
};
|
|
1848
|
-
} catch (err) {
|
|
1849
|
-
if (this.opts.strictNetwork) throw err;
|
|
1850
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1851
|
-
return {
|
|
1852
|
-
candidatesFor: () => [],
|
|
1853
|
-
database: {
|
|
1854
|
-
source: "online",
|
|
1855
|
-
stale: true,
|
|
1856
|
-
warnings: [
|
|
1857
|
-
`OSV.dev was unreachable (${reason}). Scan completed without online data \u2014 results may be incomplete.`
|
|
1858
|
-
]
|
|
1859
|
-
}
|
|
1860
|
-
};
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
};
|
|
1864
|
-
var AutoSource = class {
|
|
1865
|
-
constructor(opts) {
|
|
1866
|
-
this.opts = opts;
|
|
1867
|
-
}
|
|
1868
|
-
opts;
|
|
1869
|
-
async prepare(packages) {
|
|
1870
|
-
const meta = await readOfflineMeta(this.opts.cacheDir);
|
|
1871
|
-
const ttl = this.opts.staleAfterHours ?? 24;
|
|
1872
|
-
if (meta) {
|
|
1873
|
-
const ageHours = (Date.now() - Date.parse(meta.lastUpdated)) / HOUR_MS;
|
|
1874
|
-
if (ageHours <= ttl) {
|
|
1875
|
-
return new OfflineSource(this.opts).prepare(packages);
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
try {
|
|
1879
|
-
return await new OnlineSource({ ...this.opts, strictNetwork: true }).prepare(packages);
|
|
1880
|
-
} catch (err) {
|
|
1881
|
-
if (meta) {
|
|
1882
|
-
const resolved = await new OfflineSource(this.opts).prepare(packages);
|
|
1883
|
-
resolved.database.warnings.push(
|
|
1884
|
-
"Used the stale offline database because OSV.dev was unreachable."
|
|
1885
|
-
);
|
|
1886
|
-
return resolved;
|
|
1887
|
-
}
|
|
1888
|
-
if (this.opts.strictNetwork) throw err;
|
|
1889
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1890
|
-
return {
|
|
1891
|
-
candidatesFor: () => [],
|
|
1892
|
-
database: {
|
|
1893
|
-
source: "online",
|
|
1894
|
-
stale: true,
|
|
1895
|
-
warnings: [
|
|
1896
|
-
`No offline database and OSV.dev was unreachable (${reason}). Run \`lockhawk db update\`.`
|
|
1897
|
-
]
|
|
1898
|
-
}
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
}
|
|
1902
|
-
};
|
|
1903
|
-
function createSource(mode, opts) {
|
|
1904
|
-
switch (mode) {
|
|
1905
|
-
case "offline":
|
|
1906
|
-
return new OfflineSource(opts);
|
|
1907
|
-
case "online":
|
|
1908
|
-
return new OnlineSource(opts);
|
|
1909
|
-
default:
|
|
1910
|
-
return new AutoSource(opts);
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
// src/engine.ts
|
|
1915
|
-
var TOOL = { name: "lockhawk", version: "0.1.0" };
|
|
1916
|
-
async function scan(options = {}, source) {
|
|
1917
|
-
const start = Date.now();
|
|
1918
|
-
const dir = resolve2(options.path ?? ".");
|
|
1919
|
-
const graph = loadDependencyGraph(dir);
|
|
1920
|
-
const detected = detectLockfile(dir);
|
|
1921
|
-
const scannable = uniqueScannablePackages(graph, options.prodOnly);
|
|
1922
|
-
const cacheDir = resolveCacheDir(options.cacheDir);
|
|
1923
|
-
const src = source ?? createSource(options.mode ?? "auto", {
|
|
1924
|
-
cacheDir,
|
|
1925
|
-
concurrency: options.concurrency,
|
|
1926
|
-
noCache: options.noCache,
|
|
1927
|
-
cacheTtlHours: options.cacheTtlHours,
|
|
1928
|
-
strictNetwork: options.strictNetwork
|
|
1929
|
-
});
|
|
1930
|
-
options.onProgress?.("Checking dependencies against OSV.dev\u2026");
|
|
1931
|
-
const resolved = await src.prepare(scannable);
|
|
1932
|
-
let findings = buildFindings(graph, resolved.candidatesFor, {
|
|
1933
|
-
prodOnly: options.prodOnly,
|
|
1934
|
-
ignore: options.ignore && options.ignore.length ? new Set(options.ignore) : void 0
|
|
1935
|
-
});
|
|
1936
|
-
if (options.severityThreshold && options.severityThreshold !== "none") {
|
|
1937
|
-
findings = findings.filter(
|
|
1938
|
-
(f) => severityAtLeast(f.severity.level, options.severityThreshold)
|
|
1939
|
-
);
|
|
1940
|
-
}
|
|
1941
|
-
findings.sort(bySeverityThenName);
|
|
1942
|
-
const unscannable = unscannablePackages(graph);
|
|
1943
|
-
return {
|
|
1944
|
-
schemaVersion: 1,
|
|
1945
|
-
tool: { ...TOOL },
|
|
1946
|
-
scannedAt: new Date(start).toISOString(),
|
|
1947
|
-
target: {
|
|
1948
|
-
path: dir,
|
|
1949
|
-
manager: graph.manager,
|
|
1950
|
-
lockfile: detected?.filename ?? "",
|
|
1951
|
-
root: graph.root
|
|
1952
|
-
},
|
|
1953
|
-
database: resolved.database,
|
|
1954
|
-
summary: summarize(findings),
|
|
1955
|
-
stats: {
|
|
1956
|
-
totalPackages: Object.keys(graph.nodes).length,
|
|
1957
|
-
uniquePackages: scannable.length,
|
|
1958
|
-
directDependencies: graph.directKeys.length,
|
|
1959
|
-
unscannable: unscannable.length,
|
|
1960
|
-
durationMs: Date.now() - start
|
|
1961
|
-
},
|
|
1962
|
-
findings,
|
|
1963
|
-
unscannable
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
function uniqueScannablePackages(graph, prodOnly) {
|
|
1967
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1968
|
-
const out = [];
|
|
1969
|
-
for (const node of Object.values(graph.nodes)) {
|
|
1970
|
-
if (node.unscannable) continue;
|
|
1971
|
-
if (prodOnly && node.scope === "dev") continue;
|
|
1972
|
-
if (seen.has(node.key)) continue;
|
|
1973
|
-
seen.add(node.key);
|
|
1974
|
-
out.push({ name: node.name, version: node.version });
|
|
1975
|
-
}
|
|
1976
|
-
return out;
|
|
1977
|
-
}
|
|
1978
|
-
function unscannablePackages(graph) {
|
|
1979
|
-
const out = [];
|
|
1980
|
-
for (const node of Object.values(graph.nodes)) {
|
|
1981
|
-
if (node.unscannable)
|
|
1982
|
-
out.push({ name: node.name, version: node.version, reason: node.unscannable.reason });
|
|
1983
|
-
}
|
|
1984
|
-
return out;
|
|
1985
|
-
}
|
|
1986
|
-
function summarize(findings) {
|
|
1987
|
-
const summary = {
|
|
1988
|
-
total: findings.length,
|
|
1989
|
-
critical: 0,
|
|
1990
|
-
high: 0,
|
|
1991
|
-
medium: 0,
|
|
1992
|
-
low: 0,
|
|
1993
|
-
unknown: 0,
|
|
1994
|
-
none: 0,
|
|
1995
|
-
vulnerablePackages: 0,
|
|
1996
|
-
fixable: 0
|
|
1997
|
-
};
|
|
1998
|
-
const vulnerable = /* @__PURE__ */ new Set();
|
|
1999
|
-
for (const f of findings) {
|
|
2000
|
-
summary[f.severity.level] += 1;
|
|
2001
|
-
vulnerable.add(`${f.packageName}@${f.version}`);
|
|
2002
|
-
if (f.fixedVersions.length > 0) summary.fixable += 1;
|
|
2003
|
-
}
|
|
2004
|
-
summary.vulnerablePackages = vulnerable.size;
|
|
2005
|
-
return summary;
|
|
2006
|
-
}
|
|
2007
|
-
function bySeverityThenName(a, b) {
|
|
2008
|
-
const rank = SEVERITY_RANK[b.severity.level] - SEVERITY_RANK[a.severity.level];
|
|
2009
|
-
if (rank !== 0) return rank;
|
|
2010
|
-
return a.packageName.localeCompare(b.packageName);
|
|
2011
|
-
}
|
|
2012
|
-
function shouldFail(summary, failOn) {
|
|
2013
|
-
const levels = ["critical", "high", "medium", "low", "unknown"];
|
|
2014
|
-
return levels.some(
|
|
2015
|
-
(level) => SEVERITY_RANK[level] >= SEVERITY_RANK[failOn] && summary[level] > 0
|
|
2016
|
-
);
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
// src/report/json.ts
|
|
2020
|
-
function toJson(result) {
|
|
2021
|
-
return JSON.stringify(result, null, 2);
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// src/report/sarif.ts
|
|
2025
|
-
var INFO_URI = "https://github.com/lockhawk/lockhawk";
|
|
2026
|
-
function helpUriFor(references) {
|
|
2027
|
-
return references.find((ref) => {
|
|
2028
|
-
try {
|
|
2029
|
-
const u = new URL(ref);
|
|
2030
|
-
return u.protocol === "http:" || u.protocol === "https:";
|
|
2031
|
-
} catch {
|
|
2032
|
-
return false;
|
|
2033
|
-
}
|
|
2034
|
-
});
|
|
2035
|
-
}
|
|
2036
|
-
function securitySeverity(finding) {
|
|
2037
|
-
if (typeof finding.severity.score === "number") return finding.severity.score;
|
|
2038
|
-
switch (finding.severity.level) {
|
|
2039
|
-
case "critical":
|
|
2040
|
-
return 9.5;
|
|
2041
|
-
case "high":
|
|
2042
|
-
return 7.5;
|
|
2043
|
-
case "medium":
|
|
2044
|
-
return 5;
|
|
2045
|
-
case "low":
|
|
2046
|
-
return 2;
|
|
2047
|
-
default:
|
|
2048
|
-
return 0;
|
|
2049
|
-
}
|
|
2050
|
-
}
|
|
2051
|
-
function sarifLevel(severity) {
|
|
2052
|
-
if (severity === "critical" || severity === "high") return "error";
|
|
2053
|
-
if (severity === "medium") return "warning";
|
|
2054
|
-
return "note";
|
|
2055
|
-
}
|
|
2056
|
-
function toSarif(result) {
|
|
2057
|
-
const lockfileUri = result.target.lockfile || "package-lock.json";
|
|
2058
|
-
const rules = /* @__PURE__ */ new Map();
|
|
2059
|
-
const results = [];
|
|
2060
|
-
for (const finding of result.findings) {
|
|
2061
|
-
if (!rules.has(finding.id)) {
|
|
2062
|
-
rules.set(finding.id, {
|
|
2063
|
-
id: finding.id,
|
|
2064
|
-
name: `${finding.packageName} ${finding.id}`,
|
|
2065
|
-
shortDescription: { text: finding.summary },
|
|
2066
|
-
fullDescription: finding.details ? { text: finding.details } : void 0,
|
|
2067
|
-
helpUri: helpUriFor(finding.references),
|
|
2068
|
-
properties: {
|
|
2069
|
-
"security-severity": String(securitySeverity(finding)),
|
|
2070
|
-
tags: ["security", "dependency", finding.severity.level]
|
|
2071
|
-
}
|
|
2072
|
-
});
|
|
2073
|
-
}
|
|
2074
|
-
const path = finding.dependencyPaths[0]?.join(" \u203A ") ?? finding.packageName;
|
|
2075
|
-
const fix = finding.fixedVersions[0] ? ` Fixed in ${finding.fixedVersions[0]}.` : "";
|
|
2076
|
-
results.push({
|
|
2077
|
-
ruleId: finding.id,
|
|
2078
|
-
level: sarifLevel(finding.severity.level),
|
|
2079
|
-
message: {
|
|
2080
|
-
text: `${finding.severity.level.toUpperCase()}: ${finding.packageName}@${finding.version} \u2014 ${finding.summary}.${fix} Path: ${path}.`
|
|
2081
|
-
},
|
|
2082
|
-
locations: [
|
|
2083
|
-
{
|
|
2084
|
-
physicalLocation: {
|
|
2085
|
-
artifactLocation: { uri: lockfileUri },
|
|
2086
|
-
region: { startLine: 1 }
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
],
|
|
2090
|
-
partialFingerprints: {
|
|
2091
|
-
lockhawkFinding: `${finding.packageName}@${finding.version}@${finding.id}`
|
|
2092
|
-
},
|
|
2093
|
-
properties: {
|
|
2094
|
-
package: finding.packageName,
|
|
2095
|
-
version: finding.version,
|
|
2096
|
-
scope: finding.scope,
|
|
2097
|
-
fixedVersions: finding.fixedVersions
|
|
2098
|
-
}
|
|
2099
|
-
});
|
|
2100
|
-
}
|
|
2101
|
-
return {
|
|
2102
|
-
$schema: "https://json.schemastore.org/sarif-2.1.0.json",
|
|
2103
|
-
version: "2.1.0",
|
|
2104
|
-
runs: [
|
|
2105
|
-
{
|
|
2106
|
-
tool: {
|
|
2107
|
-
driver: {
|
|
2108
|
-
name: result.tool.name,
|
|
2109
|
-
version: result.tool.version,
|
|
2110
|
-
informationUri: INFO_URI,
|
|
2111
|
-
rules: [...rules.values()]
|
|
2112
|
-
}
|
|
2113
|
-
},
|
|
2114
|
-
results,
|
|
2115
|
-
properties: {
|
|
2116
|
-
scannedAt: result.scannedAt,
|
|
2117
|
-
databaseSource: result.database.source,
|
|
2118
|
-
databaseAgeHours: result.database.ageHours,
|
|
2119
|
-
databaseStale: result.database.stale
|
|
2120
|
-
}
|
|
2121
|
-
}
|
|
2122
|
-
]
|
|
2123
|
-
};
|
|
2124
|
-
}
|
|
2125
|
-
function toSarifString(result) {
|
|
2126
|
-
return JSON.stringify(toSarif(result), null, 2);
|
|
2127
|
-
}
|
|
2128
|
-
|
|
2129
|
-
// src/report/html.ts
|
|
2130
|
-
function toHtml(result) {
|
|
2131
|
-
return renderHtml(result);
|
|
2132
|
-
}
|
|
2133
|
-
var SEVERITY_COLORS = {
|
|
2134
|
-
critical: "#ff5d67",
|
|
2135
|
-
high: "#ff9f43",
|
|
2136
|
-
medium: "#ffd23f",
|
|
2137
|
-
low: "#5b9dff",
|
|
2138
|
-
unknown: "#8b95a5",
|
|
2139
|
-
none: "#34d399"
|
|
2140
|
-
};
|
|
2141
|
-
function esc(text) {
|
|
2142
|
-
return text.replace(
|
|
2143
|
-
/[&<>"]/g,
|
|
2144
|
-
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]
|
|
2145
|
-
);
|
|
2146
|
-
}
|
|
2147
|
-
function httpHref(url) {
|
|
2148
|
-
try {
|
|
2149
|
-
const u = new URL(url);
|
|
2150
|
-
return u.protocol === "http:" || u.protocol === "https:" ? url : void 0;
|
|
2151
|
-
} catch {
|
|
2152
|
-
return void 0;
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
var MARK = `<svg class="mark" viewBox="0 0 48 48" fill="none" aria-hidden="true">
|
|
2156
|
-
<defs>
|
|
2157
|
-
<linearGradient id="lh-g" x1="6" y1="4" x2="40" y2="44" gradientUnits="userSpaceOnUse">
|
|
2158
|
-
<stop stop-color="#ffc266"/><stop offset="1" stop-color="#ff5d67"/>
|
|
2159
|
-
</linearGradient>
|
|
2160
|
-
</defs>
|
|
2161
|
-
<path d="M24 4 40 11v13c0 9-7 15.2-16 20-9-4.8-16-11-16-20V11Z" stroke="url(#lh-g)" stroke-width="2.2" stroke-linejoin="round"/>
|
|
2162
|
-
<path d="M14 18.5 24 15l10 3.5" stroke="url(#lh-g)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
2163
|
-
<circle cx="24" cy="23" r="3.2" fill="url(#lh-g)"/>
|
|
2164
|
-
<path d="M24 26.2V32" stroke="url(#lh-g)" stroke-width="2.2" stroke-linecap="round"/>
|
|
2165
|
-
</svg>`;
|
|
2166
|
-
var CHEVRON = `<svg class="chev" width="11" height="11" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M6 4l4 4-4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`;
|
|
2167
|
-
function renderFix(rec) {
|
|
2168
|
-
const match = rec.match(/`([^`]+)`/);
|
|
2169
|
-
const command = match?.[1];
|
|
2170
|
-
const pending = /^No fixed version/i.test(rec);
|
|
2171
|
-
const prose = command ? rec.replace(/\s*\(e\.g\.[^)]*\)/, "") : rec;
|
|
2172
|
-
const bar = command ? `<div class="cmdbar"><code class="cmd" title="Click to select, then \u2318/Ctrl+C to copy">${esc(
|
|
2173
|
-
command
|
|
2174
|
-
)}</code><span class="hint">click to select</span></div>` : "";
|
|
2175
|
-
return `<div class="fix${pending ? " fix-pending" : ""}">
|
|
2176
|
-
<div class="fix-label">${CHEVRON.replace("chev", "spark")}${pending ? "No fix available yet" : "Recommended fix"}</div>
|
|
2177
|
-
<p class="fix-text">${esc(prose)}</p>${bar}
|
|
2178
|
-
</div>`;
|
|
2179
|
-
}
|
|
2180
|
-
function renderHtml(result) {
|
|
2181
|
-
const { summary } = result;
|
|
2182
|
-
const cards = ["critical", "high", "medium", "low", "unknown"].map(
|
|
2183
|
-
(sev, i) => `
|
|
2184
|
-
<div class="card${summary[sev] === 0 ? " zero" : ""}" style="--c:${SEVERITY_COLORS[sev]};--i:${i}">
|
|
2185
|
-
<div class="count">${summary[sev]}</div><div class="label">${sev}</div>
|
|
2186
|
-
</div>`
|
|
2187
|
-
).join("");
|
|
2188
|
-
const rows = result.findings.map((f, i) => {
|
|
2189
|
-
const path = f.dependencyPaths[0]?.join(" \u203A ") ?? f.packageName;
|
|
2190
|
-
const refs = f.references.slice(0, 3).map((u) => httpHref(u)).filter((u) => u !== void 0).map((u) => `<a class="ref" href="${esc(u)}" target="_blank" rel="noopener">link</a>`).join("");
|
|
2191
|
-
const fixed = f.fixedVersions[0] ? `<span class="fixchip" title="Click to select, then \u2318/Ctrl+C to copy">${esc(
|
|
2192
|
-
f.fixedVersions[0]
|
|
2193
|
-
)}</span>` : '<span class="nofix">\u2014</span>';
|
|
2194
|
-
return `
|
|
2195
|
-
<tr class="main" style="--i:${i}">
|
|
2196
|
-
<td><span class="pill" style="--c:${SEVERITY_COLORS[f.severity.level]}">${esc(
|
|
2197
|
-
f.severity.level
|
|
2198
|
-
)}${f.severity.score ? ` ${f.severity.score}` : ""}</span></td>
|
|
2199
|
-
<td><span class="pkg">${esc(f.packageName)}@${esc(f.version)}</span><div class="scope">${esc(
|
|
2200
|
-
f.scope
|
|
2201
|
-
)}${f.direct ? " \xB7 direct" : ""}</div></td>
|
|
2202
|
-
<td><a class="advid" href="https://osv.dev/${encodeURIComponent(
|
|
2203
|
-
f.id
|
|
2204
|
-
)}" target="_blank" rel="noopener">${esc(f.id)}</a></td>
|
|
2205
|
-
<td><div class="summary">${esc(f.summary)}</div><div class="path">${esc(path)}</div></td>
|
|
2206
|
-
<td>${fixed}</td>
|
|
2207
|
-
<td class="refs">${refs}</td>
|
|
2208
|
-
</tr>
|
|
2209
|
-
<tr class="detail-row">
|
|
2210
|
-
<td colspan="6">
|
|
2211
|
-
${f.recommendation ? renderFix(f.recommendation) : ""}
|
|
2212
|
-
<details>
|
|
2213
|
-
<summary>${CHEVRON}<span>Advisory details</span></summary>
|
|
2214
|
-
<div class="detail-body">
|
|
2215
|
-
<div class="advisory-wrap">
|
|
2216
|
-
<div class="advisory-head"><span class="dots"><i></i><i></i><i></i></span><span class="advisory-title">advisory \xB7 ${esc(
|
|
2217
|
-
f.id
|
|
2218
|
-
)}</span></div>
|
|
2219
|
-
<pre class="advisory">${esc(f.details ?? f.summary)}</pre>
|
|
2220
|
-
</div>
|
|
2221
|
-
</div>
|
|
2222
|
-
</details>
|
|
2223
|
-
</td>
|
|
2224
|
-
</tr>`;
|
|
2225
|
-
}).join("");
|
|
2226
|
-
const dbWarnings = result.database.warnings.map((w) => `<div class="warn"><span class="warn-i">\u26A0</span>${esc(w)}</div>`).join("");
|
|
2227
|
-
const resultsMeta = result.findings.length === 0 ? "" : `<div class="results-meta">
|
|
2228
|
-
<span><b>${summary.total}</b> findings</span><span class="sep">\xB7</span>
|
|
2229
|
-
<span><b>${summary.vulnerablePackages}</b> packages</span><span class="sep">\xB7</span>
|
|
2230
|
-
<span><b>${summary.fixable}</b> fixable</span>
|
|
2231
|
-
</div>`;
|
|
2232
|
-
return `<!doctype html>
|
|
1
|
+
var N={none:0,unknown:1,low:2,medium:3,high:4,critical:5};function qe(e,n){return N[e]>=N[n]}function w(e,n){return`${e}@${n}`}import{statSync as gt}from"fs";import{resolve as mt}from"path";import{existsSync as Fe,readFileSync as Gn,statSync as Bn}from"fs";import{join as Te}from"path";var zn=[{filename:"pnpm-lock.yaml",manager:"pnpm"},{filename:"yarn.lock",manager:"yarn"},{filename:"package-lock.json",manager:"npm"},{filename:"npm-shrinkwrap.json",manager:"npm"}];function F(e){for(let{filename:n,manager:t}of zn){let s=Te(e,n);if(Fe(s)&&Bn(s).isFile())return{manager:t,path:s,filename:n}}}function M(e){let n=Te(e,"package.json"),t={};if(Fe(n))try{t=JSON.parse(Gn(n,"utf8"))}catch{t={}}let s=r=>r&&typeof r=="object"?r:{};return{name:typeof t.name=="string"?t.name:"project",version:typeof t.version=="string"?t.version:void 0,dependencies:s(t.dependencies),devDependencies:s(t.devDependencies),optionalDependencies:s(t.optionalDependencies),peerDependencies:s(t.peerDependencies)}}function fe(e,n){let t=new Set,s=[];for(let r of e)t.has(r)||(t.add(r),s.push(r));for(let r=0;r<s.length;r++){let a=n[s[r]];if(a)for(let i of a.dependencies)t.has(i)||(t.add(i),s.push(i))}return t}function ue(e){let n=fe(e.directProd,e.nodes),t=fe(e.directOptional,e.nodes),s=fe(e.directDev,e.nodes),r=new Set([...e.directProd,...e.directOptional,...e.directDev]),a={};for(let[i,o]of Object.entries(e.nodes)){let d;n.has(i)?d="prod":t.has(i)?d="optional":s.has(i)?d="dev":d="prod",a[i]={key:i,name:o.name,version:o.version,scope:d,direct:r.has(i),dependencies:Yn(o.dependencies),...o.unscannable?{unscannable:o.unscannable}:{}}}return{manager:e.manager,lockfilePath:e.lockfilePath,lockfileVersion:e.lockfileVersion,root:e.root,nodes:a,directKeys:[...r].filter(i=>a[i]!==void 0)}}function Yn(e){return e.length>1?[...new Set(e)]:e}import{readFileSync as Wn}from"fs";import{dirname as Jn}from"path";function Ue(e){let n=JSON.parse(Wn(e,"utf8"));return n.packages?Zn(n,e):Qn(n,e)}function Xn(e){let n="node_modules/",t=e.lastIndexOf(n);return t===-1?e:e.slice(t+n.length)}function Zn(e,n){let t=e.packages??{},s=t[""]??{},r={name:s.name??e.name??"project",version:s.version??e.version},a=Object.create(null),i=[],o=Object.create(null);for(let[l,u]of Object.entries(t)){if(l===""||u.link)continue;let g=u.name??Xn(l),y=u.version;if(!y)continue;let p=w(g,y);if(o[l]=p,!a[p]){let k={key:p,name:g,version:y,dependencies:[]};l.includes("node_modules")||(k.unscannable={reason:"local workspace package"},i.push({name:g,version:y,reason:"local workspace package"})),a[p]=k}}let d=(l,u)=>{let g=l;for(;;){let y=`${g?`${g}/`:""}node_modules/${u}`,p=t[y];if(p)return p.link&&typeof p.resolved=="string"?p.resolved:y;if(!g)return;let k=g.lastIndexOf("/node_modules/");g=k===-1?"":g.slice(0,k)}},c=new Set,f=new Set,m=new Set;for(let[l,u]of Object.entries(t)){if(u.link)continue;let g=l===""?void 0:o[l],y=l===""||!l.includes("node_modules"),p=(k,R)=>{for(let S of k){let O=d(l,S);if(!O)continue;let P=o[O];P&&(g&&g!==P&&a[g].dependencies.push(P),R&&R.add(P))}};p(Object.keys(u.dependencies??{}),y?c:void 0),p(Object.keys(u.optionalDependencies??{}),y?m:void 0),y&&p(Object.keys(u.devDependencies??{}),f)}return{manager:"npm",lockfilePath:n,lockfileVersion:e.lockfileVersion,root:r,nodes:a,directProd:[...c],directDev:[...f],directOptional:[...m],unscannable:i}}var Ke=500;function Qn(e,n){let t=M(Jn(n)),s={name:e.name??t.name,version:e.version??t.version},r=Object.create(null),a=u=>{let g=new Map;for(let[y,p]of Object.entries(u??{}))p?.version&&g.set(y,p.version);return g},i=(u,g)=>{for(let y=g.length-1;y>=0;y--){let p=g[y].get(u);if(p)return p}},o=(u,g)=>{if(g.length>Ke)throw new Error(`dependency nesting exceeds ${Ke} levels (malformed or malicious lockfile)`);let y=a(u),p=[...g,y];for(let[k,R]of Object.entries(u??{})){let S=R?.version;if(!S)continue;let O=w(k,S);r[O]||(r[O]={key:O,name:k,version:S,dependencies:[]});let P=[...p,a(R.dependencies)];for(let C of Object.keys(R.requires??{})){let D=i(C,P);if(D){let $=w(C,D);$!==O&&r[O].dependencies.push($)}}R.dependencies&&o(R.dependencies,p)}};o(e.dependencies,[]);let d=a(e.dependencies),c=[],f=[],m=[],l=(u,g)=>{for(let y of u){let p=d.get(y);p&&g.push(w(y,p))}};return l(Object.keys(t.dependencies),c),l(Object.keys(t.optionalDependencies),m),l(Object.keys(t.devDependencies),f),{manager:"npm",lockfilePath:n,lockfileVersion:e.lockfileVersion,root:s,nodes:r,directProd:c,directDev:f,directOptional:m,unscannable:[]}}import{readFileSync as et}from"fs";import{dirname as nt}from"path";import{createRequire as tt}from"module";import rt from"js-yaml";var st=tt(import.meta.url),ot=st("@yarnpkg/lockfile");function Ge(e){let n=et(e,"utf8"),t=M(nt(e)),s=n.includes("__metadata"),{descriptorToVersion:r,entryByVersion:a,lockfileVersion:i}=s?at(n):it(n);return ct(e,t,r,a,i,s)}function Be(e){let n=e.lastIndexOf("@");if(!(n<=0))return{name:e.slice(0,n),range:e.slice(n+1)}}function it(e){let n=ot.parse(e),t=new Map,s=new Map;for(let[r,a]of Object.entries(n.object))if(a.version)for(let i of r.split(/,\s*/)){let o=Be(i);o&&t.set(`${o.name}@${o.range}`,a.version),o&&s.set(w(o.name,a.version),a)}return{descriptorToVersion:t,entryByVersion:s,lockfileVersion:1}}function at(e){let n=rt.load(e)??{},t=new Map,s=new Map,r="berry";for(let[a,i]of Object.entries(n)){if(a==="__metadata"){let o=i.version;o!==void 0&&(r=o);continue}if(i.version)for(let o of a.split(/,\s*/)){let d=Be(o);d&&(t.set(`${d.name}@${d.range}`,i.version),s.set(w(d.name,i.version),i))}}return{descriptorToVersion:t,entryByVersion:s,lockfileVersion:r}}function ct(e,n,t,s,r,a){let i=(c,f)=>{let m=a?[`${c}@${f}`,`${c}@npm:${f}`]:[`${c}@${f}`];for(let l of m){let u=t.get(l);if(u)return u}},o=Object.create(null);for(let[c,f]of s){let m=c.lastIndexOf("@"),l=c.slice(0,m),u=c.slice(m+1);o[c]||(o[c]={key:c,name:l,version:u,dependencies:[]});let g=y=>{for(let[p,k]of Object.entries(y??{})){let R=i(p,k);if(!R)continue;let S=w(p,R);S!==c&&o[c].dependencies.push(S)}};g(f.dependencies),g(f.optionalDependencies)}let d=c=>{let f=[];for(let[m,l]of Object.entries(c)){let u=i(m,l);u&&f.push(w(m,u))}return f};return{manager:"yarn",lockfilePath:e,lockfileVersion:r,root:{name:n.name,version:n.version},nodes:o,directProd:d(n.dependencies),directDev:d(n.devDependencies),directOptional:d(n.optionalDependencies),unscannable:[]}}import{readFileSync as dt}from"fs";import{dirname as ft}from"path";import ut from"js-yaml";function Ye(e){let n=ut.load(dt(e,"utf8"))??{},t=M(ft(e)),s=lt(n.lockfileVersion),r=Object.create(null);for(let m of Object.keys(n.packages??{})){let l=ze(m,s);if(!l)continue;let u=w(l.name,l.version);r[u]||(r[u]={key:u,name:l.name,version:l.version,dependencies:[]})}let a=s>=9?n.snapshots??{}:n.packages??{};for(let[m,l]of Object.entries(a)){let u=ze(m,s);if(!u)continue;let g=w(u.name,u.version);r[g]||(r[g]={key:g,name:u.name,version:u.version,dependencies:[]});let y=p=>{for(let[k,R]of Object.entries(p??{})){let S=w(k,Y(R,s));S!==g&&r[S]&&r[g].dependencies.push(S)}};y(l.dependencies),y(l.optionalDependencies)}let i=n.importers??{".":{dependencies:n.dependencies,devDependencies:n.devDependencies,optionalDependencies:n.optionalDependencies}},o=new Set,d=new Set,c=new Set,f=(m,l)=>{for(let[u,g]of Object.entries(m??{})){let y=pt(g,s);if(!y)continue;let p=w(u,y);r[p]&&l.add(p)}};for(let m of Object.values(i))f(m.dependencies,o),f(m.optionalDependencies,c),f(m.devDependencies,d);return{manager:"pnpm",lockfilePath:e,lockfileVersion:n.lockfileVersion,root:{name:t.name,version:t.version},nodes:r,directProd:[...o],directDev:[...d],directOptional:[...c],unscannable:[]}}function lt(e){let n=parseFloat(String(e??"9"));return Number.isNaN(n)?9:Math.floor(n)}function ze(e,n){let t=e.startsWith("/")?e.slice(1):e;if(n<6){let i=t.lastIndexOf("/");if(i<=0)return;let o=t.slice(0,i),d=Y(t.slice(i+1),n);return d?{name:o,version:d}:void 0}let s=t.lastIndexOf("@");if(s<=0)return;let r=t.slice(0,s),a=Y(t.slice(s+1),n);return a?{name:r,version:a}:void 0}function Y(e,n){let t=e;if(n<6){let r=t.indexOf("_");r!==-1&&(t=t.slice(0,r))}let s=t.indexOf("(");return s!==-1&&(t=t.slice(0,s)),t}function pt(e,n){let t;if(typeof e=="string"?t=e:e&&typeof e=="object"&&typeof e.version=="string"&&(t=e.version),!!t&&!(t.startsWith("link:")||t.startsWith("file:")||t.startsWith("workspace:")))return Y(t,n)}var E=class extends Error{constructor(n){super(n),this.name="LockfileError"}};function yt(){let e=Number(process.env.LOCKHAWK_MAX_LOCKFILE_BYTES);return Number.isFinite(e)&&e>0?e:64*1024*1024}function We(e){let n=0;try{n=gt(e.path).size}catch{}let t=yt();if(n>t)throw new E(`${e.filename} is ${n} bytes, exceeding the ${t}-byte safety limit. Set LOCKHAWK_MAX_LOCKFILE_BYTES to raise it.`);try{switch(e.manager){case"npm":return Ue(e.path);case"yarn":return Ge(e.path);case"pnpm":return Ye(e.path)}}catch(s){throw new E(`Failed to parse ${e.filename}: ${s instanceof Error?s.message:String(s)}`)}}function le(e){let n=F(mt(e));if(!n)throw new E(`No lockfile found in ${e}. Expected one of: package-lock.json, yarn.lock, pnpm-lock.yaml.`);return ue(We(n))}function pe(e){return e.root.version?`${e.root.name}@${e.root.version}`:e.root.name}function ge(e,n){if(!e.nodes[n])return null;let t="\0root",s=new Map,r=new Set([t]),a=[t];for(let i=0;i<a.length;i++){let o=a[i],d=o===t?e.directKeys:e.nodes[o]?.dependencies??[];for(let c of d)if(!r.has(c)){if(r.add(c),s.set(c,o),c===n)return vt(e,s,n,t);a.push(c)}}return[pe(e),n]}function vt(e,n,t,s){let r=[],a=t;for(;a!==void 0&&a!==s;)r.unshift(a),a=n.get(a);return r.unshift(pe(e)),r}import I from"semver";function Je(e,n){if(n.versions?.includes(e))return!0;let t=I.parse(e,{loose:!0});if(!t)return!1;for(let s of n.ranges??[])if(s.type!=="GIT"&&ht(t,s.events))return!0;return!1}function ht(e,n){let t=[];for(let r of n)r.introduced!==void 0?t.push({kind:"introduced",version:r.introduced==="0"?null:me(r.introduced)}):r.fixed!==void 0?t.push({kind:"fixed",version:me(r.fixed)}):r.last_affected!==void 0&&t.push({kind:"last_affected",version:me(r.last_affected)});t.sort((r,a)=>r.version===null?a.version===null?0:-1:a.version===null?1:r.version.compare(a.version));let s=!1;for(let r of t){if(r.version===null){s=!0;continue}let a=e.compare(r.version);switch(r.kind){case"introduced":a>=0&&(s=!0);break;case"fixed":a>=0&&(s=!1);break;case"last_affected":a>0&&(s=!1);break}}return s}function me(e){return I.parse(e,{loose:!0})??I.coerce(e)}function W(e,n,t){if(t.withdrawn)return{affected:!1,fixedVersions:[]};let s=!1,r=new Set;for(let a of t.affected??[]){let i=a.package?.ecosystem;if(!(i&&i!=="npm")&&!(a.package?.name&&a.package.name!==e)&&Je(n,a)){s=!0;for(let o of a.ranges??[])for(let d of o.events)d.fixed&&r.add(d.fixed)}}return{affected:s,fixedVersions:[...r]}}function ye(e,n){let t=I.parse(e,{loose:!0});return n.map(r=>I.parse(r,{loose:!0})??I.coerce(r)).filter(r=>r!==null).filter(r=>t?r.compare(t)>0:!0).sort((r,a)=>r.compare(a))[0]?.version}function Xe(e){return[e.id,...e.aliases??[]]}function Ze(e){let n=[...e];return n.find(t=>t.startsWith("CVE-"))??n.find(t=>t.startsWith("GHSA-"))??n[0]}function ve(e){if(e.length<=1)return e;let n=new Map,t=a=>{let i=a;for(;n.get(i)!==void 0&&n.get(i)!==i;)i=n.get(i);return n.set(a,i),i},s=(a,i)=>{n.set(t(a),t(i))};for(let a of e){let i=Xe(a);n.has(i[0])||n.set(i[0],i[0]);for(let o of i.slice(1))n.has(o)||n.set(o,o),s(i[0],o)}let r=new Map;for(let a of e){let i=t(a.id),o=r.get(i);o?o.push(a):r.set(i,[a])}return[...r.values()].map(bt)}function bt(e){if(e.length===1)return e[0];let n=new Set,t=new Map,s=[],r=[],a,i,o;for(let c of e){for(let f of Xe(c))n.add(f);for(let f of c.references??[])t.set(f.url,f);for(let f of c.severity??[])s.push(f);for(let f of c.affected??[])r.push(f);a??=c.summary,i??=c.details,o??=c.database_specific}let d=Ze(n);return{id:d,aliases:[...n].filter(c=>c!==d),summary:a,details:i,references:[...t.values()],severity:s,affected:r,database_specific:o}}import kn from"semver";var J={1e5:9.8,100001:9.5,100010:9.4,100011:8.7,100020:9.1,100021:8.1,100100:9.4,100101:8.9,100110:8.6,100111:7.4,100120:7.7,100121:6.4,100200:8.7,100201:7.5,100210:7.4,100211:6.3,100220:6.3,100221:4.9,101e3:9.4,101001:8.9,101010:8.8,101011:7.7,101020:7.6,101021:6.7,101100:8.6,101101:7.6,101110:7.4,101111:5.8,101120:5.9,101121:5,101200:7.2,101201:5.7,101210:5.7,101211:5.2,101220:5.2,101221:2.5,102001:8.3,102011:7,102021:5.4,102101:6.5,102111:5.8,102121:2.6,102201:5.3,102211:2.1,102221:1.3,11e4:9.5,110001:9,110010:8.8,110011:7.6,110020:7.6,110021:7,110100:9,110101:7.7,110110:7.5,110111:6.2,110120:6.1,110121:5.3,110200:7.7,110201:6.6,110210:6.8,110211:5.9,110220:5.2,110221:3,111e3:8.9,111001:7.8,111010:7.6,111011:6.7,111020:6.2,111021:5.8,111100:7.4,111101:5.9,111110:5.7,111111:5.7,111120:4.7,111121:2.3,111200:6.1,111201:5.2,111210:5.7,111211:2.9,111220:2.4,111221:1.6,112001:7.1,112011:5.9,112021:3,112101:5.8,112111:2.6,112121:1.5,112201:2.3,112211:1.3,112221:.6,2e5:9.3,200001:8.7,200010:8.6,200011:7.2,200020:7.5,200021:5.8,200100:8.6,200101:7.4,200110:7.4,200111:6.1,200120:5.6,200121:3.4,200200:7,200201:5.4,200210:5.2,200211:4,200220:4,200221:2.2,201e3:8.5,201001:7.5,201010:7.4,201011:5.5,201020:6.2,201021:5.1,201100:7.2,201101:5.7,201110:5.5,201111:4.1,201120:4.6,201121:1.9,201200:5.3,201201:3.6,201210:3.4,201211:1.9,201220:1.9,201221:.8,202001:6.4,202011:5.1,202021:2,202101:4.7,202111:2.1,202121:1.1,202201:2.4,202211:.9,202221:.4,21e4:8.8,210001:7.5,210010:7.3,210011:5.3,210020:6,210021:5,210100:7.3,210101:5.5,210110:5.9,210111:4,210120:4.1,210121:2,210200:5.4,210201:4.3,210210:4.5,210211:2.2,210220:2,210221:1.1,211e3:7.5,211001:5.5,211010:5.8,211011:4.5,211020:4,211021:2.1,211100:6.1,211101:5.1,211110:4.8,211111:1.8,211120:2,211121:.9,211200:4.6,211201:1.8,211210:1.7,211211:.7,211220:.8,211221:.2,212001:5.3,212011:2.4,212021:1.4,212101:2.4,212111:1.2,212121:.5,212201:1,212211:.3,212221:.1,"000000":10,"000001":9.9,"000010":9.8,"000011":9.5,"000020":9.5,"000021":9.2,"000100":10,"000101":9.6,"000110":9.3,"000111":8.7,"000120":9.1,"000121":8.1,"000200":9.3,"000201":9,"000210":8.9,"000211":8,"000220":8.1,"000221":6.8,"001000":9.8,"001001":9.5,"001010":9.5,"001011":9.2,"001020":9,"001021":8.4,"001100":9.3,"001101":9.2,"001110":8.9,"001111":8.1,"001120":8.1,"001121":6.5,"001200":8.8,"001201":8,"001210":7.8,"001211":7,"001220":6.9,"001221":4.8,"002001":9.2,"002011":8.2,"002021":7.2,"002101":7.9,"002111":6.9,"002121":5,"002201":6.9,"002211":5.5,"002221":2.7,"010000":9.9,"010001":9.7,"010010":9.5,"010011":9.2,"010020":9.2,"010021":8.5,"010100":9.5,"010101":9.1,"010110":9,"010111":8.3,"010120":8.4,"010121":7.1,"010200":9.2,"010201":8.1,"010210":8.2,"010211":7.1,"010220":7.2,"010221":5.3,"011000":9.5,"011001":9.3,"011010":9.2,"011011":8.5,"011020":8.5,"011021":7.3,"011100":9.2,"011101":8.2,"011110":8,"011111":7.2,"011120":7,"011121":5.9,"011200":8.4,"011201":7,"011210":7.1,"011211":5.2,"011220":5,"011221":3,"012001":8.6,"012011":7.5,"012021":5.2,"012101":7.1,"012111":5.2,"012121":2.9,"012201":6.3,"012211":2.9,"012221":1.7},he={eq1:{0:["AV:N/PR:N/UI:N/"],1:["AV:A/PR:N/UI:N/","AV:N/PR:L/UI:N/","AV:N/PR:N/UI:P/"],2:["AV:P/PR:N/UI:N/","AV:A/PR:L/UI:P/"]},eq2:{0:["AC:L/AT:N/"],1:["AC:H/AT:N/","AC:L/AT:P/"]},eq3:{0:{0:["VC:H/VI:H/VA:H/CR:H/IR:H/AR:H/"],1:["VC:H/VI:H/VA:L/CR:M/IR:M/AR:H/","VC:H/VI:H/VA:H/CR:M/IR:M/AR:M/"]},1:{0:["VC:L/VI:H/VA:H/CR:H/IR:H/AR:H/","VC:H/VI:L/VA:H/CR:H/IR:H/AR:H/"],1:["VC:L/VI:H/VA:L/CR:H/IR:M/AR:H/","VC:L/VI:H/VA:H/CR:H/IR:M/AR:M/","VC:H/VI:L/VA:H/CR:M/IR:H/AR:M/","VC:H/VI:L/VA:L/CR:M/IR:H/AR:H/","VC:L/VI:L/VA:H/CR:H/IR:H/AR:M/"]},2:{1:["VC:L/VI:L/VA:L/CR:H/IR:H/AR:H/"]}},eq4:{0:["SC:H/SI:S/SA:S/"],1:["SC:H/SI:H/SA:H/"],2:["SC:L/SI:L/SA:L/"]},eq5:{0:["E:A/"],1:["E:P/"],2:["E:U/"]}},Qe={eq1:{0:1,1:4,2:5},eq2:{0:1,1:2},eq3eq6:{0:{0:7,1:6},1:{0:8,1:8},2:{1:10}},eq4:{0:6,1:5,2:4},eq5:{0:1,1:1,2:1}};var mn=["AV","AC","AT","PR","UI","VC","VI","VA","SC","SI","SA"],en={N:0,A:.1,L:.2,P:.3},nn={N:0,L:.1,H:.2},tn={N:0,P:.1,A:.2},rn={L:0,H:.1},sn={N:0,P:.1},on={H:0,L:.1,N:.2},an={H:0,L:.1,N:.2},cn={H:0,L:.1,N:.2},dn={H:.1,L:.2,N:.3},fn={S:0,H:.1,L:.2,N:.3},un={S:0,H:.1,L:.2,N:.3},ln={H:0,M:.1,L:.2},pn={H:0,M:.1,L:.2},gn={H:0,M:.1,L:.2};function yn(e){let n=kt(e);if(!n)return null;let t=Rt(n);return J[t]===void 0?null:St(n,t)}function kt(e){let n=e.split("/");if(n[0]!=="CVSS:4.0")return null;let t={};for(let s of n.slice(1)){let[r,a]=s.split(":");r&&a&&(t[r]=a)}for(let s of mn)if(!(s in t))return null;for(let s of["E","CR","IR","AR"])s in t||(t[s]="X");return t}function v(e,n){let t=e[n];if(n==="E"&&t==="X")return"A";if((n==="CR"||n==="IR"||n==="AR")&&t==="X")return"H";let s=e[`M${n}`];return s!==void 0&&s!=="X"?s:t??""}function Rt(e){let n=v(e,"AV"),t=v(e,"PR"),s=v(e,"UI"),r="2";n==="N"&&t==="N"&&s==="N"?r="0":(n==="N"||t==="N"||s==="N")&&n!=="P"&&(r="1");let a=v(e,"AC")==="L"&&v(e,"AT")==="N"?"0":"1",i=v(e,"VC"),o=v(e,"VI"),d=v(e,"VA"),c="2";i==="H"&&o==="H"?c="0":(i==="H"||o==="H"||d==="H")&&(c="1");let f=v(e,"MSI"),m=v(e,"MSA"),l="2";f==="S"||m==="S"?l="0":(v(e,"SC")==="H"||v(e,"SI")==="H"||v(e,"SA")==="H")&&(l="1");let u=v(e,"E"),g=u==="A"?"0":u==="P"?"1":"2",y=v(e,"CR"),p=v(e,"IR"),k=v(e,"AR");return`${r}${a}${c}${l}${g}${y==="H"&&i==="H"||p==="H"&&o==="H"||k==="H"&&d==="H"?"0":"1"}`}function h(e,n){return e[n]??0}function X(e,n){return he[`eq${n}`][e[n-1]]??[]}function V(e,n){let t=n.indexOf(e)+e.length+1,s=n.slice(t),r=s.indexOf("/");return r>0?s.substring(0,r):s}function St(e,n){if(mn.slice(5).every(b=>v(e,b)==="N"))return 0;let t=J[n],s=Number(n[0]),r=Number(n[2]),a=Number(n[5]),i=b=>J[b.join("")]??NaN,o=n.split("").map(Number),d=i([o[0]+1,o[1],o[2],o[3],o[4],o[5]]),c=i([o[0],o[1]+1,o[2],o[3],o[4],o[5]]),f;if(r===1&&a===1)f=i([o[0],o[1],o[2]+1,o[3],o[4],o[5]]);else if(r===0&&a===1)f=i([o[0],o[1],o[2]+1,o[3],o[4],o[5]]);else if(r===1&&a===0)f=i([o[0],o[1],o[2],o[3],o[4],o[5]+1]);else if(r===0&&a===0){let b=i([o[0],o[1],o[2],o[3],o[4],o[5]+1]),q=i([o[0],o[1],o[2]+1,o[3],o[4],o[5]]);f=b>q?b:q}else f=i([o[0],o[1],o[2]+1,o[3],o[4],o[5]+1]);let m=i([o[0],o[1],o[2],o[3]+1,o[4],o[5]]),l=i([o[0],o[1],o[2],o[3],o[4]+1,o[5]]),u=X(n,1),g=X(n,2),y=he.eq3[n[2]][n[5]],p=X(n,4),k=X(n,5),R=[];for(let b of u)for(let q of g)for(let de of y)for(let Kn of p)for(let Un of k)R.push(b+q+de+Kn+Un);let S=0,O=0,P=0,C=0,D=0,$=0,ne=0,te=0,re=0,se=0,oe=0,ie=0,ae=0,ce=0;for(let b of R)if(S=h(en,v(e,"AV"))-h(en,V("AV",b)),O=h(nn,v(e,"PR"))-h(nn,V("PR",b)),P=h(tn,v(e,"UI"))-h(tn,V("UI",b)),C=h(rn,v(e,"AC"))-h(rn,V("AC",b)),D=h(sn,v(e,"AT"))-h(sn,V("AT",b)),$=h(on,v(e,"VC"))-h(on,V("VC",b)),ne=h(an,v(e,"VI"))-h(an,V("VI",b)),te=h(cn,v(e,"VA"))-h(cn,V("VA",b)),re=h(dn,v(e,"SC"))-h(dn,V("SC",b)),se=h(fn,v(e,"SI"))-h(fn,V("SI",b)),oe=h(un,v(e,"SA"))-h(un,V("SA",b)),ie=h(ln,v(e,"CR"))-h(ln,V("CR",b)),ae=h(pn,v(e,"IR"))-h(pn,V("IR",b)),ce=h(gn,v(e,"AR"))-h(gn,V("AR",b)),![S,O,P,C,D,$,ne,te,re,se,oe,ie,ae,ce].some(de=>de<0))break;let $n=S+O+P,En=C+D,In=$+ne+te+ie+ae+ce,Hn=re+se+oe,B=.1,De=t-d,$e=t-c,Ee=t-f,Ie=t-m,Ln=t-l,z=Qe,jn=z.eq1[String(s)]*B,_n=z.eq2[n[1]]*B,qn=z.eq3eq6[String(r)][String(a)]*B,Fn=z.eq4[n[3]]*B,A=0,He=0,Le=0,je=0,_e=0;Number.isNaN(De)||(A++,He=De*($n/jn)),Number.isNaN($e)||(A++,Le=$e*(En/_n)),Number.isNaN(Ee)||(A++,je=Ee*(In/qn)),Number.isNaN(Ie)||(A++,_e=Ie*(Hn/Fn)),Number.isNaN(Ln)||A++;let Tn=A===0?0:(He+Le+je+_e)/A;return t-=Tn,t<0&&(t=0),t>10&&(t=10),Math.round(t*10)/10}var xt={N:.85,A:.62,L:.55,P:.2},wt={L:.77,H:.44},Vt={N:.85,R:.62},Ot={N:.85,L:.62,H:.27},Pt={N:.85,L:.68,H:.5},be={H:.56,L:.22,N:0};function ke(e){return e<=0?"none":e<4?"low":e<7?"medium":e<9?"high":"critical"}function hn(e){switch(e.trim().toUpperCase()){case"CRITICAL":return"critical";case"HIGH":return"high";case"MODERATE":case"MEDIUM":return"medium";case"LOW":return"low";case"NONE":return"none";default:return"unknown"}}function vn(e){let n=Math.round(e*1e5);return n%1e4===0?n/1e5:(Math.floor(n/1e4)+1)/10}function bn(e){let n=e.split("/"),t=n[0]??"";if(t==="CVSS:4.0"){let p=yn(e);return p===null?null:{score:p,level:ke(p),version:t}}if(!/^CVSS:3\.[01]$/.test(t))return null;let s={};for(let p of n.slice(1)){let[k,R]=p.split(":");k&&R&&(s[k]=R)}let r=xt[s.AV??""],a=wt[s.AC??""],i=Vt[s.UI??""],o=s.S==="C",d=(o?Pt:Ot)[s.PR??""],c=be[s.C??""],f=be[s.I??""],m=be[s.A??""];if([r,a,i,d,c,f,m].some(p=>p===void 0))return null;let l=1-(1-c)*(1-f)*(1-m),u=o?7.52*(l-.029)-3.25*Math.pow(l-.02,15):6.42*l,g=8.22*r*a*d*i,y;return u<=0?y=0:o?y=vn(Math.min(1.08*(u+g),10)):y=vn(Math.min(u+g,10)),{score:y,level:ke(y),version:t}}function Re(e){let n=[];for(let r of e.severity??[])r.score&&n.push(r.score);for(let r of e.affected??[])for(let a of r.severity??[])a.score&&n.push(a.score);let t=null;for(let r of n){let a=bn(r);a&&(!t||a.score>t.score)&&(t=a)}if(t)return{level:t.level,score:t.score,vector:At(t,n),cvssVersion:t.version,source:"cvss"};let s=Nt(e);return s?{level:hn(s),source:"database"}:{level:"unknown",source:"unknown"}}function At(e,n){return n.find(t=>t.startsWith(e.version))}function Nt(e){let n=s=>{let r=s?.severity;return typeof r=="string"?r:void 0},t=n(e.database_specific);if(t)return t;for(let s of e.affected??[]){let r=n(s.database_specific);if(r)return r}}function Se(e,n,t={}){let s=[];for(let r of Object.values(e.nodes)){if(r.unscannable||t.prodOnly&&r.scope==="dev")continue;let a=n(r.name);if(a.length===0)continue;let i=new Map,o=a.filter(c=>{let f=W(r.name,r.version,c);return i.set(c,f),f.affected});if(o.length===0)continue;let d=ge(e,r.key);for(let c of ve(o)){if(Mt(c,t.ignore))continue;let{fixedVersions:f}=i.get(c)??W(r.name,r.version,c),m=Ct(f),l=ye(r.version,f);s.push({id:c.id,aliases:c.aliases??[],packageName:r.name,version:r.version,scope:r.scope,direct:r.direct,severity:Re(c),summary:c.summary??c.id,details:c.details,references:(c.references??[]).map(u=>u.url),fixedVersions:m,recommendation:$t(r.name,r.direct,l,e.manager),dependencyPaths:d?[d]:[],source:"osv"})}}return s}function Mt(e,n){return!n||n.size===0?!1:n.has(e.id)?!0:(e.aliases??[]).some(t=>n.has(t))}function Ct(e){return[...new Set(e)].sort((n,t)=>{let s=kn.parse(n,{loose:!0}),r=kn.parse(t,{loose:!0});return s&&r?s.compare(r):n.localeCompare(t)})}var Dt={npm:"npm install",yarn:"yarn add",pnpm:"pnpm add"};function $t(e,n,t,s){return t?n?`Upgrade to ${e}@${t} or later (e.g. \`${Dt[s]} ${e}@${t}\`).`:`Update the dependency that pulls in ${e} so it resolves to ${e}@${t} or later. If it cannot, force the resolution via an override.`:n?"No fixed version is available yet. Watch the advisory, or evaluate an alternative package.":"No fixed version is available yet. Pressure the parent dependency to update, or override the version."}var xe=class{byName=new Map;ids=new Set;add(n){if(n.withdrawn||this.ids.has(n.id))return;this.ids.add(n.id);let t=new Set;for(let s of n.affected??[]){let r=s.package?.ecosystem;r&&r!=="npm"||s.package?.name&&t.add(s.package.name)}for(let s of t){let r=this.byName.get(s);r?r.push(n):this.byName.set(s,[n])}}addAll(n){for(let t of n)this.add(t)}vulnerabilitiesFor(n){return this.byName.get(n)??[]}get size(){return this.ids.size}};import{resolve as cr}from"path";import{join as we}from"path";import Et from"env-paths";function Ve(e){return e||(process.env.LOCKHAWK_CACHE?process.env.LOCKHAWK_CACHE:Et("lockhawk",{suffix:""}).cache)}function T(e){return we(e,"osv-db","npm")}var Rn=4096;function Z(e){let n=2166136261;for(let t=0;t<e.length;t++)n^=e.charCodeAt(t),n=Math.imul(n,16777619);return(n>>>0)%Rn}function K(e){return we(T(e),"meta.json")}function Oe(e,n){return we(e,"query-cache",`${encodeURIComponent(n)}.json`)}import{mkdir as It,readFile as Ht,writeFile as Lt}from"fs/promises";import{dirname as jt}from"path";import _t from"p-limit";import qt,{AbortError as Ft}from"p-retry";var Pe="https://api.osv.dev/v1",Sn=1e3,U=class{constructor(n){this.opts=n;this.limit=_t(n.concurrency??16)}opts;limit;async fetchAdvisories(n){let t=[...new Set(n.map(o=>o.name))],s=await this.queryBatch(t),r=new Set;for(let o of s.values())for(let d of o)r.add(d);let a=await this.hydrate([...r]),i=new Map;for(let[o,d]of s){let c=[];for(let f of d){let m=a.get(f);m&&c.push(m)}c.length&&i.set(o,c)}return i}async queryBatch(n){let t=new Map;for(let s=0;s<n.length;s+=Sn){let r=n.slice(s,s+Sn),a={queries:r.map(o=>({package:{name:o,ecosystem:"npm"}}))},i=await this.request(`${Pe}/querybatch`,a);for(let o=0;o<r.length;o++){let d=r[o],c=i.results[o];if(!c)continue;let f=t.get(d)??new Set;for(let l of c.vulns??[])f.add(l.id);let m=c.next_page_token;for(;m;){let l=await this.request(`${Pe}/query`,{package:{name:d,ecosystem:"npm"},page_token:m});for(let u of l.vulns??[])f.add(u.id);m=l.next_page_token}f.size&&t.set(d,f)}}return t}async hydrate(n){let t=new Map;return await Promise.all(n.map(s=>this.limit(async()=>{let r=await this.getVulnerability(s);r&&t.set(s,r)}))),t}async getVulnerability(n){let t=await this.readCache(n);if(t)return t;let s=await this.request(`${Pe}/vulns/${encodeURIComponent(n)}`);return await this.writeCache(n,s),s}async request(n,t){let s=this.opts.retries??3;return qt(async()=>{let r=await fetch(n,{method:t?"POST":"GET",headers:t?{"content-type":"application/json"}:void 0,body:t?JSON.stringify(t):void 0,signal:AbortSignal.timeout(this.opts.timeoutMs??15e3)});if(r.ok)return await r.json();throw r.status===429||r.status>=500?new Error(`OSV ${r.status} for ${n}`):new Ft(`OSV ${r.status} for ${n}`)},{retries:s})}async readCache(n){if(!this.opts.noCache)try{let t=JSON.parse(await Ht(Oe(this.opts.cacheDir,n),"utf8")),s=(this.opts.cacheTtlHours??24)*36e5;return Date.now()-t.fetchedAt>s?void 0:t.vuln}catch{return}}async writeCache(n,t){if(!this.opts.noCache)try{let s=Oe(this.opts.cacheDir,n);await It(jt(s),{recursive:!0}),await Lt(s,JSON.stringify({fetchedAt:Date.now(),vuln:t}))}catch{}}};import{createWriteStream as Tt}from"fs";import{mkdir as xn,mkdtemp as Kt,readFile as wn,rm as Vn,writeFile as Ae}from"fs/promises";import{tmpdir as Ut}from"os";import{join as Q}from"path";import{Readable as Gt}from"stream";import{pipeline as Bt}from"stream/promises";import{gunzipSync as zt,gzipSync as Yt}from"zlib";import Wt from"p-limit";import Jt from"yauzl";var Xt="https://storage.googleapis.com/osv-vulnerabilities/npm/all.zip",Zt=32,Qt=8*1024*1024,H=class extends Error{constructor(){super("No offline OSV database found. Run `lockhawk db update` first."),this.name="OfflineDbMissingError"}},er=/^[A-Za-z]{3}, \d{2} [A-Za-z]{3} \d{4} \d{2}:\d{2}:\d{2} GMT$/,nr=/^(?:W\/)?"[\x21\x23-\x7e]*"$/;function tr(e){return e&&er.test(e)?e:void 0}function rr(e){return e&&nr.test(e)?e:void 0}async function L(e){try{return JSON.parse(await wn(K(e),"utf8"))}catch{return}}async function sr(e){let{cacheDir:n,nowIso:t,force:s,onProgress:r}=e,a=T(n);await xn(a,{recursive:!0});let i=s?void 0:await L(n),o={},d=rr(i?.etag),c=tr(i?.lastModified);d&&(o["If-None-Match"]=d),c&&(o["If-Modified-Since"]=c),r?.("Fetching npm advisory database from OSV.dev\u2026");let f=await fetch(Xt,{headers:o});if(f.status===304&&i){let p={...i,lastUpdated:t};return await Ae(K(n),JSON.stringify(p,null,2)),{status:"unchanged",meta:p}}if(!f.ok||!f.body)throw new Error(`OSV database download failed: HTTP ${f.status}`);let m=await Kt(Q(Ut(),"lockhawk-osv-")),l=Q(m,"all.zip"),u;try{await Bt(Gt.fromWeb(f.body),Tt(l)),r?.("Extracting advisories\u2026"),u=await ar(l)}finally{await Vn(m,{recursive:!0,force:!0})}r?.("Building package index\u2026");let g=or(u);await ir(n,g);let y={ecosystem:"npm",source:"osv.dev",lastUpdated:t,recordCount:u.length,packageCount:g.size,etag:f.headers.get("etag")??void 0,lastModified:f.headers.get("last-modified")??void 0};return await Ae(K(n),JSON.stringify(y,null,2)),{status:"updated",meta:y}}function On(e){return Q(T(e),"by-name")}function Pn(e,n){return Q(On(e),`${n}.json.gz`)}async function Ne(e,n){if(!await L(e))throw new H;let t=new Map;for(let a of new Set(n)){let i=Z(a),o=t.get(i);o?o.push(a):t.set(i,[a])}let s=new Map,r=Wt(Zt);return await Promise.all([...t.entries()].map(([a,i])=>r(async()=>{try{let o=JSON.parse(zt(await wn(Pn(e,a))).toString("utf8"));for(let d of i){let c=o[d];c?.length&&s.set(d,c)}}catch{}}))),s}function or(e){let n=new Map;for(let t of e){if(t.withdrawn)continue;let s=new Set;for(let r of t.affected??[]){let a=r.package?.ecosystem;a&&a!=="npm"||r.package?.name&&s.add(r.package.name)}for(let r of s){let a=n.get(r);a?a.push(t):n.set(r,[t])}}return n}async function ir(e,n){let t=On(e);await Vn(t,{recursive:!0,force:!0}),await xn(t,{recursive:!0});let s=new Map;for(let[i,o]of n){let d=Z(i),c=s.get(d)??{};c[i]=o,s.set(d,c)}let r=[...s.entries()],a=256;for(let i=0;i<r.length;i+=a)await Promise.all(r.slice(i,i+a).map(([o,d])=>Ae(Pn(e,o),Yt(Buffer.from(JSON.stringify(d))))))}function ar(e){return new Promise((n,t)=>{let s=[];Jt.open(e,{lazyEntries:!0},(r,a)=>{if(r||!a)return t(r??new Error("Failed to open OSV zip"));a.on("entry",i=>{if(!i.fileName.endsWith(".json")||i.uncompressedSize>Qt)return a.readEntry();a.openReadStream(i,(o,d)=>{if(o||!d)return a.readEntry();let c=[];d.on("data",f=>c.push(f)),d.on("error",()=>a.readEntry()),d.on("end",()=>{try{s.push(JSON.parse(Buffer.concat(c).toString("utf8")))}catch{}a.readEntry()})})}),a.on("end",()=>n(s)),a.on("error",t),a.readEntry()})})}var An=36e5,j=class{constructor(n){this.opts=n}opts;async prepare(n){let t=await L(this.opts.cacheDir);if(!t)throw new H;let s=await Ne(this.opts.cacheDir,n.map(o=>o.name)),r=(Date.now()-Date.parse(t.lastUpdated))/An,a=r>(this.opts.staleAfterHours??24),i=a?[`Offline database is ${Math.round(r)}h old. Run \`lockhawk db update\` for current results.`]:[];return{candidatesFor:o=>s.get(o)??[],database:{source:"offline",recordCount:t.recordCount,lastUpdated:t.lastUpdated,ageHours:Math.round(r*10)/10,stale:a,warnings:i}}}},G=class{constructor(n){this.opts=n}opts;async prepare(n){let t=new U({cacheDir:this.opts.cacheDir,concurrency:this.opts.concurrency,noCache:this.opts.noCache,cacheTtlHours:this.opts.cacheTtlHours});try{let s=await t.fetchAdvisories(n);return{candidatesFor:r=>s.get(r)??[],database:{source:"online",stale:!1,warnings:[]}}}catch(s){if(this.opts.strictNetwork)throw s;let r=s instanceof Error?s.message:String(s);return{candidatesFor:()=>[],database:{source:"online",stale:!0,warnings:[`OSV.dev was unreachable (${r}). Scan completed without online data \u2014 results may be incomplete.`]}}}}},ee=class{constructor(n){this.opts=n}opts;async prepare(n){let t=await L(this.opts.cacheDir),s=this.opts.staleAfterHours??24;if(t&&(Date.now()-Date.parse(t.lastUpdated))/An<=s)return new j(this.opts).prepare(n);try{return await new G({...this.opts,strictNetwork:!0}).prepare(n)}catch(r){if(t){let i=await new j(this.opts).prepare(n);return i.database.warnings.push("Used the stale offline database because OSV.dev was unreachable."),i}if(this.opts.strictNetwork)throw r;let a=r instanceof Error?r.message:String(r);return{candidatesFor:()=>[],database:{source:"online",stale:!0,warnings:[`No offline database and OSV.dev was unreachable (${a}). Run \`lockhawk db update\`.`]}}}}};function Me(e,n){switch(e){case"offline":return new j(n);case"online":return new G(n);default:return new ee(n)}}var Nn={name:"lockhawk",version:"0.1.0"};async function dr(e={},n){let t=Date.now(),s=cr(e.path??"."),r=le(s),a=F(s),i=Mn(r,e.prodOnly),o=Ve(e.cacheDir),d=n??Me(e.mode??"auto",{cacheDir:o,concurrency:e.concurrency,noCache:e.noCache,cacheTtlHours:e.cacheTtlHours,strictNetwork:e.strictNetwork});e.onProgress?.("Checking dependencies against OSV.dev\u2026");let c=await d.prepare(i),f=Se(r,c.candidatesFor,{prodOnly:e.prodOnly,ignore:e.ignore&&e.ignore.length?new Set(e.ignore):void 0});e.severityThreshold&&e.severityThreshold!=="none"&&(f=f.filter(l=>qe(l.severity.level,e.severityThreshold))),f.sort(lr);let m=fr(r);return{schemaVersion:1,tool:{...Nn},scannedAt:new Date(t).toISOString(),target:{path:s,manager:r.manager,lockfile:a?.filename??"",root:r.root},database:c.database,summary:ur(f),stats:{totalPackages:Object.keys(r.nodes).length,uniquePackages:i.length,directDependencies:r.directKeys.length,unscannable:m.length,durationMs:Date.now()-t},findings:f,unscannable:m}}function Mn(e,n){let t=new Set,s=[];for(let r of Object.values(e.nodes))r.unscannable||n&&r.scope==="dev"||t.has(r.key)||(t.add(r.key),s.push({name:r.name,version:r.version}));return s}function fr(e){let n=[];for(let t of Object.values(e.nodes))t.unscannable&&n.push({name:t.name,version:t.version,reason:t.unscannable.reason});return n}function ur(e){let n={total:e.length,critical:0,high:0,medium:0,low:0,unknown:0,none:0,vulnerablePackages:0,fixable:0},t=new Set;for(let s of e)n[s.severity.level]+=1,t.add(`${s.packageName}@${s.version}`),s.fixedVersions.length>0&&(n.fixable+=1);return n.vulnerablePackages=t.size,n}function lr(e,n){let t=N[n.severity.level]-N[e.severity.level];return t!==0?t:e.packageName.localeCompare(n.packageName)}function pr(e,n){return["critical","high","medium","low","unknown"].some(s=>N[s]>=N[n]&&e[s]>0)}function gr(e){return JSON.stringify(e,null,2)}var mr="https://github.com/lockhawk/lockhawk";function yr(e){return e.find(n=>{try{let t=new URL(n);return t.protocol==="http:"||t.protocol==="https:"}catch{return!1}})}function vr(e){if(typeof e.severity.score=="number")return e.severity.score;switch(e.severity.level){case"critical":return 9.5;case"high":return 7.5;case"medium":return 5;case"low":return 2;default:return 0}}function hr(e){return e==="critical"||e==="high"?"error":e==="medium"?"warning":"note"}function Cn(e){let n=e.target.lockfile||"package-lock.json",t=new Map,s=[];for(let r of e.findings){t.has(r.id)||t.set(r.id,{id:r.id,name:`${r.packageName} ${r.id}`,shortDescription:{text:r.summary},fullDescription:r.details?{text:r.details}:void 0,helpUri:yr(r.references),properties:{"security-severity":String(vr(r)),tags:["security","dependency",r.severity.level]}});let a=r.dependencyPaths[0]?.join(" \u203A ")??r.packageName,i=r.fixedVersions[0]?` Fixed in ${r.fixedVersions[0]}.`:"";s.push({ruleId:r.id,level:hr(r.severity.level),message:{text:`${r.severity.level.toUpperCase()}: ${r.packageName}@${r.version} \u2014 ${r.summary}.${i} Path: ${a}.`},locations:[{physicalLocation:{artifactLocation:{uri:n},region:{startLine:1}}}],partialFingerprints:{lockhawkFinding:`${r.packageName}@${r.version}@${r.id}`},properties:{package:r.packageName,version:r.version,scope:r.scope,fixedVersions:r.fixedVersions}})}return{$schema:"https://json.schemastore.org/sarif-2.1.0.json",version:"2.1.0",runs:[{tool:{driver:{name:e.tool.name,version:e.tool.version,informationUri:mr,rules:[...t.values()]}},results:s,properties:{scannedAt:e.scannedAt,databaseSource:e.database.source,databaseAgeHours:e.database.ageHours,databaseStale:e.database.stale}}]}}function br(e){return JSON.stringify(Cn(e),null,2)}var Ce="<!--LOCKHAWK_DATA-->";function kr(e){return`<script>window.__SCAN_RESULT__ = ${JSON.stringify(e).replace(/</g,"\\u003c").replace(/\u2028/g,"\\u2028").replace(/\u2029/g,"\\u2029")};</script>`}function Rr(e,n){let t=kr(e);return n?n.includes(Ce)?n.replace(Ce,()=>t):n.replace("</head>",()=>`${t}</head>`):xr(e,t)}var Dn={critical:"#b4232c",high:"#d9822b",medium:"#c9a227",low:"#3a7bd5",unknown:"#7a7a7a",none:"#3a9e54"};function x(e){return e.replace(/[&<>"]/g,n=>({"&":"&","<":"<",">":">",'"':"""})[n])}function Sr(e){try{let n=new URL(e);return n.protocol==="http:"||n.protocol==="https:"?e:void 0}catch{return}}function xr(e,n){let{summary:t}=e,s=["critical","high","medium","low","unknown"].map(i=>`
|
|
2
|
+
<div class="card" style="border-top:4px solid ${Dn[i]}">
|
|
3
|
+
<div class="count">${t[i]}</div><div class="label">${i}</div>
|
|
4
|
+
</div>`).join(""),r=e.findings.map(i=>{let o=i.dependencyPaths[0]?.join(" \u203A ")??i.packageName,d=i.references.slice(0,3).map(c=>Sr(c)).filter(c=>c!==void 0).map(c=>`<a href="${x(c)}" target="_blank" rel="noopener">link</a>`).join(" ");return`
|
|
5
|
+
<tr>
|
|
6
|
+
<td><span class="pill" style="background:${Dn[i.severity.level]}">${i.severity.level}${i.severity.score?` ${i.severity.score}`:""}</span></td>
|
|
7
|
+
<td><code>${x(i.packageName)}@${x(i.version)}</code><div class="scope">${i.scope}${i.direct?" \xB7 direct":""}</div></td>
|
|
8
|
+
<td><a href="https://osv.dev/${x(i.id)}" target="_blank" rel="noopener">${x(i.id)}</a></td>
|
|
9
|
+
<td>${x(i.summary)}<div class="path">${x(o)}</div></td>
|
|
10
|
+
<td>${i.fixedVersions[0]?x(i.fixedVersions[0]):"\u2014"}</td>
|
|
11
|
+
<td>${d}</td>
|
|
12
|
+
</tr>`}).join(""),a=e.database.warnings.map(i=>`<div class="warn">\u26A0 ${x(i)}</div>`).join("");return`<!doctype html>
|
|
2233
13
|
<html lang="en">
|
|
2234
14
|
<head>
|
|
2235
15
|
<meta charset="utf-8">
|
|
2236
16
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2237
|
-
<title>lockhawk report \u2014 ${
|
|
17
|
+
<title>lockhawk report \u2014 ${x(e.target.root.name)}</title>
|
|
18
|
+
${n}
|
|
2238
19
|
<style>
|
|
2239
|
-
:root {
|
|
2240
|
-
--bg:#070a12; --panel:#11151e; --panel-2:#141926; --elev:#171d2b;
|
|
2241
|
-
--border:#222a3a; --border-soft:#1b2230;
|
|
2242
|
-
--text:#f1f4fa; --muted:#aeb8cc; --faint:#808b9f;
|
|
2243
|
-
--accent:#ffc266; --fix:#48e0a6;
|
|
2244
|
-
--glass: rgba(255,255,255,.055); --glass-2: rgba(255,255,255,.09);
|
|
2245
|
-
--glass-line: rgba(255,255,255,.14); --glass-hi: inset 0 1px 0 rgba(255,255,255,.10);
|
|
2246
|
-
--glass-dark: rgba(8,11,18,.42);
|
|
2247
|
-
--blur: blur(18px) saturate(160%);
|
|
2248
|
-
--mono: ui-monospace,'SF Mono','JetBrains Mono','Cascadia Code','Fira Code',Menlo,Consolas,monospace;
|
|
2249
|
-
--sans: -apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,Roboto,sans-serif;
|
|
2250
|
-
color-scheme: dark;
|
|
2251
|
-
}
|
|
20
|
+
:root { color-scheme: light dark; }
|
|
2252
21
|
* { box-sizing: border-box; }
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
background-image: linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px), linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);
|
|
2273
|
-
background-size: 34px 34px; -webkit-mask: linear-gradient(180deg,#000,transparent); mask: linear-gradient(180deg,#000,transparent); }
|
|
2274
|
-
header > * { position: relative; }
|
|
2275
|
-
.brand { display:flex; align-items:center; gap:14px; }
|
|
2276
|
-
.mark { width:38px; height:38px; flex:none; filter: drop-shadow(0 3px 12px rgba(255,93,103,.3)); }
|
|
2277
|
-
.wordmark { font-family: var(--mono); font-size: clamp(22px,4vw,27px); letter-spacing:-.02em; display:flex; align-items:center; }
|
|
2278
|
-
.wordmark .lock { color: var(--muted); font-weight:500; }
|
|
2279
|
-
.wordmark .hawk { color: var(--text); font-weight:700; }
|
|
2280
|
-
.caret { display:inline-block; width:9px; height:1em; margin-left:5px; background: var(--accent); border-radius:1px; box-shadow:0 0 12px var(--accent); animation: blink 1.2s steps(1) infinite; }
|
|
2281
|
-
.meta { margin-top:12px; color: var(--muted); font:12.5px/1.7 var(--mono); letter-spacing:.01em; overflow-wrap:anywhere; }
|
|
2282
|
-
.meta b { color: var(--text); font-weight:600; }
|
|
2283
|
-
|
|
2284
|
-
/* ---- Layout ----------------------------------------------------------- */
|
|
2285
|
-
main { max-width: 1120px; margin: 0 auto; padding: 26px clamp(16px,4vw,32px) 48px; }
|
|
2286
|
-
.cards { display:grid; grid-template-columns: repeat(auto-fit, minmax(116px,1fr)); gap:14px; }
|
|
2287
|
-
.card { position:relative; overflow:hidden; background: linear-gradient(180deg, var(--glass-2), var(--glass)); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border:1px solid var(--glass-line); border-radius:16px; padding:16px 18px; box-shadow: 0 12px 36px -18px rgba(0,0,0,.75), var(--glass-hi); animation: rise .55s both cubic-bezier(.2,.7,.2,1); animation-delay: calc(var(--i)*55ms); }
|
|
2288
|
-
.card::before { content:''; position:absolute; top:0; left:0; right:0; height:3px; background:var(--c); box-shadow:0 0 16px var(--c); }
|
|
2289
|
-
.card .count { font:700 32px/1 var(--mono); font-variant-numeric: tabular-nums; letter-spacing:-.03em; color:var(--c); text-shadow: 0 0 24px color-mix(in srgb, var(--c) 45%, transparent); }
|
|
2290
|
-
.card .label { margin-top:7px; color:var(--muted); font-size:11.5px; text-transform:uppercase; letter-spacing:.09em; }
|
|
2291
|
-
.card.zero .count { color: var(--faint); text-shadow:none; }
|
|
2292
|
-
.card.zero::before { box-shadow:none; opacity:.45; }
|
|
2293
|
-
|
|
2294
|
-
.warn { display:flex; gap:9px; align-items:flex-start; background: rgba(255,210,63,.1); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border:1px solid rgba(255,210,63,.32); color:#ffe6a6; padding:10px 14px; border-radius:12px; margin-top:14px; font-size:13px; overflow-wrap:anywhere; }
|
|
2295
|
-
.warn-i { color: var(--accent); }
|
|
2296
|
-
|
|
2297
|
-
.results-meta { display:flex; gap:14px; flex-wrap:wrap; align-items:center; margin:26px 2px 0; color:var(--muted); font:12px/1 var(--mono); letter-spacing:.04em; }
|
|
2298
|
-
.results-meta b { color:var(--text); font-weight:600; }
|
|
2299
|
-
.results-meta .sep { color:var(--faint); }
|
|
2300
|
-
|
|
2301
|
-
/* ---- Table ------------------------------------------------------------ */
|
|
2302
|
-
.table-wrap { margin-top:14px; border:1px solid var(--glass-line); border-radius:18px; overflow:hidden; background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.025)); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); box-shadow: 0 22px 60px -32px rgba(0,0,0,.85), var(--glass-hi); }
|
|
2303
|
-
.scroll { overflow-x:auto; -webkit-overflow-scrolling:touch; }
|
|
2304
|
-
table { width:100%; border-collapse:collapse; }
|
|
2305
|
-
thead th { text-align:left; background: rgba(255,255,255,.05); color:var(--muted); font:600 11px/1 var(--mono); text-transform:uppercase; letter-spacing:.08em; padding:14px 16px; border-bottom:1px solid var(--glass-line); white-space:nowrap; }
|
|
2306
|
-
tbody td { padding:15px 16px; border-bottom:1px solid rgba(255,255,255,.07); vertical-align:top; font-size:13.5px; overflow-wrap:anywhere; word-break:break-word; }
|
|
2307
|
-
tbody tr.main { animation: rise .5s both ease; animation-delay: calc(var(--i)*30ms + 120ms); }
|
|
2308
|
-
tbody tr.main:hover { background: rgba(255,255,255,.05); }
|
|
2309
|
-
|
|
2310
|
-
.pill { display:inline-flex; align-items:center; gap:7px; font:600 11.5px/1 var(--mono); padding:6px 10px; border-radius:8px; white-space:nowrap; text-transform:capitalize; color:var(--c); background: color-mix(in srgb, var(--c) 15%, transparent); border:1px solid color-mix(in srgb, var(--c) 34%, transparent); }
|
|
2311
|
-
.pill::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--c); box-shadow:0 0 9px var(--c); flex:none; }
|
|
2312
|
-
|
|
2313
|
-
.pkg { display:inline-block; font-family:var(--mono); font-size:12.5px; color:var(--text); background: rgba(255,255,255,.07); border:1px solid var(--glass-line); padding:4px 9px; border-radius:8px; }
|
|
2314
|
-
.scope { margin-top:7px; color:var(--faint); font:11px/1 var(--mono); text-transform:uppercase; letter-spacing:.06em; }
|
|
2315
|
-
.advid { font-family:var(--mono); font-size:12.5px; color:var(--accent); text-decoration:none; border-bottom:1px solid transparent; }
|
|
2316
|
-
.advid:hover { border-bottom-color: color-mix(in srgb, var(--accent) 60%, transparent); }
|
|
2317
|
-
.summary { color:var(--text); }
|
|
2318
|
-
.path { margin-top:7px; color:var(--faint); font:11.5px/1.5 var(--mono); }
|
|
2319
|
-
|
|
2320
|
-
.fixchip { display:inline-block; font-family:var(--mono); font-size:12.5px; color:var(--fix); background: color-mix(in srgb, var(--fix) 13%, transparent); border:1px solid color-mix(in srgb, var(--fix) 30%, transparent); padding:4px 10px; border-radius:8px; cursor:copy; white-space:nowrap; user-select:all; -webkit-user-select:all; transition: background .15s ease; }
|
|
2321
|
-
.fixchip:hover { background: color-mix(in srgb, var(--fix) 22%, transparent); }
|
|
2322
|
-
.nofix { color:var(--faint); }
|
|
2323
|
-
.refs { white-space:nowrap; }
|
|
2324
|
-
.ref { display:inline-block; font:11.5px/1 var(--mono); color:var(--muted); text-decoration:none; padding:4px 8px; border:1px solid var(--glass-line); border-radius:7px; margin:0 4px 4px 0; background: rgba(255,255,255,.04); transition: color .15s, border-color .15s, background .15s; }
|
|
2325
|
-
.ref:hover { color:var(--text); border-color: rgba(255,255,255,.28); background: rgba(255,255,255,.08); }
|
|
2326
|
-
|
|
2327
|
-
/* ---- Disclosure / advisory ------------------------------------------- */
|
|
2328
|
-
.detail-row td { padding:0 16px 4px; background: rgba(0,0,0,.16); border-bottom:1px solid rgba(255,255,255,.07); }
|
|
2329
|
-
details { margin:0; }
|
|
2330
|
-
summary { list-style:none; cursor:pointer; display:inline-flex; align-items:center; gap:9px; color:var(--muted); font:600 11px/1 var(--mono); letter-spacing:.08em; text-transform:uppercase; padding:13px 0; user-select:none; transition: color .15s ease; }
|
|
2331
|
-
summary::-webkit-details-marker { display:none; }
|
|
2332
|
-
summary:hover { color:var(--text); }
|
|
2333
|
-
summary .chev { color:var(--accent); transition: transform .25s cubic-bezier(.2,.7,.2,1); flex:none; }
|
|
2334
|
-
details[open] summary .chev { transform: rotate(90deg); }
|
|
2335
|
-
.detail-body { padding-bottom:18px; animation: fade .3s both ease; }
|
|
2336
|
-
|
|
2337
|
-
.fix { border:1px solid color-mix(in srgb, var(--fix) 28%, transparent); background: linear-gradient(180deg, color-mix(in srgb, var(--fix) 12%, transparent), rgba(255,255,255,.02)); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); border-radius:13px; padding:13px 15px; margin:12px 0; box-shadow: var(--glass-hi); }
|
|
2338
|
-
.fix-label { display:flex; align-items:center; gap:7px; font:700 10px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--fix); margin-bottom:9px; }
|
|
2339
|
-
.fix-label .spark { color:var(--fix); }
|
|
2340
|
-
.fix-pending { border-color: color-mix(in srgb, var(--accent) 32%, transparent); background: linear-gradient(180deg, color-mix(in srgb, var(--accent) 11%, transparent), rgba(255,255,255,.02)); }
|
|
2341
|
-
.fix-pending .fix-label, .fix-pending .spark { color: var(--accent); }
|
|
2342
|
-
.fix-text { margin:0; color:var(--text); font-size:13px; }
|
|
2343
|
-
.fix-text + .cmdbar { margin-top:11px; }
|
|
2344
|
-
.cmdbar { display:flex; align-items:center; gap:14px; flex-wrap:wrap; background: rgba(4,7,12,.55); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); border:1px solid var(--glass-line); border-radius:11px; padding:11px 14px; transition: border-color .18s ease, background .18s ease; }
|
|
2345
|
-
.cmdbar:hover { border-color: color-mix(in srgb, var(--fix) 55%, transparent); background: rgba(4,7,12,.68); }
|
|
2346
|
-
.cmdbar .cmd { font-family:var(--mono); font-size:13px; color:#d7f7e8; cursor:copy; white-space:pre-wrap; word-break:break-all; user-select:all; -webkit-user-select:all; }
|
|
2347
|
-
.cmdbar .cmd::before { content:'$\\00a0\\00a0'; color:var(--fix); user-select:none; -webkit-user-select:none; }
|
|
2348
|
-
.cmdbar .hint { margin-left:auto; font:11px/1 var(--sans); color:var(--muted); white-space:nowrap; }
|
|
2349
|
-
|
|
2350
|
-
.advisory-wrap { border:1px solid var(--glass-line); border-radius:13px; overflow:hidden; background: var(--glass-dark); -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); box-shadow: var(--glass-hi); }
|
|
2351
|
-
.advisory-head { display:flex; align-items:center; gap:11px; padding:10px 15px; background: rgba(255,255,255,.05); border-bottom:1px solid var(--glass-line); font:600 10.5px/1 var(--mono); letter-spacing:.07em; text-transform:uppercase; color:var(--muted); }
|
|
2352
|
-
.dots { display:flex; gap:6px; }
|
|
2353
|
-
.dots i { width:9px; height:9px; border-radius:50%; background: rgba(255,255,255,.18); }
|
|
2354
|
-
pre.advisory { margin:0; padding:15px 16px; white-space:pre-wrap; word-break:break-word; overflow-x:auto; max-width:100%; font-family:var(--mono); font-size:12.5px; line-height:1.65; color:#d2dae8; }
|
|
2355
|
-
|
|
2356
|
-
.empty { background: linear-gradient(180deg, var(--glass-2), var(--glass)); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border:1px solid var(--glass-line); border-radius:18px; padding:48px; text-align:center; color:var(--fix); font:600 19px/1.4 var(--sans); margin-top:16px; box-shadow: var(--glass-hi); }
|
|
2357
|
-
footer { max-width:1120px; margin:0 auto; padding:18px clamp(16px,4vw,32px) 44px; color:var(--faint); font:12px/1.6 var(--mono); letter-spacing:.02em; }
|
|
2358
|
-
|
|
2359
|
-
/* Entrance animates position only \u2014 content stays fully opaque/visible throughout. */
|
|
2360
|
-
@keyframes rise { from { transform: translateY(12px); } to { transform: none; } }
|
|
2361
|
-
@keyframes fade { from { opacity:0; } to { opacity:1; } }
|
|
2362
|
-
@keyframes blink { 50% { opacity:0; } }
|
|
2363
|
-
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation:none !important; transition:none !important; } }
|
|
2364
|
-
|
|
2365
|
-
@media print {
|
|
2366
|
-
body { background:#fff; color:#111; }
|
|
2367
|
-
header, .card, .table-wrap, .advisory-wrap, .cmdbar, .fix, thead th, .warn, .empty {
|
|
2368
|
-
background:#fff !important; -webkit-backdrop-filter:none !important; backdrop-filter:none !important;
|
|
2369
|
-
box-shadow:none !important; border-color:#ddd !important; color:#111;
|
|
2370
|
-
}
|
|
2371
|
-
.caret { display:none; }
|
|
2372
|
-
pre.advisory, .fix-text, .summary, .pkg { color:#111 !important; }
|
|
2373
|
-
}
|
|
22
|
+
body { font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f6f7f9; color: #1c1e21; }
|
|
23
|
+
header { background: #11161d; color: #fff; padding: 24px 32px; }
|
|
24
|
+
header h1 { margin: 0 0 4px; font-size: 20px; }
|
|
25
|
+
header .meta { color: #9aa4b2; font-size: 13px; }
|
|
26
|
+
main { max-width: 1100px; margin: 0 auto; padding: 24px 32px; }
|
|
27
|
+
.cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 8px; }
|
|
28
|
+
.card { background: #fff; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
29
|
+
.card .count { font-size: 28px; font-weight: 700; }
|
|
30
|
+
.card .label { text-transform: capitalize; color: #6b7280; font-size: 13px; }
|
|
31
|
+
.warn { background: #fff7e6; border: 1px solid #ffd591; color: #874d00; padding: 8px 12px; border-radius: 6px; margin: 12px 0; font-size: 13px; }
|
|
32
|
+
table { width: 100%; border-collapse: collapse; background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.08); margin-top: 16px; }
|
|
33
|
+
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #eef0f3; vertical-align: top; font-size: 13px; }
|
|
34
|
+
th { background: #f0f2f5; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: #6b7280; }
|
|
35
|
+
.pill { color: #fff; padding: 2px 8px; border-radius: 999px; font-size: 12px; text-transform: capitalize; white-space: nowrap; }
|
|
36
|
+
.scope, .path { color: #6b7280; font-size: 12px; }
|
|
37
|
+
.path { margin-top: 4px; }
|
|
38
|
+
code { background: #eef0f3; padding: 1px 5px; border-radius: 4px; }
|
|
39
|
+
.empty { background: #fff; border-radius: 8px; padding: 40px; text-align: center; color: #3a9e54; font-size: 18px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
|
|
40
|
+
footer { max-width: 1100px; margin: 0 auto; padding: 16px 32px 40px; color: #9aa4b2; font-size: 12px; }
|
|
2374
41
|
</style>
|
|
2375
42
|
</head>
|
|
2376
43
|
<body>
|
|
2377
44
|
<header>
|
|
2378
|
-
<
|
|
2379
|
-
<div class="meta"
|
|
2380
|
-
result.scannedAt
|
|
2381
|
-
)}</div>
|
|
45
|
+
<h1>lockhawk report</h1>
|
|
46
|
+
<div class="meta">${x(e.target.root.name)}${e.target.root.version?`@${x(e.target.root.version)}`:""} \xB7 ${x(e.target.manager)} \xB7 ${e.stats.totalPackages} packages \xB7 scanned ${x(e.scannedAt)}</div>
|
|
2382
47
|
</header>
|
|
2383
48
|
<main>
|
|
2384
|
-
<div class="cards">${
|
|
2385
|
-
${
|
|
2386
|
-
${
|
|
2387
|
-
<div class="table-wrap"><div class="scroll"><table>
|
|
49
|
+
<div class="cards">${s}</div>
|
|
50
|
+
${a}
|
|
51
|
+
${e.findings.length===0?'<div class="empty">\u2713 No known vulnerabilities found.</div>':`<table>
|
|
2388
52
|
<thead><tr><th>Severity</th><th>Package</th><th>Advisory</th><th>Summary & path</th><th>Fixed in</th><th>Refs</th></tr></thead>
|
|
2389
|
-
<tbody>${
|
|
2390
|
-
</table
|
|
53
|
+
<tbody>${r}</tbody>
|
|
54
|
+
</table>`}
|
|
2391
55
|
</main>
|
|
2392
|
-
<footer>Generated by lockhawk v${
|
|
2393
|
-
result.database.source
|
|
2394
|
-
)}</footer>
|
|
56
|
+
<footer>Generated by lockhawk v${x(e.tool.version)} \xB7 data from OSV.dev \xB7 database: ${x(e.database.source)}</footer>
|
|
2395
57
|
</body>
|
|
2396
|
-
</html
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
/[&<>"']/g,
|
|
2403
|
-
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
2404
|
-
);
|
|
2405
|
-
}
|
|
2406
|
-
function failureBody(f) {
|
|
2407
|
-
const lines = [
|
|
2408
|
-
`Package: ${f.packageName}@${f.version} (${f.scope}${f.direct ? ", direct" : ", transitive"})`,
|
|
2409
|
-
`Severity: ${f.severity.level}${f.severity.score !== void 0 ? `, CVSS ${f.severity.score}` : ""}${f.severity.vector ? ` (${f.severity.vector})` : ""}`,
|
|
2410
|
-
f.fixedVersions.length ? `Fixed in: ${f.fixedVersions.join(", ")}` : "Fix: none available yet",
|
|
2411
|
-
f.dependencyPaths[0] ? `Dependency path: ${f.dependencyPaths[0].join(" > ")}` : "",
|
|
2412
|
-
f.recommendation ? `Recommendation: ${f.recommendation}` : "",
|
|
2413
|
-
f.references.length ? `References:
|
|
2414
|
-
${f.references.join("\n")}` : ""
|
|
2415
|
-
];
|
|
2416
|
-
return lines.filter(Boolean).join("\n");
|
|
2417
|
-
}
|
|
2418
|
-
function toJunit(result) {
|
|
2419
|
-
const { findings } = result;
|
|
2420
|
-
const failures = findings.length;
|
|
2421
|
-
const tests = findings.length === 0 ? 1 : findings.length;
|
|
2422
|
-
const time = ((result.stats.durationMs ?? 0) / 1e3).toFixed(3);
|
|
2423
|
-
const suiteName = `${result.target.root.name} npm dependency vulnerabilities`;
|
|
2424
|
-
const cases = findings.length === 0 ? [' <testcase name="No known vulnerabilities" classname="lockhawk" time="0" />'] : findings.map((f) => {
|
|
2425
|
-
const name = `${f.packageName}@${f.version}: ${f.id} (${f.severity.level})`;
|
|
2426
|
-
const message = `${f.severity.level.toUpperCase()}${f.severity.score !== void 0 ? ` ${f.severity.score}` : ""} ${f.id}: ${f.summary}`;
|
|
2427
|
-
return [
|
|
2428
|
-
` <testcase name="${xml(name)}" classname="lockhawk.${xml(f.severity.level)}" time="0">`,
|
|
2429
|
-
` <failure message="${xml(message)}" type="${xml(f.severity.level)}">${xml(failureBody(f))}</failure>`,
|
|
2430
|
-
` </testcase>`
|
|
2431
|
-
].join("\n");
|
|
2432
|
-
});
|
|
2433
|
-
return [
|
|
2434
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
2435
|
-
`<testsuites name="lockhawk" tests="${tests}" failures="${failures}" errors="0" time="${time}">`,
|
|
2436
|
-
` <testsuite name="${xml(suiteName)}" tests="${tests}" failures="${failures}" errors="0" time="${time}">`,
|
|
2437
|
-
...cases,
|
|
2438
|
-
" </testsuite>",
|
|
2439
|
-
"</testsuites>",
|
|
2440
|
-
""
|
|
2441
|
-
].join("\n");
|
|
2442
|
-
}
|
|
2443
|
-
export {
|
|
2444
|
-
AutoSource,
|
|
2445
|
-
LockfileError,
|
|
2446
|
-
OfflineDbMissingError,
|
|
2447
|
-
OfflineSource,
|
|
2448
|
-
OnlineSource,
|
|
2449
|
-
OsvClient,
|
|
2450
|
-
OsvDatabase,
|
|
2451
|
-
SEVERITY_RANK,
|
|
2452
|
-
SHARD_BUCKETS,
|
|
2453
|
-
TOOL,
|
|
2454
|
-
buildDependencyGraph,
|
|
2455
|
-
buildFindings,
|
|
2456
|
-
canonicalId,
|
|
2457
|
-
createSource,
|
|
2458
|
-
dedupeVulnerabilities,
|
|
2459
|
-
detectLockfile,
|
|
2460
|
-
isVersionAffected,
|
|
2461
|
-
levelFromLabel,
|
|
2462
|
-
levelFromScore,
|
|
2463
|
-
loadAdvisoriesForPackages,
|
|
2464
|
-
loadDependencyGraph,
|
|
2465
|
-
nearestFix,
|
|
2466
|
-
offlineDbDir,
|
|
2467
|
-
offlineMetaPath,
|
|
2468
|
-
parseLockfile,
|
|
2469
|
-
pkgKey,
|
|
2470
|
-
readOfflineMeta,
|
|
2471
|
-
readRootManifest,
|
|
2472
|
-
resolveCacheDir,
|
|
2473
|
-
resolveSeverity,
|
|
2474
|
-
rootLabel,
|
|
2475
|
-
scan,
|
|
2476
|
-
scoreFromVector,
|
|
2477
|
-
severityAtLeast,
|
|
2478
|
-
shardBucket,
|
|
2479
|
-
shortestPath,
|
|
2480
|
-
shouldFail,
|
|
2481
|
-
toHtml,
|
|
2482
|
-
toJson,
|
|
2483
|
-
toJunit,
|
|
2484
|
-
toSarif,
|
|
2485
|
-
toSarifString,
|
|
2486
|
-
uniqueScannablePackages,
|
|
2487
|
-
updateOfflineDatabase,
|
|
2488
|
-
vulnerabilityAffects
|
|
2489
|
-
};
|
|
58
|
+
</html>`}function _(e){return e.replace(/[&<>"']/g,n=>({"&":"&","<":"<",">":">",'"':""","'":"'"})[n])}function wr(e){return[`Package: ${e.packageName}@${e.version} (${e.scope}${e.direct?", direct":", transitive"})`,`Severity: ${e.severity.level}${e.severity.score!==void 0?`, CVSS ${e.severity.score}`:""}${e.severity.vector?` (${e.severity.vector})`:""}`,e.fixedVersions.length?`Fixed in: ${e.fixedVersions.join(", ")}`:"Fix: none available yet",e.dependencyPaths[0]?`Dependency path: ${e.dependencyPaths[0].join(" > ")}`:"",e.recommendation?`Recommendation: ${e.recommendation}`:"",e.references.length?`References:
|
|
59
|
+
${e.references.join(`
|
|
60
|
+
`)}`:""].filter(Boolean).join(`
|
|
61
|
+
`)}function Vr(e){let{findings:n}=e,t=n.length,s=n.length===0?1:n.length,r=((e.stats.durationMs??0)/1e3).toFixed(3),a=`${e.target.root.name} npm dependency vulnerabilities`,i=n.length===0?[' <testcase name="No known vulnerabilities" classname="lockhawk" time="0" />']:n.map(o=>{let d=`${o.packageName}@${o.version}: ${o.id} (${o.severity.level})`,c=`${o.severity.level.toUpperCase()}${o.severity.score!==void 0?` ${o.severity.score}`:""} ${o.id}: ${o.summary}`;return[` <testcase name="${_(d)}" classname="lockhawk.${_(o.severity.level)}" time="0">`,` <failure message="${_(c)}" type="${_(o.severity.level)}">${_(wr(o))}</failure>`," </testcase>"].join(`
|
|
62
|
+
`)});return['<?xml version="1.0" encoding="UTF-8"?>',`<testsuites name="lockhawk" tests="${s}" failures="${t}" errors="0" time="${r}">`,` <testsuite name="${_(a)}" tests="${s}" failures="${t}" errors="0" time="${r}">`,...i," </testsuite>","</testsuites>",""].join(`
|
|
63
|
+
`)}export{ee as AutoSource,Ce as DATA_MARKER,E as LockfileError,H as OfflineDbMissingError,j as OfflineSource,G as OnlineSource,U as OsvClient,xe as OsvDatabase,N as SEVERITY_RANK,Rn as SHARD_BUCKETS,Nn as TOOL,ue as buildDependencyGraph,Se as buildFindings,Ze as canonicalId,Me as createSource,ve as dedupeVulnerabilities,F as detectLockfile,Je as isVersionAffected,hn as levelFromLabel,ke as levelFromScore,Ne as loadAdvisoriesForPackages,le as loadDependencyGraph,ye as nearestFix,T as offlineDbDir,K as offlineMetaPath,We as parseLockfile,w as pkgKey,L as readOfflineMeta,M as readRootManifest,Ve as resolveCacheDir,Re as resolveSeverity,pe as rootLabel,dr as scan,bn as scoreFromVector,qe as severityAtLeast,Z as shardBucket,ge as shortestPath,pr as shouldFail,Rr as toHtml,gr as toJson,Vr as toJunit,Cn as toSarif,br as toSarifString,Mn as uniqueScannablePackages,sr as updateOfflineDatabase,W as vulnerabilityAffects};
|
|
2490
64
|
//# sourceMappingURL=index.js.map
|