@lockhawk/core 0.2.3 → 0.2.5
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/dist/index.d.ts +14 -9
- package/dist/index.js +212 -79
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -392,16 +392,19 @@ interface OnlineClientOptions {
|
|
|
392
392
|
retries?: number;
|
|
393
393
|
}
|
|
394
394
|
/**
|
|
395
|
-
* Online OSV source.
|
|
396
|
-
*
|
|
397
|
-
*
|
|
398
|
-
*
|
|
395
|
+
* Online OSV source. Queries the batch endpoint by package *name only* — never
|
|
396
|
+
* pinning an installed version — so OSV returns every advisory naming each
|
|
397
|
+
* package; lockhawk's own range matcher (`buildFindings` → `vulnerabilityAffects`)
|
|
398
|
+
* then decides what actually affects the installed version. Delegating the
|
|
399
|
+
* version match to OSV's server would cap coverage at OSV's matcher; doing it
|
|
400
|
+
* locally makes coverage depend only on lockhawk (and mirrors the offline path).
|
|
401
|
+
* Hydrated records are cached by id so repeat scans do no network work.
|
|
399
402
|
*/
|
|
400
403
|
declare class OsvClient {
|
|
401
404
|
private readonly opts;
|
|
402
405
|
private readonly limit;
|
|
403
406
|
constructor(opts: OnlineClientOptions);
|
|
404
|
-
/** Fetch advisories
|
|
407
|
+
/** Fetch advisories naming the given packages, keyed by package name. */
|
|
405
408
|
fetchAdvisories(packages: PackageRef[]): Promise<Map<string, OsvVulnerability[]>>;
|
|
406
409
|
private queryBatch;
|
|
407
410
|
private hydrate;
|
|
@@ -547,10 +550,12 @@ declare function toSarifString(result: ScanResult): string;
|
|
|
547
550
|
* Produce a self-contained HTML report for a scan result.
|
|
548
551
|
*
|
|
549
552
|
* The page is fully server-rendered: no client-side JavaScript, no embedded
|
|
550
|
-
* data blob, no external resources. Every dynamic value is
|
|
551
|
-
* layout flows
|
|
552
|
-
* text lives in a collapsed, contained block — so the report cannot
|
|
553
|
-
* viewport or render attacker-controlled advisory text as live
|
|
553
|
+
* data blob, no external resources (fonts included). Every dynamic value is
|
|
554
|
+
* HTML-escaped, the layout flows within a width-constrained container, and long
|
|
555
|
+
* advisory text lives in a collapsed, contained block — so the report cannot
|
|
556
|
+
* overflow the viewport or render attacker-controlled advisory text as live
|
|
557
|
+
* markup. Copy affordances use CSS `user-select: all` (one click selects a whole
|
|
558
|
+
* command/version) rather than a clipboard script, preserving the no-JS model.
|
|
554
559
|
*/
|
|
555
560
|
declare function toHtml(result: ScanResult): string;
|
|
556
561
|
|
package/dist/index.js
CHANGED
|
@@ -1508,10 +1508,10 @@ var OsvClient = class {
|
|
|
1508
1508
|
}
|
|
1509
1509
|
opts;
|
|
1510
1510
|
limit;
|
|
1511
|
-
/** Fetch advisories
|
|
1511
|
+
/** Fetch advisories naming the given packages, keyed by package name. */
|
|
1512
1512
|
async fetchAdvisories(packages) {
|
|
1513
|
-
const
|
|
1514
|
-
const idsByName = await this.queryBatch(
|
|
1513
|
+
const names = [...new Set(packages.map((p) => p.name))];
|
|
1514
|
+
const idsByName = await this.queryBatch(names);
|
|
1515
1515
|
const allIds = /* @__PURE__ */ new Set();
|
|
1516
1516
|
for (const ids of idsByName.values()) for (const id of ids) allIds.add(id);
|
|
1517
1517
|
const records = await this.hydrate([...allIds]);
|
|
@@ -1526,34 +1526,30 @@ var OsvClient = class {
|
|
|
1526
1526
|
}
|
|
1527
1527
|
return byName;
|
|
1528
1528
|
}
|
|
1529
|
-
async queryBatch(
|
|
1529
|
+
async queryBatch(names) {
|
|
1530
1530
|
const idsByName = /* @__PURE__ */ new Map();
|
|
1531
|
-
for (let i = 0; i <
|
|
1532
|
-
const chunk =
|
|
1531
|
+
for (let i = 0; i < names.length; i += BATCH_SIZE) {
|
|
1532
|
+
const chunk = names.slice(i, i + BATCH_SIZE);
|
|
1533
1533
|
const body = {
|
|
1534
|
-
queries: chunk.map((
|
|
1535
|
-
package: { name: p.name, ecosystem: "npm" },
|
|
1536
|
-
version: p.version
|
|
1537
|
-
}))
|
|
1534
|
+
queries: chunk.map((name) => ({ package: { name, ecosystem: "npm" } }))
|
|
1538
1535
|
};
|
|
1539
1536
|
const data = await this.request(`${OSV_API}/querybatch`, body);
|
|
1540
1537
|
for (let j = 0; j < chunk.length; j++) {
|
|
1541
|
-
const
|
|
1538
|
+
const name = chunk[j];
|
|
1542
1539
|
const result = data.results[j];
|
|
1543
1540
|
if (!result) continue;
|
|
1544
|
-
const set = idsByName.get(
|
|
1541
|
+
const set = idsByName.get(name) ?? /* @__PURE__ */ new Set();
|
|
1545
1542
|
for (const vuln of result.vulns ?? []) set.add(vuln.id);
|
|
1546
1543
|
let pageToken = result.next_page_token;
|
|
1547
1544
|
while (pageToken) {
|
|
1548
1545
|
const page = await this.request(`${OSV_API}/query`, {
|
|
1549
|
-
package: { name
|
|
1550
|
-
version: pkg.version,
|
|
1546
|
+
package: { name, ecosystem: "npm" },
|
|
1551
1547
|
page_token: pageToken
|
|
1552
1548
|
});
|
|
1553
1549
|
for (const vuln of page.vulns ?? []) set.add(vuln.id);
|
|
1554
1550
|
pageToken = page.next_page_token;
|
|
1555
1551
|
}
|
|
1556
|
-
if (set.size) idsByName.set(
|
|
1552
|
+
if (set.size) idsByName.set(name, set);
|
|
1557
1553
|
}
|
|
1558
1554
|
}
|
|
1559
1555
|
return idsByName;
|
|
@@ -1618,18 +1614,6 @@ var OsvClient = class {
|
|
|
1618
1614
|
}
|
|
1619
1615
|
}
|
|
1620
1616
|
};
|
|
1621
|
-
function dedupePackages(packages) {
|
|
1622
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1623
|
-
const out = [];
|
|
1624
|
-
for (const p of packages) {
|
|
1625
|
-
const key = `${p.name}@${p.version}`;
|
|
1626
|
-
if (!seen.has(key)) {
|
|
1627
|
-
seen.add(key);
|
|
1628
|
-
out.push(p);
|
|
1629
|
-
}
|
|
1630
|
-
}
|
|
1631
|
-
return out;
|
|
1632
|
-
}
|
|
1633
1617
|
|
|
1634
1618
|
// src/osv/offline-db.ts
|
|
1635
1619
|
import { createWriteStream } from "fs";
|
|
@@ -2147,12 +2131,12 @@ function toHtml(result) {
|
|
|
2147
2131
|
return renderHtml(result);
|
|
2148
2132
|
}
|
|
2149
2133
|
var SEVERITY_COLORS = {
|
|
2150
|
-
critical: "#
|
|
2151
|
-
high: "#
|
|
2152
|
-
medium: "#
|
|
2153
|
-
low: "#
|
|
2154
|
-
unknown: "#
|
|
2155
|
-
none: "#
|
|
2134
|
+
critical: "#ff5d67",
|
|
2135
|
+
high: "#ff9f43",
|
|
2136
|
+
medium: "#ffd23f",
|
|
2137
|
+
low: "#5b9dff",
|
|
2138
|
+
unknown: "#8b95a5",
|
|
2139
|
+
none: "#34d399"
|
|
2156
2140
|
};
|
|
2157
2141
|
function esc(text) {
|
|
2158
2142
|
return text.replace(
|
|
@@ -2168,41 +2152,83 @@ function httpHref(url) {
|
|
|
2168
2152
|
return void 0;
|
|
2169
2153
|
}
|
|
2170
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
|
+
}
|
|
2171
2180
|
function renderHtml(result) {
|
|
2172
2181
|
const { summary } = result;
|
|
2173
2182
|
const cards = ["critical", "high", "medium", "low", "unknown"].map(
|
|
2174
|
-
(sev) => `
|
|
2175
|
-
<div class="card"
|
|
2183
|
+
(sev, i) => `
|
|
2184
|
+
<div class="card${summary[sev] === 0 ? " zero" : ""}" style="--c:${SEVERITY_COLORS[sev]};--i:${i}">
|
|
2176
2185
|
<div class="count">${summary[sev]}</div><div class="label">${sev}</div>
|
|
2177
2186
|
</div>`
|
|
2178
2187
|
).join("");
|
|
2179
|
-
const rows = result.findings.map((f) => {
|
|
2188
|
+
const rows = result.findings.map((f, i) => {
|
|
2180
2189
|
const path = f.dependencyPaths[0]?.join(" \u203A ") ?? f.packageName;
|
|
2181
|
-
const refs = f.references.slice(0, 3).map((u) => httpHref(u)).filter((u) => u !== void 0).map((u) => `<a href="${esc(u)}" target="_blank" rel="noopener">link</a>`).join("
|
|
2182
|
-
const
|
|
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>';
|
|
2183
2194
|
return `
|
|
2184
|
-
<tr>
|
|
2185
|
-
<td><span class="pill" style="
|
|
2195
|
+
<tr class="main" style="--i:${i}">
|
|
2196
|
+
<td><span class="pill" style="--c:${SEVERITY_COLORS[f.severity.level]}">${esc(
|
|
2186
2197
|
f.severity.level
|
|
2187
2198
|
)}${f.severity.score ? ` ${f.severity.score}` : ""}</span></td>
|
|
2188
|
-
<td><
|
|
2189
|
-
|
|
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(
|
|
2190
2203
|
f.id
|
|
2191
|
-
)}</a></td>
|
|
2192
|
-
<td>${esc(f.summary)}
|
|
2193
|
-
<td>${
|
|
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>
|
|
2194
2207
|
<td class="refs">${refs}</td>
|
|
2195
2208
|
</tr>
|
|
2196
2209
|
<tr class="detail-row">
|
|
2197
2210
|
<td colspan="6">
|
|
2211
|
+
${f.recommendation ? renderFix(f.recommendation) : ""}
|
|
2198
2212
|
<details>
|
|
2199
|
-
<summary>Advisory details</summary>
|
|
2200
|
-
|
|
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>
|
|
2201
2222
|
</details>
|
|
2202
2223
|
</td>
|
|
2203
2224
|
</tr>`;
|
|
2204
2225
|
}).join("");
|
|
2205
|
-
const dbWarnings = result.database.warnings.map((w) => `<div class="warn">\u26A0
|
|
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>`;
|
|
2206
2232
|
return `<!doctype html>
|
|
2207
2233
|
<html lang="en">
|
|
2208
2234
|
<head>
|
|
@@ -2210,51 +2236,158 @@ function renderHtml(result) {
|
|
|
2210
2236
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
2211
2237
|
<title>lockhawk report \u2014 ${esc(result.target.root.name)}</title>
|
|
2212
2238
|
<style>
|
|
2213
|
-
:root {
|
|
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
|
+
}
|
|
2214
2252
|
* { box-sizing: border-box; }
|
|
2215
2253
|
html, body { max-width: 100%; overflow-x: hidden; }
|
|
2216
|
-
body {
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
.
|
|
2239
|
-
|
|
2240
|
-
.
|
|
2241
|
-
|
|
2254
|
+
body {
|
|
2255
|
+
margin: 0; min-height: 100vh; font: 15px/1.6 var(--sans); color: var(--text);
|
|
2256
|
+
background:
|
|
2257
|
+
radial-gradient(820px 620px at 8% -8%, rgba(91,157,255,.22), transparent 58%),
|
|
2258
|
+
radial-gradient(760px 600px at 94% -4%, rgba(255,93,103,.16), transparent 56%),
|
|
2259
|
+
radial-gradient(900px 760px at 80% 108%, rgba(72,224,166,.16), transparent 58%),
|
|
2260
|
+
radial-gradient(720px 640px at 16% 98%, rgba(255,179,71,.13), transparent 58%),
|
|
2261
|
+
linear-gradient(180deg, #0a1020, #070a12 60%);
|
|
2262
|
+
background-attachment: fixed;
|
|
2263
|
+
-webkit-font-smoothing: antialiased;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
/* ---- Header / wordmark ------------------------------------------------ */
|
|
2267
|
+
header { position: relative; overflow: hidden; padding: 30px clamp(18px,5vw,40px) 28px; background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(255,255,255,.03)); -webkit-backdrop-filter: var(--blur); backdrop-filter: var(--blur); border-bottom: 1px solid var(--glass-line); box-shadow: var(--glass-hi); }
|
|
2268
|
+
header::before { content:''; position:absolute; inset:0; pointer-events:none;
|
|
2269
|
+
background: radial-gradient(620px 220px at 10% 0%, rgba(255,194,102,.12), transparent 70%),
|
|
2270
|
+
radial-gradient(520px 240px at 58% -25%, rgba(255,93,103,.10), transparent 70%); }
|
|
2271
|
+
header::after { content:''; position:absolute; inset:0; opacity:.35; pointer-events:none;
|
|
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
|
+
}
|
|
2242
2374
|
</style>
|
|
2243
2375
|
</head>
|
|
2244
2376
|
<body>
|
|
2245
2377
|
<header>
|
|
2246
|
-
<
|
|
2247
|
-
<div class="meta">${esc(result.target.root.name)}${result.target.root.version ? `@${esc(result.target.root.version)}` : ""} \xB7 ${esc(result.target.manager)} \xB7 ${result.stats.totalPackages} packages \xB7 scanned ${esc(
|
|
2378
|
+
<div class="brand">${MARK}<div class="wordmark"><span class="lock">lock</span><span class="hawk">hawk</span><span class="caret"></span></div></div>
|
|
2379
|
+
<div class="meta"><b>${esc(result.target.root.name)}${result.target.root.version ? `@${esc(result.target.root.version)}` : ""}</b> \xB7 ${esc(result.target.manager)} \xB7 ${result.stats.totalPackages} packages \xB7 scanned ${esc(
|
|
2248
2380
|
result.scannedAt
|
|
2249
2381
|
)}</div>
|
|
2250
2382
|
</header>
|
|
2251
2383
|
<main>
|
|
2252
2384
|
<div class="cards">${cards}</div>
|
|
2253
2385
|
${dbWarnings}
|
|
2254
|
-
${result.findings.length === 0 ? '<div class="empty">\u2713 No known vulnerabilities found.</div>' :
|
|
2386
|
+
${result.findings.length === 0 ? '<div class="empty">\u2713 No known vulnerabilities found.</div>' : `${resultsMeta}
|
|
2387
|
+
<div class="table-wrap"><div class="scroll"><table>
|
|
2255
2388
|
<thead><tr><th>Severity</th><th>Package</th><th>Advisory</th><th>Summary & path</th><th>Fixed in</th><th>Refs</th></tr></thead>
|
|
2256
2389
|
<tbody>${rows}</tbody>
|
|
2257
|
-
</table></div>`}
|
|
2390
|
+
</table></div></div>`}
|
|
2258
2391
|
</main>
|
|
2259
2392
|
<footer>Generated by lockhawk v${esc(result.tool.version)} \xB7 data from OSV.dev \xB7 database: ${esc(
|
|
2260
2393
|
result.database.source
|