@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 CHANGED
@@ -392,16 +392,19 @@ interface OnlineClientOptions {
392
392
  retries?: number;
393
393
  }
394
394
  /**
395
- * Online OSV source. Uses the batch endpoint (which returns only matched ids)
396
- * then hydrates each unique id to a full record caching records by id so
397
- * repeat scans do no network work. All matching is re-validated downstream by
398
- * `buildFindings`, so this client only has to gather candidate records.
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 affecting the given packages, keyed by package name. */
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 HTML-escaped, the
551
- * layout flows normally within a width-constrained container, and long advisory
552
- * text lives in a collapsed, contained block — so the report cannot overflow the
553
- * viewport or render attacker-controlled advisory text as live markup.
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 affecting the given packages, keyed by package name. */
1511
+ /** Fetch advisories naming the given packages, keyed by package name. */
1512
1512
  async fetchAdvisories(packages) {
1513
- const unique = dedupePackages(packages);
1514
- const idsByName = await this.queryBatch(unique);
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(packages) {
1529
+ async queryBatch(names) {
1530
1530
  const idsByName = /* @__PURE__ */ new Map();
1531
- for (let i = 0; i < packages.length; i += BATCH_SIZE) {
1532
- const chunk = packages.slice(i, i + BATCH_SIZE);
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((p) => ({
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 pkg = chunk[j];
1538
+ const name = chunk[j];
1542
1539
  const result = data.results[j];
1543
1540
  if (!result) continue;
1544
- const set = idsByName.get(pkg.name) ?? /* @__PURE__ */ new Set();
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: pkg.name, ecosystem: "npm" },
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(pkg.name, 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: "#b4232c",
2151
- high: "#d9822b",
2152
- medium: "#c9a227",
2153
- low: "#3a7bd5",
2154
- unknown: "#7a7a7a",
2155
- none: "#3a9e54"
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" style="border-top:4px solid ${SEVERITY_COLORS[sev]}">
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 rec = f.recommendation ? `<p class="rec"><strong>Recommendation:</strong> ${esc(f.recommendation)}</p>` : "";
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="background:${SEVERITY_COLORS[f.severity.level]}">${esc(
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><code>${esc(f.packageName)}@${esc(f.version)}</code><div class="scope">${esc(f.scope)}${f.direct ? " \xB7 direct" : ""}</div></td>
2189
- <td><a href="https://osv.dev/${encodeURIComponent(f.id)}" target="_blank" rel="noopener">${esc(
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)}<div class="path">${esc(path)}</div></td>
2193
- <td>${f.fixedVersions[0] ? esc(f.fixedVersions[0]) : "\u2014"}</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
- ${rec}<pre class="advisory">${esc(f.details ?? f.summary)}</pre>
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 ${esc(w)}</div>`).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>`;
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 { color-scheme: light dark; }
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 { font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f6f7f9; color: #1c1e21; }
2217
- header { background: #11161d; color: #fff; padding: 24px 32px; }
2218
- header h1 { margin: 0 0 4px; font-size: 20px; }
2219
- header .meta { color: #9aa4b2; font-size: 13px; overflow-wrap: anywhere; }
2220
- main { max-width: 1100px; margin: 0 auto; padding: 24px clamp(16px, 4vw, 32px) 40px; }
2221
- .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: 12px; margin-bottom: 8px; }
2222
- .card { background: #fff; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
2223
- .card .count { font-size: 28px; font-weight: 700; }
2224
- .card .label { text-transform: capitalize; color: #6b7280; font-size: 13px; }
2225
- .warn { background: #fff7e6; border: 1px solid #ffd591; color: #874d00; padding: 8px 12px; border-radius: 6px; margin: 12px 0; font-size: 13px; overflow-wrap: anywhere; }
2226
- .table-wrap { margin-top: 16px; overflow-x: auto; -webkit-overflow-scrolling: touch; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
2227
- table { width: 100%; border-collapse: collapse; background: #fff; }
2228
- th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #eef0f3; vertical-align: top; font-size: 13px; overflow-wrap: anywhere; word-break: break-word; }
2229
- th { background: #f0f2f5; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: #6b7280; white-space: nowrap; }
2230
- .pill { color: #fff; padding: 2px 8px; border-radius: 999px; font-size: 12px; text-transform: capitalize; white-space: nowrap; }
2231
- .scope, .path { color: #6b7280; font-size: 12px; }
2232
- .path { margin-top: 4px; }
2233
- .refs { white-space: nowrap; }
2234
- code { background: #eef0f3; padding: 1px 5px; border-radius: 4px; max-width: 100%; }
2235
- .detail-row td { border-bottom: 2px solid #eef0f3; padding-top: 0; }
2236
- details { font-size: 13px; }
2237
- summary { cursor: pointer; color: #3a7bd5; font-size: 12px; padding: 4px 0; width: max-content; }
2238
- .rec { margin: 8px 0; font-size: 13px; }
2239
- pre.advisory { white-space: pre-wrap; word-break: break-word; overflow-x: auto; max-width: 100%; background: #f6f7f9; border: 1px solid #eef0f3; border-radius: 6px; padding: 10px 12px; margin: 6px 0 4px; font-size: 12.5px; }
2240
- .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); margin-top: 16px; }
2241
- footer { max-width: 1100px; margin: 0 auto; padding: 16px clamp(16px, 4vw, 32px) 40px; color: #9aa4b2; font-size: 12px; }
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
- <h1>lockhawk report</h1>
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>' : `<div class="table-wrap"><table>
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 &amp; 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