Skip to content

refactor: remove documentation site and implement new web dashboard w… - #52

Open
taha2samy-3 wants to merge 2 commits into
taha2samy:mainfrom
taha2samy-3:dev
Open

taha2samy-3 wants to merge 2 commits into
taha2samy:mainfrom
taha2samy-3:dev

Conversation

@taha2samy-3

@taha2samy-3 taha2samy-3 commented Aug 2, 2026

Copy link
Copy Markdown

…ith updated SBOM metadata configuration

Summary by CodeRabbit

  • New Features
    • Added a dark/light assurance dashboard with compliance status, security scorecards, validation results, vulnerability and KICS findings, benchmark charts, provenance, SBOM links, and registry instructions.
    • Added local dashboard build and serving workflows.
    • Added resilient empty states, interactive filtering, tabs, forensic logs, clipboard actions, and print/PDF layouts.
  • Documentation
    • Removed the previous documentation site and its operations, architecture, compliance, validation, and performance pages.
  • Chores
    • Updated image and repository ownership defaults.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The project replaces the MkDocs reporting site with a Node.js dashboard. It adds report aggregation, static serving, browser rendering, charts, compliance views, test inspection, fallback handling, and updated registry ownership. The previous documentation and Python reporting components are removed.

Changes

Node.js dashboard migration

Layer / File(s) Summary
Dashboard data pipeline and serving
web/builder.js, web/verify.js, server.js, package.json, web/package.json, tasks/Taskfile.doc.yml, metadata.json
The project builds and verifies web/data.json, serves dashboard assets through Node.js, and adds Node.js build and serve tasks.
Dashboard shell and orchestration
web/index.html, web/styles.css, web/assets/js/main.js, web/assets/js/fallbacks.js
The new dashboard provides themed layouts, scorecards, tabs, registry instructions, print rules, data sanitization, fallback states, and application initialization.
Charts, compliance, and test features
web/assets/js/charts.js, web/assets/js/tables.js, web/assets/js/tests.js
The dashboard renders cryptographic charts, vulnerability and KICS findings, hardening scorecards, Pytest results, and forensic logs.
Legacy reporting removal and registry updates
.gitignore, Pipfile, docker-bake.hcl, taskfile.yml, scripts/update_readme.py, scripts/upload_sbom.py, docs/..., main.py, mkdocs.yml, infra/...
MkDocs dependencies, documentation pages, Allure images, main.py, and mkdocs.yml are removed. Registry owner defaults and metadata upload paths change to taha2samy-3.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Reports
  participant Builder as web/builder.js
  participant Data as web/data.json
  participant Server as server.js
  participant Dashboard
  Reports->>Builder: Parse reports and metadata
  Builder->>Data: Write normalized dashboard data
  Dashboard->>Server: Request dashboard assets
  Server->>Dashboard: Return static files
  Dashboard->>Data: Fetch dashboard data
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 70.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: removing the documentation site and implementing a new web dashboard.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (18)
web/verify.js-17-29 (1)

17-29: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Strengthen the validation beyond project_name.

project_name always has a fallback value (process.env.PROJECT_NAME || 'Wolfi OpenSSL FIPS' in web/builder.js Line 375), so it is never falsy. This means the check at Line 21 can never fail, and this verification step always reports success, even when the build produced only default/empty fallback data (no Trivy scans, no Pytest reports, no metadata found). Validate that core sections were actually populated, for example reports, security.compliance, or packages.

🛠️ Proposed fix
   if (!data.project_name) {
     throw new Error('Missing "project_name" field');
   }
