Skip to content

Comments

feat: add HTML report output format#23

Open
clarabennettdev wants to merge 1 commit intorezmoss:mainfrom
clarabennettdev:feat/html-report
Open

feat: add HTML report output format#23
clarabennettdev wants to merge 1 commit intorezmoss:mainfrom
clarabennettdev:feat/html-report

Conversation

@clarabennettdev
Copy link

@clarabennettdev clarabennettdev commented Feb 22, 2026

Adds --format html for self-contained HTML reports (diff + stats mode). Embedded CSS, no external deps, XSS-safe. 13 tests passing.

Closes #4

Adds --format html for generating self-contained HTML reports suitable
for auditors, compliance documentation, and non-technical stakeholders.

Both diff mode (two SBOMs) and stats mode (single SBOM) are supported.

The HTML report includes:
- Side-by-side SBOM comparison overview
- Summary cards for added/removed/changed counts
- Key findings section
- Drift summary with severity badges
- Dependency depth risk table
- Policy violation tables (errors + warnings)
- Collapsible component detail tables with type and license info
- XSS-safe output (all values HTML-escaped)

The report is completely self-contained (embedded CSS, no external
dependencies) and can be opened directly in any browser, attached
to emails, or stored as compliance artifacts.

Closes rezmoss#4
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds HTML report output format support to sbomlyze, enabling generation of self-contained, audit-friendly HTML reports. The implementation follows established patterns from other output formats (markdown, SARIF, JUnit) and includes comprehensive test coverage.

Changes:

  • Added FormatHTML constant and HTML generation functions (GenerateHTML for diffs, GenerateHTMLStats for single SBOMs)
  • Integrated HTML format into CLI with --format html flag and updated help text
  • Added 13 comprehensive tests covering structure, XSS protection, policy violations, drift summary, and edge cases

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/output/format.go Adds FormatHTML constant to enum of output formats
internal/output/html.go Implements HTML generation with embedded CSS, proper escaping, and support for both diff and stats modes
internal/output/html_test.go Comprehensive test suite covering valid structure, XSS protection, component rendering, policy violations, and edge cases
cmd/sbomlyze/main.go Integrates HTML format into switch statement for both single-file and diff modes
internal/cli/usage.go Updates help text to include HTML format in list and examples
testdata/snapshots/no_args.stderr Updates help text snapshot to include HTML format
testdata/snapshots/help_long.stderr Updates long help text snapshot to include HTML format

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +221 to +224
fmt.Fprintf(sb, "<tr><td><strong>Hash Coverage</strong></td><td>%s</td><td>%s</td></tr>\n",
formatPct(b.Stats.WithHashes, b.Stats.TotalComponents),
formatPct(a.Stats.WithHashes, a.Stats.TotalComponents))
sb.WriteString("</table>\n")
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The HTML diff overview is missing CPE Coverage, which is included in both the text and markdown formats. This creates an inconsistency across output formats. The CPE Coverage metric should be added to the overview table to match the completeness of other formats.

Copilot uses AI. Check for mistakes.
Comment on lines +205 to +224
sb.WriteString("<h2>SBOM Comparison</h2>\n<table>\n")
sb.WriteString("<tr><th></th><th>Before</th><th>After</th></tr>\n")
fmt.Fprintf(sb, "<tr><td><strong>File</strong></td><td>%s</td><td>%s</td></tr>\n",
html.EscapeString(filepath.Base(b.FileName)), html.EscapeString(filepath.Base(a.FileName)))
fmt.Fprintf(sb, "<tr><td><strong>File Size</strong></td><td>%s</td><td>%s</td></tr>\n",
formatFileSize(b.FileSize), formatFileSize(a.FileSize))
fmt.Fprintf(sb, "<tr><td><strong>Format</strong></td><td>%s</td><td>%s</td></tr>\n",
html.EscapeString(orNone(b.Info.ToolName)), html.EscapeString(orNone(a.Info.ToolName)))
fmt.Fprintf(sb, "<tr><td><strong>Components</strong></td><td>%d</td><td>%d</td></tr>\n",
b.Stats.TotalComponents, a.Stats.TotalComponents)
fmt.Fprintf(sb, "<tr><td><strong>PURL Coverage</strong></td><td>%s</td><td>%s</td></tr>\n",
formatPct(b.Stats.WithPURL, b.Stats.TotalComponents),
formatPct(a.Stats.WithPURL, a.Stats.TotalComponents))
fmt.Fprintf(sb, "<tr><td><strong>License Coverage</strong></td><td>%s</td><td>%s</td></tr>\n",
formatPct(b.Stats.TotalComponents-b.Stats.WithoutLicense, b.Stats.TotalComponents),
formatPct(a.Stats.TotalComponents-a.Stats.WithoutLicense, a.Stats.TotalComponents))
fmt.Fprintf(sb, "<tr><td><strong>Hash Coverage</strong></td><td>%s</td><td>%s</td></tr>\n",
formatPct(b.Stats.WithHashes, b.Stats.TotalComponents),
formatPct(a.Stats.WithHashes, a.Stats.TotalComponents))
sb.WriteString("</table>\n")
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The HTML diff overview is missing several fields that are present in text and markdown formats: OS (OSPrettyName), Source (SourceName), and Source Type (SourceType). This creates inconsistency across output formats. These fields should be added to the overview table for completeness.

