@lockhawk/core 0.2.2 → 0.2.4

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
@@ -543,16 +543,20 @@ declare function toSarif(result: ScanResult): SarifLog;
543
543
  /** Stringified SARIF log. */
544
544
  declare function toSarifString(result: ScanResult): string;
545
545
 
546
- /** Placeholder the report UI shell embeds so we can inject scan data at scan time. */
547
- declare const DATA_MARKER = "<!--LOCKHAWK_DATA-->";
548
546
  /**
549
- * Produce a self-contained HTML report. When given the built report-UI `shell`
550
- * (M6), the scan data is injected into it; otherwise a dependency-free fallback
551
- * template is rendered so `--format html` always works.
547
+ * Produce a self-contained HTML report for a scan result.
548
+ *
549
+ * The page is fully server-rendered: no client-side JavaScript, no embedded
550
+ * data blob, no external resources (fonts included). Every dynamic value is
551
+ * HTML-escaped, the layout flows within a width-constrained container, and long
552
+ * advisory text lives in a collapsed, contained block — so the report cannot
553
+ * overflow the viewport or render attacker-controlled advisory text as live
554
+ * markup. Copy affordances use CSS `user-select: all` (one click selects a whole
555
+ * command/version) rather than a clipboard script, preserving the no-JS model.
552
556
  */
553
- declare function toHtml(result: ScanResult, shell?: string): string;
557
+ declare function toHtml(result: ScanResult): string;
554
558
 
555
559
  /** Render a scan result as JUnit XML (one failed test per finding). */
556
560
  declare function toJunit(result: ScanResult): string;
557
561
 
558
- export { AutoSource, type CvssResult, DATA_MARKER, type DatabaseInfo, type DepNode, type DepScope, type DependencyGraph, type DetectedLockfile, type Finding, type FindingsOptions, LockfileError, OfflineDbMissingError, type OfflineMeta, OfflineSource, type OnlineClientOptions, OnlineSource, type OsvAffected, OsvClient, OsvDatabase, type OsvEvent, type OsvRange, type OsvReference, type OsvSeverity, type OsvVulnerability, type PackageManager, type PackageRef, type PkgKey, type RawGraph, type RawNode, type ResolvedSource, type RootManifest, SEVERITY_RANK, SHARD_BUCKETS, type SarifLog, type ScanOptions, type ScanResult, type ScanStats, type ScanSummary, type Severity, type SeverityInfo, type SourceMode, type SourceOptions, TOOL, type UnscannablePackage, type UpdateResult, type VulnMatch, type VulnSource, buildDependencyGraph, buildFindings, canonicalId, createSource, dedupeVulnerabilities, detectLockfile, isVersionAffected, levelFromLabel, levelFromScore, loadAdvisoriesForPackages, loadDependencyGraph, nearestFix, offlineDbDir, offlineMetaPath, parseLockfile, pkgKey, readOfflineMeta, readRootManifest, resolveCacheDir, resolveSeverity, rootLabel, scan, scoreFromVector, severityAtLeast, shardBucket, shortestPath, shouldFail, toHtml, toJson, toJunit, toSarif, toSarifString, uniqueScannablePackages, updateOfflineDatabase, vulnerabilityAffects };
562
+ export { AutoSource, type CvssResult, type DatabaseInfo, type DepNode, type DepScope, type DependencyGraph, type DetectedLockfile, type Finding, type FindingsOptions, LockfileError, OfflineDbMissingError, type OfflineMeta, OfflineSource, type OnlineClientOptions, OnlineSource, type OsvAffected, OsvClient, OsvDatabase, type OsvEvent, type OsvRange, type OsvReference, type OsvSeverity, type OsvVulnerability, type PackageManager, type PackageRef, type PkgKey, type RawGraph, type RawNode, type ResolvedSource, type RootManifest, SEVERITY_RANK, SHARD_BUCKETS, type SarifLog, type ScanOptions, type ScanResult, type ScanStats, type ScanSummary, type Severity, type SeverityInfo, type SourceMode, type SourceOptions, TOOL, type UnscannablePackage, type UpdateResult, type VulnMatch, type VulnSource, buildDependencyGraph, buildFindings, canonicalId, createSource, dedupeVulnerabilities, detectLockfile, isVersionAffected, levelFromLabel, levelFromScore, loadAdvisoriesForPackages, loadDependencyGraph, nearestFix, offlineDbDir, offlineMetaPath, parseLockfile, pkgKey, readOfflineMeta, readRootManifest, resolveCacheDir, resolveSeverity, rootLabel, scan, scoreFromVector, severityAtLeast, shardBucket, shortestPath, shouldFail, toHtml, toJson, toJunit, toSarif, toSarifString, uniqueScannablePackages, updateOfflineDatabase, vulnerabilityAffects };
package/dist/index.js CHANGED
@@ -2143,26 +2143,16 @@ function toSarifString(result) {
2143
2143
  }