+
+  if (!Array.isArray(data.packages) || data.packages.length === 0) {
+    throw new Error('No packages discovered; build likely ran without versions.hcl data');
+  }
+  if (!data.security || !data.reports) {
+    throw new Error('Missing "security" or "reports" section in build output');
+  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/verify.js` around lines 17 - 29, Replace the ineffective
project_name-only check in the verification flow with validation that core build
data is populated, requiring at least one meaningful section such as reports,
security.compliance, or packages. Keep the existing success and error handling
in the surrounding try/catch, and reject fallback-only data before logging
success.
web/assets/js/main.js-254-264 (1)

254-264: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Both clipboard handlers assume the Clipboard API exists and neither handles a rejection. The shared root cause is duplicated clipboard logic with no availability guard and no .catch. navigator.clipboard is undefined on an insecure origin other than localhost, and this dashboard is served over plain HTTP. Accessing the page by host name or LAN IP makes every copy button raise a TypeError.

  • web/assets/js/main.js#L254-L264: add a navigator.clipboard?.writeText guard before the call and a .catch that reports the failure on the button. Extract the result into a shared helper.
  • web/assets/js/tests.js#L284-L298: replace the inline navigator.clipboard.writeText(...) call in copyBtn.onclick with the shared helper.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/main.js` around lines 254 - 264, Eliminate duplicated clipboard
handling by extracting a shared helper from copyToClipboard that guards
navigator.clipboard?.writeText and catches failures, reporting the error on the
button. Update web/assets/js/main.js lines 254-264 to implement the guarded
helper and update web/assets/js/tests.js lines 284-298 so copyBtn.onclick uses
that shared helper instead of calling navigator.clipboard.writeText inline.
web/assets/js/tables.js-498-505 (1)

498-505: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The same HTML escaper is defined three times and carries the same two defects in each copy. The shared root cause is the absence of a common utility module. Each copy uses if (!str) return '' followed by str.replace(...), so each one throws a TypeError on a numeric value from data.json and silently converts 0 and false to an empty string.

  • web/assets/js/tables.js#L498-L505: change the guard to a null and undefined check, then call String(str) before the replacements. This copy has the most call sites, so fix it first and export it from a new web/assets/js/utils.js.
  • web/assets/js/fallbacks.js#L224-L231: delete escapeFallbackHTML and import the shared escapeHTML.
  • web/assets/js/tests.js#L316-L323: delete the local escapeHTML and import the shared one.

web/assets/js/main.js has no escaper at all and needs the same import for the sinks flagged on its lines 128-152 and 216-250.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 498 - 505, The duplicated HTML escapers
have inconsistent falsy handling and fail on numeric inputs. In
web/assets/js/tables.js:498-505, update escapeHTML to return only for null or
undefined, convert non-null values with String(str), and export it from new
web/assets/js/utils.js; in web/assets/js/fallbacks.js:224-231 and
web/assets/js/tests.js:316-323, remove the local escapers and import the shared
utility; also import escapeHTML in web/assets/js/main.js for the sinks at lines
128-152 and 216-250.
web/assets/js/tables.js-39-40 (1)

39-40: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Only the first Trivy target is read. Findings from other targets are dropped.

Line 40 reads raw_scan?.Results?.[0]?.Vulnerabilities. Trivy emits one entry in Results per scanned target, for example the OS package database and each language dependency file. The table therefore shows the findings of the first target only.

renderBaseScorecards in web/assets/js/main.js line 84 reads the precomputed vulnerabilities.total. If the builder counts all targets, the headline CVE count will not match the table contents. For a compliance dashboard, a silent undercount in the detail view is a reporting defect.

🛠️ Proposed fix
     const variantData = data.security?.compliance?.[currentVariant] || {};
-    const vulnerabilities = variantData.raw_scan?.Results?.[0]?.Vulnerabilities || [];
+    const results = Array.isArray(variantData.raw_scan?.Results) ? variantData.raw_scan.Results : [];
+    const vulnerabilities = results.flatMap(r => Array.isArray(r?.Vulnerabilities) ? r.Vulnerabilities : []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 39 - 40, Update the vulnerability
extraction near variantData so it iterates over every entry in raw_scan.Results
and combines all available Vulnerabilities arrays into one collection, rather
than reading only Results[0]. Preserve the existing empty-array fallback and
ensure the resulting collection remains compatible with the table rendering and
the precomputed vulnerabilities.total count.
web/assets/js/charts.js-224-231 (1)

224-231: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

getOps throws when any signature record lacks algorithm or operation.

Array.prototype.find runs the predicate on every element until it matches. Lines 227-228 call .toUpperCase() and .toLowerCase() without a guard. sanitizeDashboardData in web/assets/js/fallbacks.js line 63 verifies only that bench_signatures_raw is an array. It does not verify the shape of the elements. One record with a missing or non-string field raises TypeError: Cannot read properties of undefined (reading 'toUpperCase').

bootstrapDashboard wraps all renderers in one try, so this single record replaces the entire dashboard with the parse-failure card.

🛡️ Proposed fix
   const getOps = (algo, op) => {
-    const record = rawSigs.find(s => 
-      s.algorithm.toUpperCase() === algo.toUpperCase() && 
-      s.operation.toLowerCase() === op.toLowerCase()
-    );
-    return record ? record.ops_per_sec : 0;
+    const record = rawSigs.find(s =>
+      typeof s?.algorithm === 'string' &&
+      typeof s?.operation === 'string' &&
+      s.algorithm.toUpperCase() === algo.toUpperCase() &&
+      s.operation.toLowerCase() === op.toLowerCase()
+    );
+    return typeof record?.ops_per_sec === 'number' ? record.ops_per_sec : 0;
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/charts.js` around lines 224 - 231, Update getOps so its
rawSigs.find predicate validates that each record’s algorithm and operation
fields are strings before calling toUpperCase or toLowerCase. Skip malformed
records and preserve the existing ops_per_sec return for valid matches, with 0
when no valid record matches.
web/assets/js/tests.js-226-230 (1)

226-230: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The badge selector stops matching after the first open. The badge then shows stale data.

Line 226 selects the badge with the class badge-accent. Line 229 overwrites className with either badge badge-success ... or badge badge-error .... Neither string contains badge-accent.

On every subsequent call, document.querySelector('#forensicLogDrawerOverlay .badge-accent') returns null. The if on line 227 is false, so lines 228-229 never run again. The drawer then displays the variant and outcome of the first inspected test for every later test. In a forensic log viewer this misattributes a pass or a fail to the wrong test.

Select the element by a stable id.

🛠️ Proposed fix

In initLogDrawerOverlay, add an id to the badge:

-          <span class="badge badge-accent text-[10px] py-0 px-1.5 font-bold mb-1">Pytest Standard Logs</span>
+          <span id="drawerVariantBadge" class="badge badge-accent text-[10px] py-0 px-1.5 font-bold mb-1">Pytest Standard Logs</span>

Then here:

-  const variantBadge = document.querySelector('`#forensicLogDrawerOverlay` .badge-accent');
+  const variantBadge = document.getElementById('drawerVariantBadge');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tests.js` around lines 226 - 230, Update initLogDrawerOverlay
to assign a stable id to the variant badge, then change the badge lookup in the
displayed update logic to query that id instead of the mutable badge-accent
class. Preserve the existing text and pass/fail class updates so repeated drawer
openings refresh the correct badge.
web/assets/js/fallbacks.js-128-169 (1)

128-169: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The chart fallback destroys the live <canvas> elements and leaves stale Chart.js instances.

renderFallbackState assigns container.innerHTML, and container is the parent of the canvas. main.js calls renderAESChart, renderHashChart, and renderAsymmetricChart before checkAndRenderFeatureFallbacks, so Chart.js instances already exist and hold references to those canvases. Two consequences follow:

  1. The module-level aesChart, hashChart, and asymmetricChart variables in web/assets/js/charts.js keep pointing to detached canvases. updateChartsTheme then calls chart.update() on a detached canvas on every theme toggle.
  2. document.getElementById('aesCurveChart') returns null afterwards. Any later re-render returns early and the chart never comes back.

Hide the canvas and append the fallback into a sibling node instead of overwriting the parent.

🛠️ Proposed approach
-    const aesCanvas = document.getElementById('aesCurveChart');
-    if (aesCanvas) {
-      const container = aesCanvas.parentElement;
-      if (container) {
-        renderFallbackState(
-          container,
+    const aesCanvas = document.getElementById('aesCurveChart');
+    if (aesCanvas) {
+      const container = aesCanvas.parentElement;
+      if (container) {
+        aesCanvas.classList.add('hidden');
+        const slot = document.createElement('div');
+        container.appendChild(slot);
+        renderFallbackState(
+          slot,
           'Performance Benchmark Data Pending',
           'Automated OpenSSL speed runs have not been executed on this host. Run the benchmark tool to populate cryptographic throughput curves.',
           'chart'
         );
       }
     }

Apply the same pattern to hashComparisonChart and asymmetricVelocityChart. Alternatively, export a destroyCharts() helper from charts.js and call it before the fallback replaces the containers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/fallbacks.js` around lines 128 - 169, Update the benchmark
fallback handling in checkAndRenderFeatureFallbacks so it preserves the live
canvases and existing Chart.js instances: hide each canvas and append or render
the fallback in a separate sibling node rather than replacing the canvas
parent’s innerHTML. Apply this consistently to aesCurveChart,
hashComparisonChart, and asymmetricVelocityChart, ensuring the canvas IDs remain
available for later chart rendering and theme updates.
web/assets/js/main.js-309-349 (1)

309-349: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

One try block wraps the fetch and every renderer. A single renderer failure blanks the dashboard and misreports the cause.

Two distinct problems follow from the current structure:

  1. initThemeEngine() on line 320 runs only after the fetch succeeds. If the fetch fails, or if any renderer throws, the theme engine never initializes. The error card then renders with no data-theme attribute, and the theme toggle has no click listener. The user cannot switch themes on the error page.
  2. If renderAESChart, initVulnerabilityInspector, or initTestsSuiteViewer throws, the catch replaces the whole overview panel and displays "We couldn't retrieve or parse web/data.json file". That message is wrong. The data parsed correctly. A rendering defect is reported as a data-build failure, which sends the user to the wrong place.

Initialize the theme first, then isolate each renderer.

🛠️ Proposed restructure
 async function bootstrapDashboard() {
+  initThemeEngine();
+
+  const safeRender = (name, fn) => {
+    try {
+      fn();
+    } catch (err) {
+      console.error(`Dashboard feature "${name}" failed to render:`, err);
+    }
+  };
+
   try {
     const response = await fetch('data.json');
     if (!response.ok) {
       throw new Error(`HTTP fetch error! Status code: ${response.status}`);
     }
     const rawData = await response.json();
     appState = sanitizeDashboardData(rawData);
     window.globalDashboardState = appState;
-
-    initThemeEngine();
-    renderBaseScorecards();
-    renderSystemBaseSpec();
-    renderOpenSSLPackageSpecs();
-
-    renderAESChart(appState);
-    renderHashChart(appState);
-    renderAsymmetricChart(appState);
-
-    initVulnerabilityInspector(appState);
-    renderHardeningScorecard(appState);
-    renderKicsSecurityViewer(appState);
-
-    initTestsSuiteViewer(appState);
-
-    renderRegistryArtifacts();
-    setupPullTypeSelectors();
-
-    checkAndRenderFeatureFallbacks(appState);
-
+    updateChartsTheme(appState);
+
+    safeRender('scorecards', renderBaseScorecards);
+    safeRender('system spec', renderSystemBaseSpec);
+    safeRender('package spec', renderOpenSSLPackageSpecs);
+    safeRender('AES chart', () => renderAESChart(appState));
+    safeRender('hash chart', () => renderHashChart(appState));
+    safeRender('asymmetric chart', () => renderAsymmetricChart(appState));
+    safeRender('vulnerabilities', () => initVulnerabilityInspector(appState));
+    safeRender('hardening', () => renderHardeningScorecard(appState));
+    safeRender('KICS', () => renderKicsSecurityViewer(appState));
+    safeRender('tests', () => initTestsSuiteViewer(appState));
+    safeRender('registry', renderRegistryArtifacts);
+    safeRender('pull selectors', setupPullTypeSelectors);
+    safeRender('fallbacks', () => checkAndRenderFeatureFallbacks(appState));
   } catch (err) {
     console.error('Failed to fetch or parse data.json payload:', err);
     renderErrorFallback(err);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/main.js` around lines 309 - 349, Restructure bootstrapDashboard
so initThemeEngine runs before fetch and remains available on both success and
failure paths. Keep fetch/JSON parsing errors handled separately from rendering,
then isolate each renderer or rendering group so one failure does not replace
the entire dashboard or get reported as a data retrieval/parsing error; preserve
the existing renderErrorFallback behavior only for fetch or parse failures.
web/assets/js/tests.js-149-201 (1)

149-201: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

The forensic drawer is a modal without keyboard support.

The overlay has no role="dialog", no aria-modal, and no Escape-key handler. Focus is not moved into the drawer when it opens, and it is not restored when it closes. The page behind the overlay stays in the tab order while it is visually obscured, so a keyboard user tabs into hidden content.

A keyboard user cannot dismiss the drawer except by tabbing blindly to the close button.

♿ Proposed fix
   const overlay = document.createElement('div');
   overlay.id = 'forensicLogDrawerOverlay';
+  overlay.setAttribute('role', 'dialog');
+  overlay.setAttribute('aria-modal', 'true');
+  overlay.setAttribute('aria-labelledby', 'drawerTestTitle');
   overlay.className = 'fixed inset-0 bg-slate-950/60 backdrop-blur-sm z-50 flex justify-end opacity-0 pointer-events-none transition-opacity duration-300';
@@
   overlay.addEventListener('click', (e) => {
     if (e.target === overlay) {
       closeForensicLogsDrawer();
     }
   });
+
+  document.addEventListener('keydown', (e) => {
+    if (e.key === 'Escape' && overlay.classList.contains('opacity-100')) {
+      closeForensicLogsDrawer();
+    }
+  });
 }

In openForensicLogsDrawer, store document.activeElement, then move focus to the close button. In closeForensicLogsDrawer, restore focus to the stored element.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tests.js` around lines 149 - 201, Update initLogDrawerOverlay
and the related openForensicLogsDrawer/closeForensicLogsDrawer flow to make the
drawer an accessible modal: add dialog semantics with role="dialog" and
aria-modal="true", handle Escape to close it, move focus to the close button
when opened, store the previously active element, restore that element on close,
and prevent keyboard tabbing into obscured page content while the drawer is
open.
web/assets/js/tests.js-124-131 (1)

124-131: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A suite with no tests shows "100% Pass Rate" in the success color.

Line 125 sets passRate to 100 when total is 0. Line 131 then renders "100% Pass Rate" using text-[var(--color-success)]. A test suite that never ran is presented as fully passing.

Line 131 also applies the success color unconditionally. A suite at 40% pass rate is rendered in green.

🛠️ Proposed fix
   const total = stats.total || stats.passed + stats.failed || 0;
-  const passRate = total > 0 ? Math.round((stats.passed / total) * 100) : 100;
+  const hasRun = total > 0;
+  const passRate = hasRun ? Math.round((stats.passed / total) * 100) : 0;
+  const rateLabel = hasRun ? `${passRate}% Pass Rate` : 'Not Executed';
+  const rateColor = !hasRun
+    ? 'text-[var(--text-muted)]'
+    : passRate === 100
+      ? 'text-[var(--color-success)]'
+      : 'text-[var(--color-warning)]';
@@
-        <div class="text-lg font-bold text-[var(--color-success)] mt-1 font-mono">${passRate}% Pass Rate</div>
+        <div class="text-lg font-bold ${rateColor} mt-1 font-mono">${rateLabel}</div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tests.js` around lines 124 - 131, Update the pass-rate
calculation and status markup in the test statistics rendering so a suite with
total 0 is represented as not run rather than 100% passed. Make the status text
and color conditional on the computed pass rate, using the success styling only
for fully passing suites and an appropriate non-success style for partial or
empty results.
web/assets/js/charts.js-237-238 (1)

237-238: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Hardcoded Ed25519 numbers are rendered as measured throughput.

When no Ed25519 record exists, lines 237-238 substitute 21500 and 7400. The chart presents these as measured operations per second. No marker tells the viewer that the values are synthetic.

The logarithmic y-axis on line 287 makes the result worse. Chart.js cannot plot 0 on a log scale, so the missing RSA and ECDSA bars disappear. The fabricated Ed25519 bars are then the only visible data, and the chart looks like a completed benchmark run.

Return 0 for missing records and let the pending-state fallback handle the empty case.

🛠️ Proposed fix
-  const edSign = getOps('Ed25519', 'Sign') || 21500;
-  const edVerify = getOps('Ed25519', 'Verify') || 7400;
+  const edSign = getOps('Ed25519', 'Sign');
+  const edVerify = getOps('Ed25519', 'Verify');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/charts.js` around lines 237 - 238, Update the Ed25519 fallback
assignments near getOps('Ed25519', 'Sign') and getOps('Ed25519', 'Verify') to
return 0 when records are missing instead of hardcoded throughput values,
allowing the existing pending-state handling to represent an incomplete
benchmark.
web/assets/js/tests.js-242-247 (1)

242-247: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

l.levelname is written to innerHTML without escaping.

Line 245 escapes l.msg but interpolates l.levelname directly inside [${l.levelname}]. Both values come from the same pytest report record in data.json. The escaping is inconsistent for two fields of the same object.

Line 244 also compares l.levelname against 'ERROR' and 'WARNING' only. Any other level, including CRITICAL, is styled as success green.

🔒 Proposed fix
     logsHtml += emittedLogs.map(l => {
-      const levelClass = l.levelname === 'ERROR' || l.levelname === 'WARNING' ? 'text-amber-500 font-bold' : 'text-emerald-500';
-      return `<div class="mb-2"><span class="text-[var(--text-muted)] font-bold">[${l.levelname}]</span> <span class="${levelClass}">${escapeHTML(l.msg)}</span></div>`;
+      const level = String(l?.levelname ?? 'INFO');
+      const levelClass = ['ERROR', 'CRITICAL', 'FATAL'].includes(level)
+        ? 'text-rose-500 font-bold'
+        : level === 'WARNING'
+          ? 'text-amber-500 font-bold'
+          : 'text-emerald-500';
+      return `<div class="mb-2"><span class="text-[var(--text-muted)] font-bold">[${escapeHTML(level)}]</span> <span class="${levelClass}">${escapeHTML(l?.msg)}</span></div>`;
     }).join('');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tests.js` around lines 242 - 247, Update the emittedLogs
rendering map to escape l.levelname before interpolating it into innerHTML, and
classify all non-success levels—including CRITICAL—as error/warning styling
instead of defaulting them to green. Preserve the existing escaped l.msg
rendering and markup structure.

Source: Linters/SAST tools

web/assets/js/main.js-351-364 (1)

351-364: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

The error card writes an unescaped error string into innerHTML.

Line 361 interpolates err.stack || err.message. When response.json() rejects, the engine embeds a fragment of the offending document in the message text. A malformed or crafted data.json therefore places attacker-influenced characters into the DOM as markup.

Escape the value. Better, use textContent on the <pre> element, because no markup is intended there.

🔒 Proposed fix
 function renderErrorFallback(err) {
   const container = document.getElementById('tab-panel-overview');
   if (container) {
     container.innerHTML = `
       <div class="p-8 bg-red-500/10 border border-red-500/30 rounded-xl max-w-2xl mx-auto text-center space-y-3">
         <svg class="w-12 h-12 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
           <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
         </svg>
         <h3 class="text-base font-bold text-[var(--text-primary)]">System Aggregator Parse Failure</h3>
         <p class="text-xs text-[var(--text-secondary)]">We couldn't retrieve or parse web/data.json file. Make sure the Node data build process completed successfully.</p>
-        <pre class="bg-black/40 text-left p-3 rounded-lg text-[10px] text-red-400 font-mono overflow-x-auto">${err.stack || err.message}</pre>
+        <pre class="bg-black/40 text-left p-3 rounded-lg text-[10px] text-red-400 font-mono overflow-x-auto" id="error-detail"></pre>
       </div>
     `;
+    const detail = container.querySelector('`#error-detail`');
+    if (detail) detail.textContent = err.stack || err.message || String(err);
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/main.js` around lines 351 - 364, Update renderErrorFallback so
the error detail from err.stack or err.message is not interpolated into
innerHTML. Render the static error card markup separately, then locate its pre
element and assign the error text via textContent, preserving the existing
fallback value without allowing it to be interpreted as HTML.

Source: Linters/SAST tools

web/assets/js/tables.js-329-365 (1)

329-365: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Numeric report fields are interpolated into innerHTML without escaping.

Lines 337, 341, 345, and 361-365 interpolate kics.files_scanned, kics.lines_scanned, totalCount, and the severity_counters entries directly. The code assumes these are numbers, but JSON places no such constraint and sanitizeDashboardData does not coerce them. A string value in any of these fields is injected as markup.

Line 281 has the same pattern with ${find.line}.

Coerce to a number at the read site.

🛠️ Proposed fix
+    const num = (v) => (Number.isFinite(Number(v)) ? Number(v) : 0);
     const sev = kics.severity_counters || { CRITICAL: 0, HIGH: 0, MEDIUM: 0, LOW: 0, INFO: 0 };
-    const totalCount = kics.total_counter !== undefined ? kics.total_counter : 0;
+    const totalCount = num(kics.total_counter);
@@
-          <div class="text-xl font-bold text-[var(--text-primary)] mt-1 font-mono">${kics.files_scanned || 0}</div>
+          <div class="text-xl font-bold text-[var(--text-primary)] mt-1 font-mono">${num(kics.files_scanned)}</div>
@@
-          <div class="text-xl font-bold text-[var(--text-primary)] mt-1 font-mono">${kics.lines_scanned || 0}</div>
+          <div class="text-xl font-bold text-[var(--text-primary)] mt-1 font-mono">${num(kics.lines_scanned)}</div>
@@
-          <span class="badge badge-error py-1 px-3 font-mono text-xs">CRITICAL: <strong>${sev.CRITICAL || 0}</strong></span>
-          <span class="badge badge-error py-1 px-3 font-mono text-xs">HIGH: <strong>${sev.HIGH || 0}</strong></span>
-          <span class="badge badge-warning py-1 px-3 font-mono text-xs">MEDIUM: <strong>${sev.MEDIUM || 0}</strong></span>
-          <span class="badge badge-indigo py-1 px-3 font-mono text-xs">LOW: <strong>${sev.LOW || 0}</strong></span>
-          <span class="badge badge-accent py-1 px-3 font-mono text-xs">INFO: <strong>${sev.INFO || 0}</strong></span>
+          <span class="badge badge-error py-1 px-3 font-mono text-xs">CRITICAL: <strong>${num(sev.CRITICAL)}</strong></span>
+          <span class="badge badge-error py-1 px-3 font-mono text-xs">HIGH: <strong>${num(sev.HIGH)}</strong></span>
+          <span class="badge badge-warning py-1 px-3 font-mono text-xs">MEDIUM: <strong>${num(sev.MEDIUM)}</strong></span>
+          <span class="badge badge-indigo py-1 px-3 font-mono text-xs">LOW: <strong>${num(sev.LOW)}</strong></span>
+          <span class="badge badge-accent py-1 px-3 font-mono text-xs">INFO: <strong>${num(sev.INFO)}</strong></span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 329 - 365, Coerce all interpolated
numeric report values to numbers at their read sites before constructing the
HTML: `kics.files_scanned`, `kics.lines_scanned`, `totalCount`, each `sev`
severity entry, and `find.line` in the surrounding rendering logic. Update the
related comparisons and interpolations to use these numeric values while
preserving the existing defaults and display behavior.

Source: Linters/SAST tools

web/assets/js/charts.js-37-39 (1)

37-39: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard Chart.js against unavailable global usage

Chart is not imported; web/index.html relies on the unversioned CDN script before assets/js/main.js. If that script fails or its module fails, new Chart(...) raises ReferenceError: Chart is not defined and the shared bootstrapDashboard() try replaces the whole dashboard with parse failure.

Use a pinned source for the Chart.js dependency, or add an availability guard before creating chart instances so rendering does not make the dashboard unavailable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/charts.js` around lines 37 - 39, Update the chart creation flow
around aesChart and new Chart in bootstrapDashboard to avoid referencing an
unavailable Chart global. Prefer a pinned Chart.js dependency in the page, or
guard chart initialization by checking Chart availability and skip chart
rendering while allowing the shared dashboard bootstrap to continue.
web/assets/js/tests.js-50-60 (1)

50-60: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

renderSuite throws when a test record lacks nodeid, and toFixed throws on a non-numeric duration.

Line 52 calls test.nodeid.split('::') with no guard. sanitizeDashboardData in web/assets/js/fallbacks.js guarantees only that reports[variant].tests is an array. It does not validate the elements. A record without nodeid raises TypeError: Cannot read properties of undefined (reading 'split').

Line 84 calls (test.setup?.duration || 0).toFixed(3). If duration is a string such as "0.42", the || guard passes it through and .toFixed raises TypeError.

bootstrapDashboard in web/assets/js/main.js wraps every renderer in one try, so one malformed record replaces the whole dashboard with the parse-failure card.

Line 53 also uses .replace('test_', ''), which removes the first occurrence anywhere in the string rather than the prefix. Anchor the pattern.

🛡️ Proposed fix
     tests.forEach((test, idx) => {
       const isPassed = test.outcome === 'passed';
-      const cleanTitle = test.nodeid.split('::').pop()
-        .replace('test_', '')
+      const nodeId = typeof test.nodeid === 'string' ? test.nodeid : 'unknown::unknown';
+      const cleanTitle = nodeId.split('::').pop()
+        .replace(/^test_/, '')
         .replace(/_/g, ' ')
         .replace(/\b\w/g, c => c.toUpperCase());
@@
-      const duration = test.call?.duration ? test.call.duration.toFixed(3) : '0.000';
+      const toSeconds = (v) => (Number.isFinite(Number(v)) ? Number(v).toFixed(3) : '0.000');
+      const duration = toSeconds(test.call?.duration);

Apply toSeconds(test.setup?.duration) on line 84 and use escapeHTML(nodeId) on line 77.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tests.js` around lines 50 - 60, Update renderSuite to safely
handle malformed test records: default a missing test.nodeid before splitting,
anchor the test_ removal to the prefix, and escape the resulting node ID with
escapeHTML. Normalize setup and call durations through toSeconds before
formatting with toFixed, preserving the existing zero fallback for invalid or
absent values.
web/assets/js/tables.js-64-69 (1)

64-69: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

This code and fallbacks.js toggle different elements for the same empty state. The table can stay hidden permanently.

Lines 66 and 69 toggle tableBody.parentElement. The parent of a <tbody> is the <table> element. checkAndRenderFeatureFallbacks in web/assets/js/fallbacks.js line 100 instead hides tableBody.closest('.overflow-x-auto'), which is the outer wrapper.

The failure sequence is:

  1. No scan data exists. fallbacks.js adds hidden to the .overflow-x-auto wrapper.
  2. The user selects a different variant that does have data.
  3. render() removes hidden from the <table> only. The wrapper keeps hidden, so nothing appears.

Use the same element in both modules. Line 30 also reads emptyState without a null guard, while line 35 guards tableBody. Add the matching guard.

🛠️ Proposed fix
   const tableBody = document.getElementById('vulnerability-table-rows');
   const emptyState = document.getElementById('vulnerability-empty-state');
@@
-  if (!tableBody) return;
+  if (!tableBody || !emptyState) return;
+  const tableWrapper = tableBody.closest('.overflow-x-auto') || tableBody.parentElement;
@@
     if (filtered.length === 0) {
       emptyState.classList.remove('hidden');
-      tableBody.parentElement.classList.add('hidden');
+      tableWrapper.classList.add('hidden');
     } else {
       emptyState.classList.add('hidden');
-      tableBody.parentElement.classList.remove('hidden');
+      tableWrapper.classList.remove('hidden');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 64 - 69, Update the empty-state
visibility logic in render to toggle the table’s closest .overflow-x-auto
wrapper, matching checkAndRenderFeatureFallbacks, instead of
tableBody.parentElement. Also guard emptyState before accessing classList,
consistent with the existing tableBody guard, while preserving the current
empty/non-empty visibility behavior.
web/assets/js/tables.js-384-443 (1)

384-443: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Three different values represent the KICS finding count and they can disagree.

  • Line 330 uses kics.total_counter for the "Total Issues" metric card.
  • Line 372 gates the entire findings section on totalCount === 0.
  • Line 443 displays findings.length as "Total Findings".
  • Lines 385-436 build the table rows from kics.queries, not from findings.

If total_counter is 0 but queries is non-empty, the dashboard shows "Zero Security Misconfigurations Found" and hides real findings. If queries is absent while total_counter is greater than zero, the table header renders with an empty body.

Derive one count from one source, and gate the empty state on the same source that supplies the rows.

🛠️ Proposed change
-    if (totalCount === 0) {
+    const queries = Array.isArray(kics.queries) ? kics.queries : [];
+    const rowCount = queries.reduce((n, q) => n + (Array.isArray(q.files) ? q.files.length : 0), 0);
+    if (rowCount === 0) {
@@
-            <span class="badge badge-accent py-0.5 px-2.5 font-mono text-[10px]">Total Findings: <strong>${findings.length}</strong></span>
+            <span class="badge badge-accent py-0.5 px-2.5 font-mono text-[10px]">Total Findings: <strong>${rowCount}</strong></span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 384 - 443, Unify the findings count and
empty-state logic around the normalized collection used to build table rows from
kics.queries and their files. Update totalCount, the “Total Issues” metric, and
the “Total Findings” badge to use that same collection length instead of
kics.total_counter or findings.length, and gate the empty state on this count.
🟡 Minor comments (6)
web/builder.js-417-424 (1)

417-424: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Replace hardcoded hardware_context values with real measurements.

release is a literal placeholder string 'Kernel Release', and cpu_cores/ram_gb are fixed constants instead of measured values. This dashboard reports these fields as the "Runner Infrastructure Environment" for a compliance audit, so a fabricated value undermines the accuracy of that report. Node's os module provides these values natively.

🛠️ Proposed fix
 import fs from 'fs';
 import path from 'path';
+import os from 'os';
 import { fileURLToPath } from 'url';
     hardware_context: {
       system: process.platform === 'win32' ? 'Windows' : process.platform === 'darwin' ? 'macOS' : 'Linux',
-      release: 'Kernel Release',
+      release: os.release(),
       architecture: process.arch,
-      cpu_cores: 4,
-      ram_gb: 16,
+      cpu_cores: os.cpus().length,
+      ram_gb: Math.round(os.totalmem() / (1024 ** 3)),
       runner: process.env.CI ? 'GitHub Actions CI' : 'Local Runner'
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/builder.js` around lines 417 - 424, Update the hardware_context object to
report actual runner measurements: use Node’s os module for the kernel release,
CPU core count, and total memory converted to gigabytes, replacing the
placeholder release and fixed cpu_cores/ram_gb constants while preserving the
existing system, architecture, and runner fields.
web/data.json-1-10 (1)

1-10: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the generated web/data.json from the repository.

web/builder.js writes web/data.json, tools/Taskfile.doc.yml compiles reports into it, and web/verify.js reads it. Add web/data.json to .gitignore and remove the committed snapshot so the web payload is produced by the pipeline instead of being tracked in source control.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/data.json` around lines 1 - 10, Remove the committed generated snapshot
web/data.json, and add web/data.json to .gitignore so the builder.js and
Taskfile.doc.yml pipeline output remains untracked while verify.js can continue
reading the generated file.
web/assets/js/fallbacks.js-117-126 (1)

117-126: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

This fallback competes with the empty state already rendered by renderKicsSecurityViewer.

renderKicsSecurityViewer in web/assets/js/tables.js (lines 258-265) already writes "Zero IaC Infrastructure Security Violations Emitted by KICS." into kicsStaticAnalysisAccordion when findings.length === 0. This block overwrites that message with "KICS Static Audit Pending". The two messages state different things. A clean scan is not a pending scan.

Choose one owner for the empty state of kicsStaticAnalysisAccordion. Use the presence of scan metadata, for example data.security.kics.total_counter !== undefined, to distinguish "clean" from "not run".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/fallbacks.js` around lines 117 - 126, Remove the competing
empty-state rendering from the KICS fallback block in fallbacks.js and make one
owner responsible for kicsStaticAnalysisAccordion. Update the logic around
renderKicsSecurityViewer and the KICS fallback to use
data.security.kics.total_counter !== undefined (or the established scan-metadata
equivalent) to distinguish a completed clean scan from a scan that has not run,
preserving the clean-scan message and showing “KICS Static Audit Pending” only
when metadata is absent.
web/assets/js/fallbacks.js-47-70 (1)

47-70: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Placeholder values can present invented data as measured facts.

The fallback fills openssl_version: '3.1.2', cpu_cores: 4, ram_gb: 16, and architecture: 'x86_64'. renderSystemBaseSpec in web/assets/js/main.js renders these values without any "unknown" marker. A viewer cannot distinguish a real measurement from a default. This is a compliance-reporting dashboard, so invented hardware and version data is misleading.

Use neutral sentinels such as 'N/A' (as already done for provenance.digest), and let the render layer show a pending state.

🛠️ Proposed change
-  safeData.benchmarks.metadata = safeData.benchmarks.metadata || { fips: { openssl_version: '3.1.2' } };
+  safeData.benchmarks.metadata = safeData.benchmarks.metadata || { fips: { openssl_version: 'N/A' } };
@@
   safeData.hardware_context = safeData.hardware_context || {
-    system: 'Linux',
-    architecture: 'x86_64',
-    cpu_cores: 4,
-    ram_gb: 16,
-    runner: 'Local Runner'
+    system: 'N/A',
+    architecture: 'N/A',
+    cpu_cores: null,
+    ram_gb: null,
+    runner: 'N/A'
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/fallbacks.js` around lines 47 - 70, Replace the fabricated
fallback values in the benchmark metadata and hardware_context
defaults—openssl_version, architecture, cpu_cores, and ram_gb—with neutral
unknown sentinels such as the existing 'N/A' convention. Keep genuine runtime
measurements unchanged and ensure renderSystemBaseSpec receives these sentinels
so the UI can display its pending/unknown state.
web/assets/js/main.js-16-26 (1)

16-26: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The initial theme is persisted even when the user never chose it.

setTheme writes to localStorage on line 48. The call on line 26 therefore stores the system-derived value on the first visit. After that, savedTheme is always set, so the dashboard stops following prefers-color-scheme.

Persist the theme only when the user clicks the toggle.

🛠️ Proposed change
-  setTheme(currentTheme);
+  setTheme(currentTheme, false);
 
   if (themeToggle) {
     themeToggle.addEventListener('click', () => {
       const targetTheme = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
-      setTheme(targetTheme);
+      setTheme(targetTheme, true);
     });
   }
-  function setTheme(theme) {
+  function setTheme(theme, persist = true) {
     document.documentElement.setAttribute('data-theme', theme);
-    localStorage.setItem('theme', theme);
+    if (persist) localStorage.setItem('theme', theme);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/main.js` around lines 16 - 26, Update the initial theme setup
around currentTheme and setTheme so it applies the saved, system-derived, or
default theme without persisting it on first load. Ensure localStorage is
written only by the user-triggered theme toggle handler, while preserving
saved-theme precedence and system preference detection.
web/assets/js/charts.js-130-142 (1)

130-142: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The index fallback can plot a different payload size under the "16KB" label.

Line 137 falls back from arr[5] to arr[arr.length - 1]. If a metrics array holds fewer than six samples, the code plots the last available bucket. The chart labels on line 153 still read "SHA-256 (16KB)", "SHA-512 (16KB)", and "SHA3-256 (16KB)". The rendered value then does not match the stated payload size.

The || chain also treats a measured 0 as a missing sample. Use an explicit length check and a nullish check.

🛠️ Proposed fix
   const extractMB = (algo) => {
-    const arr = metrics[algo]?.fips || [0,0,0,0,0,0];
-    const uArr = metrics[algo]?.ubuntu || [0,0,0,0,0,0];
-    const dArr = metrics[algo]?.debian || [0,0,0,0,0,0];
-    const aArr = metrics[algo]?.alpine || [0,0,0,0,0,0];
-    return {
-      fips: Number(((arr[5] || arr[arr.length - 1] || 0) / 1024).toFixed(2)),
-      ubuntu: Number(((uArr[5] || uArr[uArr.length - 1] || 0) / 1024).toFixed(2)),
-      debian: Number(((dArr[5] || dArr[dArr.length - 1] || 0) / 1024).toFixed(2)),
-      alpine: Number(((aArr[5] || aArr[aArr.length - 1] || 0) / 1024).toFixed(2))
-    };
+    const at16KB = (arr) => {
+      if (!Array.isArray(arr) || arr.length < 6) return 0;
+      const v = arr[5];
+      return typeof v === 'number' ? Number((v / 1024).toFixed(2)) : 0;
+    };
+    return {
+      fips: at16KB(metrics[algo]?.fips),
+      ubuntu: at16KB(metrics[algo]?.ubuntu),
+      debian: at16KB(metrics[algo]?.debian),
+      alpine: at16KB(metrics[algo]?.alpine)
+    };
   };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/charts.js` around lines 130 - 142, Update extractMB so each
algorithm reads only sample index 5 for the 16KB chart value, returning zero
when that index is unavailable; do not fall back to the last array element. Use
explicit length validation and nullish handling so a measured zero remains zero
across fips, ubuntu, debian, and alpine.
🧹 Nitpick comments (5)
web/index.html (1)

7-10: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Avoid the Tailwind Play CDN in production; add integrity checks for CDN scripts.

Tailwind's Play CDN is documented as "designed for development purposes only, and is not intended for production." It also compiles utility CSS at runtime in the browser, adding load latency and risking flash-of-unstyled-content on a live dashboard. Additionally, neither this script nor the Chart.js CDN script has an integrity/crossorigin attribute, so a compromised CDN could inject arbitrary code into a security compliance dashboard with no verification.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/index.html` around lines 7 - 10, Replace the Tailwind Play CDN script in
the document with a production-built Tailwind stylesheet, and add verified
integrity and crossorigin attributes to the Chart.js CDN script. Ensure the
integrity hashes match the exact pinned CDN assets and preserve the dashboard’s
existing styling and chart loading behavior.
web/assets/js/fallbacks.js (1)

224-231: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

escapeFallbackHTML duplicates the same escaper defined in two other modules.

The identical function exists as escapeHTML in web/assets/js/tables.js (lines 498-505) and web/assets/js/tests.js (lines 316-323). Three copies will drift. Move one implementation into a shared module, for example web/assets/js/utils.js, and import it in all three files.

The guard if (!str) return '' also converts 0 and false into an empty string. Return String(str) for non-nullish input instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/fallbacks.js` around lines 224 - 231, Consolidate the
duplicated HTML escaping logic from escapeFallbackHTML and the escapeHTML
functions in tables.js and tests.js into a shared utility, then import and reuse
that utility in all three modules. Preserve empty output only for nullish input;
coerce other values such as 0 and false with String(str) before escaping.
web/assets/js/tables.js (1)

8-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

renderCodeOrEmptyState is duplicated in tests.js and the two copies have already diverged.

web/assets/js/tests.js lines 6-24 define a function with the same name and nearly the same body. Two differences exist:

  1. This copy treats the literal string 'N/A' as empty (line 9). The tests.js copy does not.
  2. The default customClasses value differs.

The duplication is already producing inconsistent behavior for the same input. Move one implementation into a shared module and pass the differing default class string as an argument.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 8 - 26, Move renderCodeOrEmptyState
into a shared JavaScript module and remove the duplicated implementation from
both tables.js and tests.js. Preserve the shared empty-state behavior, including
treating trimmed "N/A" as empty, and pass each caller’s differing default
customClasses value explicitly so rendering remains consistent without
duplicated logic.
web/assets/js/charts.js (1)

304-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

updateChartsTheme ignores its data parameter and accesses the options tree without guards.

Line 304 declares data, but the body never reads it. main.js line 64 passes appState. Remove the parameter, or use it.

Lines 308-312 also assume chart.options.plugins.legend.labels and chart.options.plugins.tooltip always exist, while lines 314 and 319 do guard the scales. Make the access consistent.

Related: if the chart fallback in web/assets/js/fallbacks.js replaced a canvas parent, these chart objects still exist and chart.update() on line 324 runs against a detached canvas. Fixing the fallback container handling resolves that path.

♻️ Proposed refactor
-export function updateChartsTheme(data) {
+export function updateChartsTheme() {
   const colors = getThemeColors();
   const updateOpts = (chart) => {
-    if (!chart) return;
-    chart.options.plugins.legend.labels.color = colors.textColor;
-    chart.options.plugins.tooltip.backgroundColor = colors.tooltipBg;
-    chart.options.plugins.tooltip.titleColor = colors.tooltipColor;
-    chart.options.plugins.tooltip.bodyColor = colors.tooltipColor;
-    chart.options.plugins.tooltip.borderColor = colors.tooltipBorder;
+    if (!chart?.options) return;
+    const plugins = chart.options.plugins;
+    if (plugins?.legend?.labels) plugins.legend.labels.color = colors.textColor;
+    if (plugins?.tooltip) {
+      plugins.tooltip.backgroundColor = colors.tooltipBg;
+      plugins.tooltip.titleColor = colors.tooltipColor;
+      plugins.tooltip.bodyColor = colors.tooltipColor;
+      plugins.tooltip.borderColor = colors.tooltipBorder;
+    }

Update the call site in web/assets/js/main.js line 64 to updateChartsTheme();.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/charts.js` around lines 304 - 330, UpdateChartsTheme should no
longer accept the unused data parameter, and update its caller in main.js to
invoke it without arguments. In updateChartsTheme, guard the plugins, legend,
labels, and tooltip objects before assigning theme colors, matching the existing
scale guards; preserve chart.update() only for valid, attached chart instances
and ensure fallback container replacement does not leave these charts targeting
detached canvases.
web/assets/js/tests.js (1)

86-86: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

The inline onclick interpolates activeTestSuiteVariant into a JavaScript string literal.

Line 108 assigns activeTestSuiteVariant from a data-suite attribute. The value is authored in web/index.html today, so this is not currently exploitable. The pattern is still fragile: a data-suite value containing an apostrophe breaks the handler, and any future change that sources the variant from data.json turns this into an injection point.

web/assets/js/main.js line 229 has the same pattern with data-derived values, where it is exploitable. Fix both with the same approach: bind with addEventListener and read the value from dataset.

🛠️ Proposed change
-          <button class="inspect-btn ..." onclick="openForensicLogsDrawer('${activeTestSuiteVariant}', ${idx})">
+          <button class="inspect-btn ..." data-suite="${escapeHTML(activeTestSuiteVariant)}" data-index="${idx}">

Then after container.appendChild(card):

+      card.querySelector('.inspect-btn')?.addEventListener('click', (e) => {
+        const el = e.currentTarget;
+        window.openForensicLogsDrawer(el.dataset.suite, Number(el.dataset.index));
+      });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tests.js` at line 86, Replace the inline onclick on the inspect
button generated in tests.js with an addEventListener binding after
container.appendChild(card), storing the required index and suite variant in
dataset properties and reading them from the clicked element when calling
openForensicLogsDrawer. Apply the same dataset-based event-binding approach to
the corresponding data-derived handler in main.js, removing its inline
interpolation and preserving the existing action.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9570a497-fdd4-4a93-a2aa-d2b41bd7a172

📥 Commits

Reviewing files that changed from the base of the PR and between 44d9491 and de2714b.

⛔ Files ignored due to path filters (1)
  • Pipfile.lock is excluded by !**/*.lock
📒 Files selected for processing (49)
  • .gitignore
  • Pipfile
  • benchmark/parser.py
  • benchmark/run_benchmark.sh
  • docker-bake.hcl
  • docs/OPERATIONS.md
  • docs/architecture/index.md
  • docs/architecture/supply-chain.md
  • docs/index.md
  • docs/performance/benchmarks.md
  • docs/validation/compliance/development/docker-cis.md
  • docs/validation/compliance/development/k8s-nsa.md
  • docs/validation/compliance/development/k8s-pss.md
  • docs/validation/compliance/development/vulnerability.md
  • docs/validation/compliance/distroless/docker-cis.md
  • docs/validation/compliance/distroless/k8s-nsa.md
  • docs/validation/compliance/distroless/k8s-pss.md
  • docs/validation/compliance/distroless/vulnerability.md
  • docs/validation/compliance/standard/docker-cis.md
  • docs/validation/compliance/standard/k8s-nsa.md
  • docs/validation/compliance/standard/k8s-pss.md
  • docs/validation/compliance/standard/vulnerability.md
  • docs/validation/functional-tests/distroless-image.md
  • docs/validation/functional-tests/standard-image.md
  • docs/validation/index.md
  • docs/validation/static-analysis/kics-report.md
  • infra/allure2/Dockerfile
  • infra/allure3/Dockerfile
  • main.py
  • metadata.json
  • mkdocs.yml
  • package.json
  • scripts/update_readme.py
  • scripts/upload_sbom.py
  • scripts/wolfi-pkg-updater.py
  • server.js
  • taskfile.yml
  • tasks/Taskfile.doc.yml
  • web/assets/js/charts.js
  • web/assets/js/fallbacks.js
  • web/assets/js/main.js
  • web/assets/js/tables.js
  • web/assets/js/tests.js
  • web/builder.js
  • web/data.json
  • web/index.html
  • web/package.json
  • web/styles.css
  • web/verify.js
💤 Files with no reviewable changes (25)
  • infra/allure2/Dockerfile
  • docs/validation/compliance/distroless/k8s-pss.md
  • docs/validation/compliance/development/k8s-nsa.md
  • docs/performance/benchmarks.md
  • docs/validation/functional-tests/distroless-image.md
  • docs/validation/compliance/distroless/k8s-nsa.md
  • docs/validation/compliance/standard/k8s-pss.md
  • docs/validation/compliance/standard/vulnerability.md
  • docs/validation/static-analysis/kics-report.md
  • docs/architecture/supply-chain.md
  • docs/validation/compliance/distroless/vulnerability.md
  • docs/validation/compliance/standard/k8s-nsa.md
  • docs/validation/compliance/standard/docker-cis.md
  • docs/validation/compliance/development/docker-cis.md
  • docs/validation/compliance/development/k8s-pss.md
  • mkdocs.yml
  • docs/validation/functional-tests/standard-image.md
  • docs/OPERATIONS.md
  • docs/architecture/index.md
  • docs/validation/index.md
  • main.py
  • infra/allure3/Dockerfile
  • docs/validation/compliance/distroless/docker-cis.md
  • docs/index.md
  • docs/validation/compliance/development/vulnerability.md

Comment thread server.js
Comment on lines +33 to +41
const filePath = path.join(WEB_DIR, pathname);

// Security check: Prevent directory traversal
if (!filePath.startsWith(WEB_DIR)) {
res.statusCode = 403;
res.setHeader('Content-Type', 'text/plain');
res.end('Forbidden');
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Fix the directory-traversal check: startsWith alone allows sibling-directory escape.

filePath.startsWith(WEB_DIR) is a bare string-prefix check. If a sibling entry next to web/ shares the same prefix (for example web-backup, web.bak, or any name starting with web), a request such as /../web-backup/secret.txt normalizes via path.join to a path outside WEB_DIR that still satisfies startsWith(WEB_DIR), because the string "…/web-backup" starts with "…/web". This bypasses the traversal guard. Require a path separator (or trailing sentinel) after WEB_DIR in the comparison.

🛠️ Proposed fix
   const filePath = path.join(WEB_DIR, pathname);
   
   // Security check: Prevent directory traversal
-  if (!filePath.startsWith(WEB_DIR)) {
+  if (filePath !== WEB_DIR && !filePath.startsWith(WEB_DIR + path.sep)) {
     res.statusCode = 403;
     res.setHeader('Content-Type', 'text/plain');
     res.end('Forbidden');
     return;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const filePath = path.join(WEB_DIR, pathname);
// Security check: Prevent directory traversal
if (!filePath.startsWith(WEB_DIR)) {
res.statusCode = 403;
res.setHeader('Content-Type', 'text/plain');
res.end('Forbidden');
return;
}
const filePath = path.join(WEB_DIR, pathname);
// Security check: Prevent directory traversal
if (filePath !== WEB_DIR && !filePath.startsWith(WEB_DIR + path.sep)) {
res.statusCode = 403;
res.setHeader('Content-Type', 'text/plain');
res.end('Forbidden');
return;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server.js` around lines 33 - 41, Update the directory-traversal validation
around filePath so it accepts only WEB_DIR itself or paths whose next character
is the platform path separator, rather than relying on a bare WEB_DIR.startsWith
prefix check. Preserve the existing 403 response for paths outside that
boundary.

Comment thread tasks/Taskfile.doc.yml Outdated
Comment on lines +9 to +12
serve:
desc: "Run MkDocs server locally for live preview"
desc: "Run local Node.js preview server for the web dashboard"
cmds:
- pipenv run mkdocs serve --dev-addr 0.0.0.0:8000


build-MkDocs-site:
desc: "Build the final FIPS Boundary Security Dashboard"
summary: |
This task generates the static site (MkDocs).
It assumes all JSON reports (Trivy, KICS, Benchmarks)
are already gathered in the 'reports/' folder.
cmds:
- echo "Building MkDocs Security Dashboard..."
- pipenv run mkdocs build --clean
- echo "Done! The site is ready in the 'site/' directory."
- cd web && node cli.js --serve

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd -i cli.js
fd -i server.js

Repository: taha2samy/openssl_fips

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== tracked files matching cli/server/task =="
git ls-files | rg -n '(^|/)(cli\.js|server\.js|Taskfile\..*yml)$|web/' || true

echo
echo "== Taskfile.doc.yml =="
if [ -f tasks/Taskfile.doc.yml ]; then
  cat -n tasks/Taskfile.doc.yml
else
  echo "missing tasks/Taskfile.doc.yml"
fi

echo
echo "== server.js first 220 lines =="
if [ -f server.js ]; then
  sed -n '1,220p' server.js | cat -n
else
  echo "missing server.js"
fi

echo
echo "== web/cli.js presence check =="
if [ -f web/cli.js ]; then
  sed -n '1,160p' web/cli.js | cat -n
else
  echo "web/cli.js not present"
fi

echo
echo "== package.json scripts =="
if [ -f package.json ]; then
  node -e "const p=require('./package.json'); console.log(JSON.stringify({scripts:p.scripts, dependencies:p.dependencies, devDependencies:p.devDependencies}, null, 2))"
fi

Repository: taha2samy/openssl_fips

Length of output: 3603


Fix the serve task to launch the tracked server.

tasks/Taskfile.doc.yml:9-12 runs cd web && node cli.js --serve, but web/cli.js is not present and the repository package script is start: node server.js. server.js services web/ from the repository root, so this task should launch server.js without changing directories.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tasks/Taskfile.doc.yml` around lines 9 - 12, Update the serve task in
Taskfile.doc.yml to invoke the tracked server.js from the repository root,
matching the package start script; remove the web directory change and preserve
the task’s existing description.

Comment thread web/assets/js/main.js
Comment on lines +128 to +152
container.innerHTML = specs.map(item => `
<div class="flex items-center justify-between py-2 text-xs border-b border-[var(--border-color)] last:border-b-0">
<span class="text-[var(--text-secondary)] font-medium">${item.label}</span>
<span class="font-mono text-[var(--text-primary)] select-all">${item.val}</span>
</div>
`).join('');
}

// Side-by-side OpenSSL core packages
function renderOpenSSLPackageSpecs() {
const container = document.getElementById('sidebar-package-spec');
if (!container) return;

const packages = appState.packages || [];
if (packages.length === 0) {
container.innerHTML = `<span class="italic text-[var(--text-muted)] text-xs">No core packages identified.</span>`;
return;
}

container.innerHTML = packages.map(pkg => `
<div class="flex items-center justify-between py-2 text-xs border-b border-[var(--border-color)] last:border-b-0">
<span class="text-[var(--text-primary)] font-semibold">${pkg.name}</span>
<span class="font-mono text-[var(--text-secondary)] bg-[var(--bg-tertiary)] border border-[var(--border-color)] px-1.5 py-0.5 rounded">${pkg.version}</span>
</div>
`).join('');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Unescaped report data reaches innerHTML in both sidebar renderers.

Lines 130-131 interpolate item.val, which carries hardware.kernel from data.json. Lines 149-150 interpolate pkg.name and pkg.version from data.json. Neither value is escaped, and both originate from generated scan and host telemetry rather than from literals in this file.

Every other renderer in this change escapes dynamic values. For example web/assets/js/tables.js line 95 uses escapeHTML. Apply the same treatment here.

🔒 Proposed fix
   container.innerHTML = specs.map(item => `
     <div class="flex items-center justify-between py-2 text-xs border-b border-[var(--border-color)] last:border-b-0">
-      <span class="text-[var(--text-secondary)] font-medium">${item.label}</span>
-      <span class="font-mono text-[var(--text-primary)] select-all">${item.val}</span>
+      <span class="text-[var(--text-secondary)] font-medium">${escapeHTML(item.label)}</span>
+      <span class="font-mono text-[var(--text-primary)] select-all">${escapeHTML(item.val)}</span>
     </div>
   `).join('');
   container.innerHTML = packages.map(pkg => `
     <div class="flex items-center justify-between py-2 text-xs border-b border-[var(--border-color)] last:border-b-0">
-      <span class="text-[var(--text-primary)] font-semibold">${pkg.name}</span>
-      <span class="font-mono text-[var(--text-secondary)] bg-[var(--bg-tertiary)] border border-[var(--border-color)] px-1.5 py-0.5 rounded">${pkg.version}</span>
+      <span class="text-[var(--text-primary)] font-semibold">${escapeHTML(pkg.name)}</span>
+      <span class="font-mono text-[var(--text-secondary)] bg-[var(--bg-tertiary)] border border-[var(--border-color)] px-1.5 py-0.5 rounded">${escapeHTML(pkg.version)}</span>
     </div>
   `).join('');

main.js has no escapeHTML yet. Import it from the shared utility module proposed for web/assets/js/fallbacks.js, web/assets/js/tables.js, and web/assets/js/tests.js.

🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 142-142: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: container.innerHTML = <span class="italic text-[var(--text-muted)] text-xs">No core packages identified.</span>
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)


[warning] 146-151: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: container.innerHTML = packages.map(pkg => <div class="flex items-center justify-between py-2 text-xs border-b border-[var(--border-color)] last:border-b-0"> <span class="text-[var(--text-primary)] font-semibold">${pkg.name}</span> <span class="font-mono text-[var(--text-secondary)] bg-[var(--bg-tertiary)] border border-[var(--border-color)] px-1.5 py-0.5 rounded">${pkg.version}</span> </div>).join('')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/main.js` around lines 128 - 152, Escape the dynamic report
values interpolated by renderSpecs and renderOpenSSLPackageSpecs before
assigning their templates to innerHTML. Import and reuse the shared escapeHTML
utility for item.val, pkg.name, and pkg.version, preserving the existing
rendering and empty-state behavior.

Source: Linters/SAST tools

Comment thread web/assets/js/main.js
Comment on lines +216 to +250
const card = document.createElement('div');
card.className = 'border border-[var(--border-color)] p-5 rounded-xl bg-[var(--bg-secondary)] space-y-4';
card.innerHTML = `
<div class="flex items-center justify-between flex-wrap gap-2">
<h4 class="text-xs font-semibold text-[var(--text-primary)] flex items-center gap-2">
${item.name}
<span class="badge ${item.badge} text-[9px] py-0 px-1.5 font-bold">${item.badgeText}</span>
</h4>
<span class="text-[10px] text-[var(--text-muted)] font-mono">${displayLabel}</span>
</div>

<div class="flex items-center justify-between gap-3 bg-[var(--bg-primary)] p-2.5 rounded-lg border border-[var(--border-color)] font-mono text-[10.5px]">
<span class="text-[var(--text-secondary)] select-all truncate">${copyText}</span>
<button class="copy-btn py-1 px-2 text-[9.5px]" onclick="copyToClipboard('${copyText}', this)">Copy</button>
</div>

<div class="flex flex-wrap gap-3 pt-1">
<button class="flex items-center gap-1.5 py-1 px-2.5 rounded bg-[var(--bg-primary)] border border-[var(--border-color)] text-[10.5px] text-[var(--text-secondary)] font-semibold font-mono hover:bg-[var(--bg-tertiary)] transition-colors cursor-pointer" onclick="copyToClipboard('${item.digest}', this)">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
Digest: ${item.digest.substring(0, 12)}...
</button>

<a href="${item.provenanceUrl}" target="_blank" class="flex items-center gap-1.5 py-1 px-2.5 rounded bg-indigo-600/10 border border-indigo-500/30 text-[10.5px] text-indigo-400 font-semibold hover:bg-indigo-600/20 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
SLSA L3 Provenance
</a>

<a href="${item.sbomUrl}" target="_blank" class="flex items-center gap-1.5 py-1 px-2.5 rounded bg-emerald-600/10 border border-emerald-500/30 text-[10.5px] text-emerald-400 font-semibold hover:bg-emerald-600/20 transition-colors">
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
CycloneDX SBOM
</a>
</div>
`;
container.appendChild(card);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

Inline onclick handlers interpolate report data into a JavaScript string. This allows script injection.

Line 229 builds onclick="copyToClipboard('${copyText}', this)". copyText contains registry, owner, repoName, coreVersion, and item.digest, all read from data.json on lines 160-163 and 171-193. A single apostrophe in any of those values terminates the JavaScript string literal. A crafted value such as x'),alert(document.cookie),(0,' executes in the page origin. Line 233 has the same defect with item.digest.

Escaping is not enough here because the value crosses an HTML attribute boundary and then a JavaScript string boundary. Bind the handler with addEventListener and pass the value through dataset.

Two related points in the same block:

  • Lines 238 and 243 place item.provenanceUrl and item.sbomUrl directly into href. A javascript: URL from data.json executes on click. Validate the scheme and accept only http: and https:.
  • Both anchors use target="_blank" without rel="noopener noreferrer". Add the attribute.
🔒 Proposed fix
+    const safeUrl = (u) => {
+      try {
+        const parsed = new URL(u, window.location.href);
+        return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '#';
+      } catch {
+        return '#';
+      }
+    };
+
     const card = document.createElement('div');
     card.className = 'border border-[var(--border-color)] p-5 rounded-xl bg-[var(--bg-secondary)] space-y-4';
     card.innerHTML = `
@@
-        <span class="text-[var(--text-secondary)] select-all truncate">${copyText}</span>
-        <button class="copy-btn py-1 px-2 text-[9.5px]" onclick="copyToClipboard('${copyText}', this)">Copy</button>
+        <span class="text-[var(--text-secondary)] select-all truncate">${escapeHTML(copyText)}</span>
+        <button class="copy-btn py-1 px-2 text-[9.5px]" data-copy="${escapeHTML(copyText)}">Copy</button>
@@
-        <button class="... cursor-pointer" onclick="copyToClipboard('${item.digest}', this)">
+        <button class="... cursor-pointer" data-copy="${escapeHTML(item.digest)}">
@@
-        <a href="${item.provenanceUrl}" target="_blank" class="...">
+        <a href="${escapeHTML(safeUrl(item.provenanceUrl))}" target="_blank" rel="noopener noreferrer" class="...">
@@
-        <a href="${item.sbomUrl}" target="_blank" class="...">
+        <a href="${escapeHTML(safeUrl(item.sbomUrl))}" target="_blank" rel="noopener noreferrer" class="...">
     `;
+    card.querySelectorAll('[data-copy]').forEach(btn => {
+      btn.addEventListener('click', () => window.copyToClipboard(btn.dataset.copy, btn));
+    });
     container.appendChild(card);
🧰 Tools
🪛 ast-grep (0.45.0)

[warning] 217-247: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: card.innerHTML = `



${item.name}
${item.badgeText}


${displayLabel}

  <div class="flex items-center justify-between gap-3 bg-[var(--bg-primary)] p-2.5 rounded-lg border border-[var(--border-color)] font-mono text-[10.5px]">
    <span class="text-[var(--text-secondary)] select-all truncate">${copyText}</span>
    <button class="copy-btn py-1 px-2 text-[9.5px]" onclick="copyToClipboard('${copyText}', this)">Copy</button>
  </div>

  <div class="flex flex-wrap gap-3 pt-1">
    <button class="flex items-center gap-1.5 py-1 px-2.5 rounded bg-[var(--bg-primary)] border border-[var(--border-color)] text-[10.5px] text-[var(--text-secondary)] font-semibold font-mono hover:bg-[var(--bg-tertiary)] transition-colors cursor-pointer" onclick="copyToClipboard('${item.digest}', this)">
      <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
      Digest: ${item.digest.substring(0, 12)}...
    </button>
    
    <a href="${item.provenanceUrl}" target="_blank" class="flex items-center gap-1.5 py-1 px-2.5 rounded bg-indigo-600/10 border border-indigo-500/30 text-[10.5px] text-indigo-400 font-semibold hover:bg-indigo-600/20 transition-colors">
      <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/></svg>
      SLSA L3 Provenance
    </a>

    <a href="${item.sbomUrl}" target="_blank" class="flex items-center gap-1.5 py-1 px-2.5 rounded bg-emerald-600/10 border border-emerald-500/30 text-[10.5px] text-emerald-400 font-semibold hover:bg-emerald-600/20 transition-colors">
      <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"/></svg>
      CycloneDX SBOM
    </a>
  </div>
`

Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/main.js` around lines 216 - 250, In the card-rendering loop,
remove the inline onclick handlers for the copy controls and bind them with
addEventListener using dataset values for copyText and item.digest, avoiding
interpolation into JavaScript or HTML attributes. Validate item.provenanceUrl
and item.sbomUrl so only http: and https: URLs are assigned to href, and add
rel="noopener noreferrer" to both target="_blank" anchors.

Source: Linters/SAST tools

Comment thread web/assets/js/tables.js
Comment on lines +186 to +220
function parseSummaryControls(obj, defaultSuccess = 0, defaultFail = 0) {
if (!obj) return { SuccessCount: defaultSuccess, FailCount: defaultFail };
if (typeof obj.SuccessCount === 'number' && typeof obj.FailCount === 'number') {
return obj;
}
const controls = obj.SummaryControls || [];
let FailCount = 0;
let SuccessCount = 0;
controls.forEach(ctrl => {
if (ctrl.TotalFail > 0) {
FailCount++;
} else {
SuccessCount++;
}
});
if (controls.length === 0) {
return { SuccessCount: obj.SuccessCount !== undefined ? obj.SuccessCount : defaultSuccess, FailCount: obj.FailCount !== undefined ? obj.FailCount : defaultFail };
}
return { SuccessCount, FailCount };
}

const cis = parseSummaryControls(variantData.docker_cis, 12, 0);
const nsa = parseSummaryControls(variantData.k8s_nsa, 19, 0);
const pss = parseSummaryControls(variantData.k8s_pss_restricted, 16, 0);

const rules = [
{ name: 'Docker CIS Compliance Standard', passed: cis.SuccessCount, failed: cis.FailCount, desc: 'Verifies root-less isolation, strict signal handling, and secure mounts' },
{ name: 'NSA / CISA Kubernetes Hardening Standards', passed: nsa.SuccessCount, failed: nsa.FailCount, desc: 'Requires read-only file systems, non-privilege escalation constraints, and default-deny policies' },
{ name: 'Kubernetes Restricted Pod Security Standard (PSS)', passed: pss.SuccessCount, failed: pss.FailCount, desc: 'Requires drop-all capabilities, non-root user enforcement, and cryptographic validation boundary mapping' }
];

rules.forEach((rule, idx) => {
const total = rule.passed + rule.failed;
const rate = total > 0 ? Math.round((rule.passed / total) * 100) : 100;
const isCompliant = rule.failed === 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift

Missing telemetry is rendered as a positive measured result across four modules. One design decision drives all four sites: when data is absent, the code substitutes a plausible-looking value instead of an explicit "not measured" state. On a FIPS and CIS compliance dashboard, this converts an absent scan into an apparent pass.

  • web/assets/js/tables.js#L186-L220: stop defaulting parseSummaryControls to 12/0, 19/0, and 16/0, and stop deriving rate = 100 and isCompliant = true from a 0/0 control count. Carry a hasData flag and render a "Not Scanned" badge.
  • web/assets/js/fallbacks.js#L47-L70: replace the invented openssl_version: '3.1.2', cpu_cores: 4, ram_gb: 16, and architecture: 'x86_64' defaults with 'N/A' or null sentinels.
  • web/assets/js/charts.js#L237-L238: remove the || 21500 and || 7400 Ed25519 fallbacks so a missing benchmark reads as absent rather than as a measurement.
  • web/assets/js/tests.js#L124-L131: stop setting passRate to 100 when total is 0, and stop applying the success color unconditionally.

Adopt one convention for the whole dashboard: absent data renders a neutral pending state, and only measured data renders a pass or fail verdict.

📍 Affects 4 files
  • web/assets/js/tables.js#L186-L220 (this comment)
  • web/assets/js/fallbacks.js#L47-L70
  • web/assets/js/charts.js#L237-L238
  • web/assets/js/tests.js#L124-L131
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 186 - 220, Adopt a neutral “Not
Scanned” state for absent dashboard data across all sites: in
web/assets/js/tables.js lines 186-220, remove the 12/0, 19/0, and 16/0 defaults
in parseSummaryControls, track hasData, and avoid treating 0/0 as a 100%
compliant result; render a “Not Scanned” badge instead. In
web/assets/js/fallbacks.js lines 47-70, replace invented openssl_version,
cpu_cores, ram_gb, and architecture defaults with 'N/A' or null. In
web/assets/js/charts.js lines 237-238, remove the 21500 and 7400 Ed25519
fallbacks. In web/assets/js/tests.js lines 124-131, leave passRate unset or
pending when total is zero and apply success styling only to measured results.

Comment thread web/assets/js/tables.js
Comment on lines +412 to +413
<div>${escapeHTML(queryName)}</div>
${queryUrl !== '#' ? `<a href="${escapeHTML(queryUrl)}" target="_blank" class="text-[10px] text-indigo-400 hover:underline flex items-center gap-1">Docs <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"/></svg></a>` : ''}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

escapeHTML does not block a javascript: URL in href.

Line 413 escapes queryUrl, which prevents attribute breakout. It does not validate the URL scheme. The string javascript:alert(document.cookie) contains no character that escapeHTML rewrites, so it reaches the href unchanged and executes when the "Docs" link is clicked. query.query_url comes from the KICS report through data.json.

Validate the scheme before rendering.

🔒 Proposed fix
+          const safeHref = (u) => {
+            try {
+              const parsed = new URL(u, window.location.href);
+              return ['http:', 'https:'].includes(parsed.protocol) ? parsed.href : '';
+            } catch {
+              return '';
+            }
+          };
+          const docsHref = safeHref(queryUrl);
@@
-                      ${queryUrl !== '#' ? `<a href="${escapeHTML(queryUrl)}" target="_blank" class="...">Docs <svg ...></svg></a>` : ''}
+                      ${docsHref ? `<a href="${escapeHTML(docsHref)}" target="_blank" rel="noopener noreferrer" class="...">Docs <svg ...></svg></a>` : ''}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/assets/js/tables.js` around lines 412 - 413, Validate queryUrl’s scheme
before rendering the Docs link in the template, allowing only safe HTTP(S) URLs
and treating invalid or unsafe schemes such as javascript: as unavailable. Keep
escapeHTML for the rendered href and preserve the existing queryUrl !== '#'
conditional behavior for valid URLs.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tasks/Taskfile.doc.yml`:
- Line 25: Update the serve task command to run the tracked dashboard entry
point from the repository root by invoking server.js directly, replacing the
web-directory cli.js invocation.
- Line 12: Update the description for the parse_benchmark_data task to
accurately describe the outputs produced by benchmark/parser.py and
benchmark/parser_signatures.py, naming the structured benchmark metrics and
signature CSV rather than claiming both outputs are CSV files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 73d24052-c6ae-42f6-aaf2-5c5c8d37afef

📥 Commits

Reviewing files that changed from the base of the PR and between de2714b and 7e27afa.

📒 Files selected for processing (1)
  • tasks/Taskfile.doc.yml

Comment thread tasks/Taskfile.doc.yml

parse_benchmark_data:
desc: Parse benchmark results to be csv files results.csv and signatures.csv.
desc: "Parse benchmark results to be csv files results.csv and signatures.csv."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the parse_benchmark_data description.

Line [12] says that the task creates results.csv and signatures.csv. benchmark/parser.py produces structured benchmark metrics, while benchmark/parser_signatures.py writes the signature CSV. Update the description to name the actual outputs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tasks/Taskfile.doc.yml` at line 12, Update the description for the
parse_benchmark_data task to accurately describe the outputs produced by
benchmark/parser.py and benchmark/parser_signatures.py, naming the structured
benchmark metrics and signature CSV rather than claiming both outputs are CSV
files.

Comment thread tasks/Taskfile.doc.yml
serve:
desc: "Run local Node.js preview server for the web dashboard"
cmds:
- cd web && node cli.js --serve No newline at end of file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

Fix the serve task entry point.

Line [25] runs cd web && node cli.js --serve. The tracked dashboard server is server.js at the repository root, so this task does not launch the dashboard entry point. Run node server.js from the repository root.

Proposed fix
-      - cd web && node cli.js --serve
+      - node server.js
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tasks/Taskfile.doc.yml` at line 25, Update the serve task command to run the
tracked dashboard entry point from the repository root by invoking server.js
directly, replacing the web-directory cli.js invocation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant