Skip to content

Rework the HTML report - #133

Merged
Tanq16 merged 1 commit into
mainfrom
html-output-fixes
Aug 5, 2026
Merged

Rework the HTML report#133
Tanq16 merged 1 commit into
mainfrom
html-output-fixes

Conversation

@Icarus7337

Copy link
Copy Markdown
Collaborator

HTML report format updates.

The rendered report was hard to read and leaked internals. Move HTML rendering
out of render.go into an html/template with embedded CSS and JS, and rework what
it puts on the page. Markdown output is unchanged.

- Heading is the scanned scope from _meta.json, not the run directory name; the
  run id and a human-readable generated stamp move to the footer.
- Severity counts become tiles that are also the severity filter, so the counts
  are no longer printed twice.
- Sidebar lists rules grouped by their cat-NN prefix with counts, a filter box,
  and jump-to-severity links, so no rule name has to be remembered.
- Cards carry a severity ribbon, fill the column, and hide description,
  provenance, subject and rule id behind a Details toggle.
- Subject line is a location: org/repo/file, or the scope plus the subject kind
  when the finding is not anchored to a file. Built from canonical fields, so
  fingerprints do not change.
- Search matches titles only. Light and dark themes with an explicit toggle.

html/template's contextual escaping also closes a gap: Rule.URL was previously
interpolated into href after only HTML-escaping, so a non-http scheme survived.

The document stays a single self-contained file with no external requests, which
is what makes it usable offline.

@github-actions github-actions 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.

Codex Review

Critical Issues

  • internal/report/assets/report.html:129 drops Rule.ScenarioID from HTML output. The previous renderer displayed it alongside the rule ID, so scenario-linked findings now lose meaningful traceability. Render the scenario when non-empty and add regression coverage.

Security

No security concerns flagged.

Suggestions

No suggestions.


Reviewed by Codex (gpt-5.6-sol)

@github-actions github-actions 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.

Claude Review

Critical issues

None. The rework is clean: renderHTML now returns ([]byte, error) and both the html and all cases in report.go are updated to propagate it; the new reportMeta fields (Platform, Scope, Generated) map to real engine.State fields; and all template funcs (location, ruleID, codeCaption, provRows, hasDetails, etc.) are registered and back existing helpers. hasDetails gates on exactly the four fields the Details block renders.

Security

  • Net improvement, not a regression: replacing hand-rolled html.EscapeString string-building with html/template means Rule.URL is now URL-sanitized in href context, so a javascript: scheme is neutered (previously it survived escaping). report.js/report.css are inlined via template.JS/template.CSS from trusted //go:embed assets, and all finding-derived text goes through auto-escaping. No new network calls or secrets handling.

Test coverage

Present and strong — html_test.go covers nil-field tolerance, the XSS/non-http URL case, empty-report shape, rule grouping, scope-vs-runID heading, and locationOf edge cases against concrete inputs rather than implementation shape.

No critical issues — LGTM pending human review.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The report package now renders a complete HTML report with metadata, findings, filters, navigation, evidence, remediation, provenance, and run details. It embeds CSS and JavaScript assets. Client-side behavior supports themes, filtering, searching, navigation, detail expansion, printing, and empty states. Report metadata now stores platform, scope, and generation time. HTML rendering errors are returned before output. Tests cover rendering, formatting, grouping, sanitization, fallbacks, and empty reports.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch html-output-fixes

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: 3

🧹 Nitpick comments (1)
internal/report/assets/report.html (1)

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

Give both search inputs an explicit accessible name.

Both inputs rely on the placeholder alone. The other controls in this template carry aria-label. Match them.

♿ Proposed change
-    <input id="ruleQ" class="side-input" type="search" placeholder="Filter rules…" autocomplete="off" spellcheck="false">
+    <input id="ruleQ" class="side-input" type="search" aria-label="Filter rules" placeholder="Filter rules…" autocomplete="off" spellcheck="false">
-    <input id="q" type="search" placeholder="Search finding titles…" autocomplete="off" spellcheck="false">
+    <input id="q" type="search" aria-label="Search finding titles" placeholder="Search finding titles…" autocomplete="off" spellcheck="false">

Also applies to: 84-84

🤖 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 `@internal/report/assets/report.html` at line 63, Update both search inputs,
including the controls identified by ruleQ and the corresponding input at the
other referenced location, to include explicit aria-label attributes matching
their filtering purpose; preserve their existing placeholder and other
attributes.
🤖 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 `@internal/report/assets/report.css`:
- Line 12: Darken the --faint CSS variable in both its definitions, including
the matching value used by the dark blocks, so small text reaches at least 4.5:1
contrast on the relevant light and dark surfaces while preserving all existing
token usages.
- Around line 26-27: Update the --sans and --mono font-family declarations to
quote all multi-word and proper-name families, including existing names such as
"Segoe UI" and "SF Mono", so every family flagged by value-keyword-case follows
the stylesheet’s quoting convention.

In `@internal/report/assets/report.js`:
- Around line 126-127: Update the beforeprint handler to synchronize the expand
control after calling setDetails(true): set expand.dataset.open to the open
state and update the button text to match all details being expanded, reusing
the existing symbols and control-update logic.

---

Nitpick comments:
In `@internal/report/assets/report.html`:
- Line 63: Update both search inputs, including the controls identified by ruleQ
and the corresponding input at the other referenced location, to include
explicit aria-label attributes matching their filtering purpose; preserve their
existing placeholder and other attributes.
🪄 Autofix

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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58cf8493-637f-40c2-8919-3f0abb859764

📥 Commits

Reviewing files that changed from the base of the PR and between 12c5a86 and 1c7b062.