2144
2144
 
2145
2145
  // src/report/html.ts
2146
- var DATA_MARKER = "<!--LOCKHAWK_DATA-->";
2147
- function embed(result) {
2148
- const json = JSON.stringify(result).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
2149
- return `<script>window.__SCAN_RESULT__ = ${json};</script>`;
2150
- }
2151
- function toHtml(result, shell) {
2152
- const dataScript = embed(result);
2153
- if (shell) {
2154
- if (shell.includes(DATA_MARKER)) return shell.replace(DATA_MARKER, () => dataScript);
2155
- return shell.replace("</head>", () => `${dataScript}</head>`);
2156
- }
2157
- return fallbackTemplate(result, dataScript);
2146
+ function toHtml(result) {
2147
+ return renderHtml(result);
2158
2148
  }
2159
2149
  var SEVERITY_COLORS = {
2160
- critical: "#b4232c",
2161
- high: "#d9822b",
2162
- medium: "#c9a227",
2163
- low: "#3a7bd5",
2164
- unknown: "#7a7a7a",
2165
- none: "#3a9e54"
2150
+ critical: "#ff5d67",
2151
+ high: "#ff9f43",
2152
+ medium: "#ffd23f",
2153
+ low: "#5b9dff",
2154
+ unknown: "#8b95a5",
2155
+ none: "#34d399"
2166
2156
  };
2167
2157
  function esc(text) {
2168
2158
  return text.replace(
@@ -2178,71 +2168,242 @@ function httpHref(url) {
2178
2168
  return void 0;
2179
2169
  }
2180
2170
  }
2181
- function fallbackTemplate(result, dataScript) {
2171
+ var MARK = `<svg class="mark" viewBox="0 0 48 48" fill="none" aria-hidden="true">
2172
+ <defs>
2173
+ <linearGradient id="lh-g" x1="6" y1="4" x2="40" y2="44" gradientUnits="userSpaceOnUse">
2174
+ <stop stop-color="#ffc266"/><stop offset="1" stop-color="#ff5d67"/>
2175
+ </linearGradient>
2176
+ </defs>
2177
+ <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"/>
2178
+ <path d="M14 18.5 24 15l10 3.5" stroke="url(#lh-g)" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
2179
+ <circle cx="24" cy="23" r="3.2" fill="url(#lh-g)"/>
2180
+ <path d="M24 26.2V32" stroke="url(#lh-g)" stroke-width="2.2" stroke-linecap="round"/>
2181
+ </svg>`;
2182
+ 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>`;
2183
+ function renderFix(rec) {
2184
+ const match = rec.match(/`([^`]+)`/);
2185
+ const command = match?.[1];
2186
+ const pending = /^No fixed version/i.test(rec);
2187
+ const prose = command ? rec.replace(/\s*\(e\.g\.[^)]*\)/, "") : rec;
2188
+ const bar = command ? `<div class="cmdbar"><code class="cmd" title="Click to select, then \u2318/Ctrl+C to copy">${esc(
2189
+ command
2190
+ )}</code><span class="hint">click to select</span></div>` : "";
2191
+ return `<div class="fix${pending ? " fix-pending" : ""}">
2192
+ <div class="fix-label">${CHEVRON.replace("chev", "spark")}${pending ? "No fix available yet" : "Recommended fix"}</div>
2193
+ <p class="fix-text">${esc(prose)}</p>${bar}
2194
+ </div>`;
2195
+ }
2196
+ function renderHtml(result) {
2182
2197
  const { summary } = result;
2183
2198
  const cards = ["critical", "high", "medium", "low", "unknown"].map(
2184
- (sev) => `
2185
- <div class="card" style="border-top:4px solid ${SEVERITY_COLORS[sev]}">
2199
+ (sev, i) => `
2200
+ <div class="card${summary[sev] === 0 ? " zero" : ""}" style="--c:${SEVERITY_COLORS[sev]};--i:${i}">
2186
2201
  <div class="count">${summary[sev]}</div><div class="label">${sev}</div>
2187
2202
  </div>`
2188
2203
  ).join("");
2189
- const rows = result.findings.map((f) => {
2204
+ const rows = result.findings.map((f, i) => {
2190
2205
  const path = f.dependencyPaths[0]?.join(" \u203A ") ?? f.packageName;
2191
- 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(" ");
2206
+ 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("");
2207
+ const fixed = f.fixedVersions[0] ? `<span class="fixchip" title="Click to select, then \u2318/Ctrl+C to copy">${esc(
2208
+ f.fixedVersions[0]
2209
+ )}</span>` : '<span class="nofix">\u2014</span>';
2192
2210
  return `
2193
- <tr>
2194
- <td><span class="pill" style="background:${SEVERITY_COLORS[f.severity.level]}">${f.severity.level}${f.severity.score ? ` ${f.severity.score}` : ""}</span></td>
2195
- <td><code>${esc(f.packageName)}@${esc(f.version)}</code><div class="scope">${f.scope}${f.direct ? " \xB7 direct" : ""}</div></td>
2196
- <td><a href="https://osv.dev/${esc(f.id)}" target="_blank" rel="noopener">${esc(f.id)}</a></td>
2197
- <td>${esc(f.summary)}<div class="path">${esc(path)}</div></td>
2198
- <td>${f.fixedVersions[0] ? esc(f.fixedVersions[0]) : "\u2014"}</td>
2199
- <td>${refs}</td>
2211
+ <tr class="main" style="--i:${i}">
2212
+ <td><span class="pill" style="--c:${SEVERITY_COLORS[f.severity.level]}">${esc(
2213
+ f.severity.level
2214
+ )}${f.severity.score ? ` ${f.severity.score}` : ""}</span></td>
2215
+ <td><span class="pkg">${esc(f.packageName)}@${esc(f.version)}</span><div class="scope">${esc(
2216
+ f.scope
2217
+ )}${f.direct ? " \xB7 direct" : ""}</div></td>
2218
+ <td><a class="advid" href="https://osv.dev/${encodeURIComponent(
2219
+ f.id
2220
+ )}" target="_blank" rel="noopener">${esc(f.id)}</a></td>
2221
+ <td><div class="summary">${esc(f.summary)}</div><div class="path">${esc(path)}</div></td>
2222
+ <td>${fixed}</td>
2223
+ <td class="refs">${refs}</td>
2224
+ </tr>
2225
+ <tr class="detail-row">
2226
+ <td colspan="6">
2227
+ ${f.recommendation ? renderFix(f.recommendation) : ""}
2228
+ <details>
2229
+ <summary>${CHEVRON}<span>Advisory details</span></summary>
2230
+ <div class="detail-body">
2231
+ <div class="advisory-wrap">
2232
+ <div class="advisory-head"><span class="dots"><i></i><i></i><i></i></span><span class="advisory-title">advisory \xB7 ${esc(
2233
+ f.id
2234
+ )}</span></div>
2235
+ <pre class="advisory">${esc(f.details ?? f.summary)}</pre>
2236
+ </div>
2237
+ </div>
2238
+ </details>
2239
+ </td>
2200
2240
  </tr>`;
2201
2241
  }).join("");
2202
- const dbWarnings = result.database.warnings.map((w) => `<div class="warn">\u26A0 ${esc(w)}</div>`).join("");
2242
+ const dbWarnings = result.database.warnings.map((w) => `<div class="warn"><span class="warn-i">\u26A0</span>${esc(w)}</div>`).join("");
2243
+ const resultsMeta = result.findings.length === 0 ? "" : `<div class="results-meta">
2244
+ <span><b>${summary.total}</b> findings</span><span class="sep">\xB7</span>
2245
+ <span><b>${summary.vulnerablePackages}</b> packages</span><span class="sep">\xB7</span>
2246
+ <span><b>${summary.fixable}</b> fixable</span>
2247
+ </div>`;
2203
2248
  return `<!doctype html>
2204
2249
  <html lang="en">
2205
2250
  <head>
2206
2251
  <meta charset="utf-8">
2207
2252
  <meta name="viewport" content="width=device-width, initial-scale=1">
2208
2253
  <title>lockhawk report \u2014 ${esc(result.target.root.name)}</title>
2209
- ${dataScript}
2210
2254
  <style>
2211
- :root { color-scheme: light dark; }
2255
+ :root {
2256
+ --bg:#070a12; --panel:#11151e; --panel-2:#141926; --elev:#171d2b;
2257
+ --border:#222a3a; --border-soft:#1b2230;
2258
+ --text:#f1f4fa; --muted:#aeb8cc; --faint:#808b9f;
2259
+ --accent:#ffc266; --fix:#48e0a6;
2260
+ --glass: rgba(255,255,255,.055); --glass-2: rgba(255,255,255,.09);
2261
+ --glass-line: rgba(255,255,255,.14); --glass-hi: inset 0 1px 0 rgba(255,255,255,.10);
2262
+ --glass-dark: rgba(8,11,18,.42);
2263
+ --blur: blur(18px) saturate(160%);
2264
+ --mono: ui-monospace,'SF Mono','JetBrains Mono','Cascadia Code','Fira Code',Menlo,Consolas,monospace;
2265
+ --sans: -apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',system-ui,Roboto,sans-serif;
2266
+ color-scheme: dark;
2267
+ }
2212
2268
  * { box-sizing: border-box; }
2213
- body { font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; margin: 0; background: #f6f7f9; color: #1c1e21; }
2214
- header { background: #11161d; color: #fff; padding: 24px 32px; }
2215
- header h1 { margin: 0 0 4px; font-size: 20px; }
2216
- header .meta { color: #9aa4b2; font-size: 13px; }
2217
- main { max-width: 1100px; margin: 0 auto; padding: 24px 32px; }
2218
- .cards { display: grid; grid-template-columns: repeat(5, 1fr); gap: 12px; margin-bottom: 8px; }
2219
- .card { background: #fff; border-radius: 8px; padding: 14px 16px; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
2220
- .card .count { font-size: 28px; font-weight: 700; }
2221
- .card .label { text-transform: capitalize; color: #6b7280; font-size: 13px; }
2222
- .warn { background: #fff7e6; border: 1px solid #ffd591; color: #874d00; padding: 8px 12px; border-radius: 6px; margin: 12px 0; font-size: 13px; }
2223
- 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; }
2224
- th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid #eef0f3; vertical-align: top; font-size: 13px; }
2225
- th { background: #f0f2f5; font-size: 12px; text-transform: uppercase; letter-spacing: .03em; color: #6b7280; }
2226
- .pill { color: #fff; padding: 2px 8px; border-radius: 999px; font-size: 12px; text-transform: capitalize; white-space: nowrap; }
2227
- .scope, .path { color: #6b7280; font-size: 12px; }
2228
- .path { margin-top: 4px; }
2229
- code { background: #eef0f3; padding: 1px 5px; border-radius: 4px; }
2230
- .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); }
2231
- footer { max-width: 1100px; margin: 0 auto; padding: 16px 32px 40px; color: #9aa4b2; font-size: 12px; }
2269
+ html, body { max-width: 100%; overflow-x: hidden; }
2270
+ body {
2271
+ margin: 0; min-height: 100vh; font: 15px/1.6 var(--sans); color: var(--text);
2272
+ background:
2273
+ radial-gradient(820px 620px at 8% -8%, rgba(91,157,255,.22), transparent 58%),
2274
+ radial-gradient(760px 600px at 94% -4%, rgba(255,93,103,.16), transparent 56%),
2275
+ radial-gradient(900px 760px at 80% 108%, rgba(72,224,166,.16), transparent 58%),
2276
+ radial-gradient(720px 640px at 16% 98%, rgba(255,179,71,.13), transparent 58%),
2277
+ linear-gradient(180deg, #0a1020, #070a12 60%);
2278
+ background-attachment: fixed;
2279
+ -webkit-font-smoothing: antialiased;
2280
+ }
2281
+
2282
+ /* ---- Header / wordmark ------------------------------------------------ */
2283
+ 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); }
2284
+ header::before { content:''; position:absolute; inset:0; pointer-events:none;
2285
+ background: radial-gradient(620px 220px at 10% 0%, rgba(255,194,102,.12), transparent 70%),
2286
+ radial-gradient(520px 240px at 58% -25%, rgba(255,93,103,.10), transparent 70%); }
2287
+ header::after { content:''; position:absolute; inset:0; opacity:.35; pointer-events:none;
2288
+ background-image: linear-gradient(rgba(255,255,255,.025) 1px,transparent 1px), linear-gradient(90deg,rgba(255,255,255,.025) 1px,transparent 1px);
2289
+ background-size: 34px 34px; -webkit-mask: linear-gradient(180deg,#000,transparent); mask: linear-gradient(180deg,#000,transparent); }
2290
+ header > * { position: relative; }
2291
+ .brand { display:flex; align-items:center; gap:14px; }
2292
+ .mark { width:38px; height:38px; flex:none; filter: drop-shadow(0 3px 12px rgba(255,93,103,.3)); }
2293
+ .wordmark { font-family: var(--mono); font-size: clamp(22px,4vw,27px); letter-spacing:-.02em; display:flex; align-items:center; }
2294
+ .wordmark .lock { color: var(--muted); font-weight:500; }
2295
+ .wordmark .hawk { color: var(--text); font-weight:700; }
2296
+ .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; }
2297
+ .meta { margin-top:12px; color: var(--muted); font:12.5px/1.7 var(--mono); letter-spacing:.01em; overflow-wrap:anywhere; }
2298
+ .meta b { color: var(--text); font-weight:600; }
2299
+
2300
+ /* ---- Layout ----------------------------------------------------------- */
2301
+ main { max-width: 1120px; margin: 0 auto; padding: 26px clamp(16px,4vw,32px) 48px; }
2302
+ .cards { display:grid; grid-template-columns: repeat(auto-fit, minmax(116px,1fr)); gap:14px; }
2303
+ .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); }
2304
+ .card::before { content:''; position:absolute; top:0; left:0; right:0; height:3px; background:var(--c); box-shadow:0 0 16px var(--c); }
2305
+ .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); }
2306
+ .card .label { margin-top:7px; color:var(--muted); font-size:11.5px; text-transform:uppercase; letter-spacing:.09em; }
2307
+ .card.zero .count { color: var(--faint); text-shadow:none; }
2308
+ .card.zero::before { box-shadow:none; opacity:.45; }
2309
+
2310
+ .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; }
2311
+ .warn-i { color: var(--accent); }
2312
+
2313
+ .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; }
2314
+ .results-meta b { color:var(--text); font-weight:600; }
2315
+ .results-meta .sep { color:var(--faint); }
2316
+
2317
+ /* ---- Table ------------------------------------------------------------ */
2318
+ .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); }
2319
+ .scroll { overflow-x:auto; -webkit-overflow-scrolling:touch; }
2320
+ table { width:100%; border-collapse:collapse; }
2321
+ 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; }
2322
+ 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; }
2323
+ tbody tr.main { animation: rise .5s both ease; animation-delay: calc(var(--i)*30ms + 120ms); }
2324
+ tbody tr.main:hover { background: rgba(255,255,255,.05); }
2325
+
2326
+ .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); }
2327
+ .pill::before { content:''; width:7px; height:7px; border-radius:50%; background:var(--c); box-shadow:0 0 9px var(--c); flex:none; }
2328
+
2329
+ .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; }
2330
+ .scope { margin-top:7px; color:var(--faint); font:11px/1 var(--mono); text-transform:uppercase; letter-spacing:.06em; }
2331
+ .advid { font-family:var(--mono); font-size:12.5px; color:var(--accent); text-decoration:none; border-bottom:1px solid transparent; }
2332
+ .advid:hover { border-bottom-color: color-mix(in srgb, var(--accent) 60%, transparent); }
2333
+ .summary { color:var(--text); }
2334
+ .path { margin-top:7px; color:var(--faint); font:11.5px/1.5 var(--mono); }
2335
+
2336
+ .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; }
2337
+ .fixchip:hover { background: color-mix(in srgb, var(--fix) 22%, transparent); }
2338
+ .nofix { color:var(--faint); }
2339
+ .refs { white-space:nowrap; }
2340
+ .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; }
2341
+ .ref:hover { color:var(--text); border-color: rgba(255,255,255,.28); background: rgba(255,255,255,.08); }
2342
+
2343
+ /* ---- Disclosure / advisory ------------------------------------------- */
2344
+ .detail-row td { padding:0 16px 4px; background: rgba(0,0,0,.16); border-bottom:1px solid rgba(255,255,255,.07); }
2345
+ details { margin:0; }
2346
+ 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; }
2347
+ summary::-webkit-details-marker { display:none; }
2348
+ summary:hover { color:var(--text); }
2349
+ summary .chev { color:var(--accent); transition: transform .25s cubic-bezier(.2,.7,.2,1); flex:none; }
2350
+ details[open] summary .chev { transform: rotate(90deg); }
2351
+ .detail-body { padding-bottom:18px; animation: fade .3s both ease; }
2352
+
2353
+ .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); }
2354
+ .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; }
2355
+ .fix-label .spark { color:var(--fix); }
2356
+ .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)); }
2357
+ .fix-pending .fix-label, .fix-pending .spark { color: var(--accent); }
2358
+ .fix-text { margin:0; color:var(--text); font-size:13px; }
2359
+ .fix-text + .cmdbar { margin-top:11px; }
2360
+ .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; }
2361
+ .cmdbar:hover { border-color: color-mix(in srgb, var(--fix) 55%, transparent); background: rgba(4,7,12,.68); }
2362
+ .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; }
2363
+ .cmdbar .cmd::before { content:'$\\00a0\\00a0'; color:var(--fix); user-select:none; -webkit-user-select:none; }
2364
+ .cmdbar .hint { margin-left:auto; font:11px/1 var(--sans); color:var(--muted); white-space:nowrap; }
2365
+
2366
+ .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); }
2367
+ .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); }
2368
+ .dots { display:flex; gap:6px; }
2369
+ .dots i { width:9px; height:9px; border-radius:50%; background: rgba(255,255,255,.18); }
2370
+ 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; }
2371
+
2372
+ .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); }
2373
+ 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; }
2374
+
2375
+ /* Entrance animates position only \u2014 content stays fully opaque/visible throughout. */
2376
+ @keyframes rise { from { transform: translateY(12px); } to { transform: none; } }
2377
+ @keyframes fade { from { opacity:0; } to { opacity:1; } }
2378
+ @keyframes blink { 50% { opacity:0; } }
2379
+ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation:none !important; transition:none !important; } }
2380
+
2381
+ @media print {
2382
+ body { background:#fff; color:#111; }
2383
+ header, .card, .table-wrap, .advisory-wrap, .cmdbar, .fix, thead th, .warn, .empty {
2384
+ background:#fff !important; -webkit-backdrop-filter:none !important; backdrop-filter:none !important;
2385
+ box-shadow:none !important; border-color:#ddd !important; color:#111;
2386
+ }
2387
+ .caret { display:none; }
2388
+ pre.advisory, .fix-text, .summary, .pkg { color:#111 !important; }
2389
+ }
2232
2390
  </style>
2233
2391
  </head>
2234
2392
  <body>
2235
2393
  <header>
2236
- <h1>lockhawk report</h1>
2237
- <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(result.scannedAt)}</div>
2394
+ <div class="brand">${MARK}<div class="wordmark"><span class="lock">lock</span><span class="hawk">hawk</span><span class="caret"></span></div></div>
2395
+ <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(
2396
+ result.scannedAt
2397
+ )}</div>
2238
2398
  </header>
2239
2399
  <main>
2240
2400
  <div class="cards">${cards}</div>
2241
2401
  ${dbWarnings}
2242
- ${result.findings.length === 0 ? '<div class="empty">\u2713 No known vulnerabilities found.</div>' : `<table>
2402
+ ${result.findings.length === 0 ? '<div class="empty">\u2713 No known vulnerabilities found.</div>' : `${resultsMeta}
2403
+ <div class="table-wrap"><div class="scroll"><table>
2243
2404
  <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>
2244
2405
  <tbody>${rows}</tbody>
2245
- </table>`}
2406
+ </table></div></div>`}
2246
2407
  </main>
2247
2408
  <footer>Generated by lockhawk v${esc(result.tool.version)} \xB7 data from OSV.dev \xB7 database: ${esc(
2248
2409
  result.database.source
@@ -2297,7 +2458,6 @@ function toJunit(result) {
2297
2458
  }
2298
2459
  export {
2299
2460
  AutoSource,
2300
- DATA_MARKER,
2301
2461
  LockfileError,
2302
2462
  OfflineDbMissingError,
2303
2463
  OfflineSource,