Skip to content

feat(html-reporter): show duplicate package versions in HTML report - #845

Merged
sonukapoor merged 6 commits into
OWASP:mainfrom
ELHart05:feature/issue-235-duplicate-package-versions-html
Jul 28, 2026
Merged

feat(html-reporter): show duplicate package versions in HTML report#845
sonukapoor merged 6 commits into
OWASP:mainfrom
ELHart05:feature/issue-235-duplicate-package-versions-html

Conversation

@ELHart05

Copy link
Copy Markdown
Contributor

Summary

When the same vulnerable package is installed at more than one version, the findings table lists each install on its own row and nothing ties them together. This adds a "Duplicate package versions" card above the findings table so you can see, in one place, which vulnerable packages exist at multiple versions and where each version comes from.

Why this change

Issue #235: the report had no consolidated view of a vulnerable package that appears at several versions across the tree. That is exactly the case where a single upgrade or an override can collapse several findings at once, but today you have to spot it by scanning the table yourself.

What changed

  • Add a "Duplicate package versions" card, rendered above the findings table. It groups the vulnerable findings by package name, keeps the ones present at two or more distinct versions, and lists each version with its severity and its origin (a direct dependency, or the parent it is pulled in through).
  • Sort the most-duplicated packages first for a stable, useful order.
  • The card is derived at render time from the findings already in the report, so there is no change to the scan or to the shape of report.json.
  • The section is omitted entirely when no package is installed at multiple versions.
  • Reuses the existing card and severity-badge styling, so it inherits the report's light/dark theming and needs no new client-side JS.

Validation

  • npm run build (strict tsc) passes.
  • The reporter suite passes, including 5 new tests: grouping, same-version-not-duplicated, rendering with the version count and origin, HTML-escaping, and empty-case omission. The test-hygiene pretest gate passes.
  • Eyeballed the rendered fragment on a mixed sample (a three-version and a two-version package plus a single-version one) to confirm ordering, origins, and escaping.

User-facing impact

Does this change:

  • affect scanning behavior
  • affect output formatting
  • affect JSON output
  • affect docs only

Notes

The consolidation is a view over existing finding data, so it does not add a field to report.json; the raw per-version findings are already there. If you would rather also expose a computed duplicates array in report.json for tooling, I am happy to add that as a follow-up.

Closes #235

When the same vulnerable package is installed at more than one version,
the findings table lists each install separately and there is no single
place that surfaces the duplication. This adds a "Duplicate package
versions" card above the findings table: it groups vulnerable findings by
package name, keeps the ones present at two or more versions, and shows
each version with its severity and where it comes from (a direct
dependency, or the parent it is pulled in through).

The card is computed at render time from the findings already in the
report, so there is no scan-logic change and report.json keeps its
current shape. It is omitted when nothing is installed at multiple
versions.

Closes OWASP#235
@ELHart05
ELHart05 requested a review from sonukapoor as a code owner July 18, 2026 14:06

@sonukapoor sonukapoor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Really nice card @ELHart05 - reusing sev-badge, escaping everything, and hiding it when there are no duplicates is exactly right, and CI is green.

One correctness fix before it goes in: the origin line derives the parent from path[path.length - 2] (the immediate parent), but we already compute the top-level responsible dependency as finding.primaryParent and render it as "Parent: X" in the findings table just above. Using the immediate parent means the same finding can say "Parent: webpack" in the table but "via loader-utils" in this card, and for a hoisted 2-node transitive path (e.g. ['my-app','lodash']) it prints "via my-app" - naming the project itself as the parent. primaryParent is already on SerializedFinding, so it is a one-line swap that also drops the re-derivation:

function duplicateOrigin(finding: SerializedFinding): string {
  if (finding.relationship === 'direct') return 'direct dependency';
  if (finding.primaryParent) return `via ${finding.primaryParent}`;
  return finding.relationship === 'transitive' ? 'transitive dependency' : 'unknown origin';
}

Could you also add a test with a 4+ node path and a 2-node hoisted transitive path? The current fixtures all use 3-node paths where the immediate parent and primaryParent happen to coincide, so the difference does not show up.

Two optional nits if you are in there anyway: pluralize(duplicates.length, "package") is already imported and would replace the manual plural, and sorting the version rows (by severity then version) makes the output deterministic. Thanks again - genuinely useful addition once the origin is squared away.

Use finding.primaryParent for the duplicate-versions origin line instead of
re-deriving the immediate parent from the dependency path. The immediate
parent could disagree with the Parent line in the findings table, and on a
hoisted 2-node transitive path it named the project itself.

Also sort the version rows (severity then version) for deterministic output
and reuse pluralize for the package count.

Adds tests for a 4-node path and a hoisted 2-node transitive path, where the
immediate parent and primaryParent differ.
@ELHart05

Copy link
Copy Markdown
Contributor Author

Thanks for the careful review. You're right, the immediate parent was the wrong thing to show.

  • Origin now uses finding.primaryParent, exactly as you suggested, so the card matches the "Parent: X" line in the table. Dropped the path re-derivation.
  • Added two tests: a 4-node path (webpack -> loader-utils -> lodash) where the immediate parent and primaryParent differ, and a hoisted 2-node transitive path that used to print "via my-app". Both now assert the correct origin.
  • Also took both nits: pluralize(duplicates.length, "package") for the count, and the version rows sort by severity then version for stable output.

Pushed. Let me know if you'd like anything else.

@sonukapoor sonukapoor left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is exactly right now. primaryParent matches the table, and the two new tests (the 4-node deep path and the 2-node hoisted path) nail the specific cases that were silently broken before. Both optional nits are in too (pluralize and the deterministic sort). Thanks for working through this @ELHart05.

@sonukapoor
sonukapoor merged commit 7b42add into OWASP:main Jul 28, 2026
6 checks passed
@sonukapoor

Copy link
Copy Markdown
Collaborator

Merged - thank you @ELHart05!

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.

feat: show duplicate vulnerable package consolidation in HTML report

2 participants