📒 Files selected for processing (7)
  • internal/report/assets/report.css
  • internal/report/assets/report.html
  • internal/report/assets/report.js
  • internal/report/html.go
  • internal/report/html_test.go
  • internal/report/render.go
  • internal/report/report.go

--raised: #ffffff;
--ink: #11151a;
--muted: #5b6673;
--faint: #8a94a1;

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

Darken --faint; it fails WCAG AA for small text.

--faint is about 3.1:1 on white and about 3.8:1 on the dark surface. AA needs 4.5:1. The token drives 10.5–12px text: .eyebrow, .side-h, .side-n, .fid, .lbl-sec, and the footer. The same value at line 62 needs the matching change.

♿ Proposed token values
-  --faint: `#8a94a1`;
+  --faint: `#6b7480`;

Dark blocks at lines 40 and 62:

-    --faint: `#6d7783`;
+    --faint: `#8b95a1`;

Also applies to: 40-40

🤖 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 `@internal/report/assets/report.css` at line 12, Darken the --faint CSS
variable in both its definitions, including the matching value used by the dark
blocks, so small text reaches at least 4.5:1 contrast on the relevant light and
dark surfaces while preserving all existing token usages.

Comment on lines +26 to +27
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;

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

Quote the multi-word and proper-name font families to clear stylelint.

Stylelint flags six unquoted family names under value-keyword-case. Quoting matches the already-quoted "Segoe UI" and "SF Mono".

🎨 Proposed fix
-  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
-  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
+  --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif;
+  --mono: ui-monospace, "SFMono-Regular", "SF Mono", "Menlo", "Consolas", monospace;
📝 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
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
--sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", "Roboto", "Helvetica", "Arial", sans-serif;
--mono: ui-monospace, "SFMono-Regular", "SF Mono", "Menlo", "Consolas", monospace;
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 26-26: Expected "Roboto" to be "roboto" (value-keyword-case)

(value-keyword-case)


[error] 26-26: Expected "Helvetica" to be "helvetica" (value-keyword-case)

(value-keyword-case)


[error] 26-26: Expected "Arial" to be "arial" (value-keyword-case)

(value-keyword-case)


[error] 27-27: Expected "SFMono-Regular" to be "sfmono-regular" (value-keyword-case)

(value-keyword-case)


[error] 27-27: Expected "Menlo" to be "menlo" (value-keyword-case)

(value-keyword-case)


[error] 27-27: Expected "Consolas" to be "consolas" (value-keyword-case)

(value-keyword-case)

🤖 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 `@internal/report/assets/report.css` around lines 26 - 27, Update the --sans
and --mono font-family declarations to quote all multi-word and proper-name
families, including existing names such as "Segoe UI" and "SF Mono", so every
family flagged by value-keyword-case follows the stylesheet’s quoting
convention.

Source: Linters/SAST tools

Comment on lines +126 to +127
// Printing a report should not silently drop whatever sits behind a toggle.
addEventListener("beforeprint", function () { setDetails(true); });

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 | 🟡 Minor | ⚡ Quick win

Synchronize the expand control before printing.

beforeprint opens every details.more element. It does not update expand.dataset.open or the button text. After printing, the button can display Expand all while all details are open. The first click then has no visible effect.

Proposed fix
-  addEventListener("beforeprint", function () { setDetails(true); });
+  addEventListener("beforeprint", function () {
+    setDetails(true);
+    expand.dataset.open = "true";
+    expand.textContent = "Collapse all";
+  });
📝 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
// Printing a report should not silently drop whatever sits behind a toggle.
addEventListener("beforeprint", function () { setDetails(true); });
// Printing a report should not silently drop whatever sits behind a toggle.
addEventListener("beforeprint", function () {
setDetails(true);
expand.dataset.open = "true";
expand.textContent = "Collapse all";
});
🧰 Tools
🪛 ast-grep (0.45.0)

[error] 126-126: React's useState should not be directly called
Context: setDetails(true)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(usestate-direct-usage)

🤖 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 `@internal/report/assets/report.js` around lines 126 - 127, Update the
beforeprint handler to synchronize the expand control after calling
setDetails(true): set expand.dataset.open to the open state and update the
button text to match all details being expanded, reusing the existing symbols
and control-update logic.

@github-actions github-actions 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.

Gemini Review

Critical Issues

  • Nil pointer dereference on unreadable _meta.json: In internal/report/report.go, the call to engine.LoadState(runDir) ignores its returned error. While a missing file is handled safely by LoadState, a corrupted or unreadable _meta.json causes it to return a nil state and an error. Ignoring this error and immediately accessing state.Platform and state.Scope (introduced in this PR) will trigger a runtime panic. Initialize a fallback State struct if state == nil.

Security

  • No security concerns flagged. The migration to html/template natively handles contextual HTML escaping and safely neuters potentially harmful links (e.g., javascript: URIs in Rule.URL becoming #ZgotmplZ), acting as a robust replacement for the previous manual html.EscapeString approach.

Suggestions

  • Dynamic dark mode listener: The JS toggle in internal/report/assets/report.js evaluates the prefers-color-scheme media query once on load. Consider attaching an event listener (matchMedia("(prefers-color-scheme: dark)").addEventListener("change", ...)) to automatically update the theme if the user's OS preference changes dynamically while the report is open (assuming no manual override exists in localStorage).
  • emptyInline scope in tests: In internal/report/html_test.go, the emptyInline utility only targets and strips the first occurrence of a tag. This works perfectly for the current single-asset document shape, but using a loop or regex replacement would be more robust against future changes that introduce multiple <style> or <script> blocks.

Reviewed by Gemini (gemini-3.1-pro-preview)

@Tanq16
Tanq16 merged commit bf35d87 into main Aug 5, 2026
39 of 41 checks passed
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.

2 participants