Copilot uses AI. Check for mistakes.
Comment on lines +353 to +355
func writeHTMLRow(sb *strings.Builder, col1, col2 string) {
fmt.Fprintf(sb, "<tr><td>%s</td><td>%s</td></tr>\n", col1, col2)
}
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The writeHTMLRow helper function is called with unescaped data from parsed SBOM files in multiple places (lines 125-129, 151, 167, 190). Since SBOM files can contain arbitrary user-controlled strings, this creates XSS vulnerabilities. All data passed to writeHTMLRow should be HTML-escaped before being passed in, or writeHTMLRow itself should escape its parameters. The safest approach would be to have writeHTMLRow perform the escaping to prevent future mistakes.

Copilot uses AI. Check for mistakes.
Comment on lines +307 to +308
fmt.Fprintf(sb, "<details>\n<summary>%s (%d)</summary>\n<table>\n",
html.EscapeString(title), len(comps))
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The class parameter in writeHTMLComponentSection is passed but never used in the function body. This is dead code that should be removed to improve code clarity.

Suggested change
fmt.Fprintf(sb, "<details>\n<summary>%s (%d)</summary>\n<table>\n",
html.EscapeString(title), len(comps))
classAttr := ""
if class != "" {
classAttr = fmt.Sprintf(" class=\"%s\"", html.EscapeString(class))
}
fmt.Fprintf(sb, "<details%s>\n<summary>%s (%d)</summary>\n<table>\n",
classAttr, html.EscapeString(title), len(comps))

Copilot uses AI. Check for mistakes.
Comment on lines +362 to +364
func writeHTMLCardStyled(sb *strings.Builder, num, label, color string) {
fmt.Fprintf(sb, "<div class=\"summary-card\" style=\"border-color: %s;\"><div class=\"num\" style=\"color: %s;\">%s</div><div class=\"label\">%s</div></div>\n",
color, color, html.EscapeString(num), html.EscapeString(label))
Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

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

The color parameter in writeHTMLCardStyled is used directly in inline CSS without validation or escaping. While currently only called with the constant "var(--red)", this creates a potential CSS injection vulnerability if the function is ever called with user-controlled data. Consider validating that the color parameter matches expected CSS variable syntax or is from a whitelist.

Suggested change
func writeHTMLCardStyled(sb *strings.Builder, num, label, color string) {
fmt.Fprintf(sb, "<div class=\"summary-card\" style=\"border-color: %s;\"><div class=\"num\" style=\"color: %s;\">%s</div><div class=\"label\">%s</div></div>\n",
color, color, html.EscapeString(num), html.EscapeString(label))
// sanitizeCSSColor ensures that only safe CSS variable references of the form
// var(--name) are allowed, where name consists of [A-Za-z0-9_-]+.
// If the input does not match this pattern, a safe default is returned.
func sanitizeCSSColor(color string) string {
const defaultColor = "var(--red)"
if strings.HasPrefix(color, "var(--") && strings.HasSuffix(color, ")") {
name := color[len("var(--") : len(color)-1]
if name == "" {
return defaultColor
}
for _, ch := range name {
if (ch >= 'a' && ch <= 'z') ||
(ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') ||
ch == '-' || ch == '_' {
continue
}
return defaultColor
}
return color
}
return defaultColor
}
func writeHTMLCardStyled(sb *strings.Builder, num, label, color string) {
safeColor := sanitizeCSSColor(color)
fmt.Fprintf(sb, "<div class=\"summary-card\" style=\"border-color: %s;\"><div class=\"num\" style=\"color: %s;\">%s</div><div class=\"label\">%s</div></div>\n",
safeColor, safeColor, html.EscapeString(num), html.EscapeString(label))

Copilot uses AI. Check for mistakes.
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.

HTML/PDF Report Generation

1 participant