Skip to content

fix(hardened): preserve multiple recommendations per base image#637

Merged
a-oren merged 1 commit into
guacsec:mainfrom
a-oren:TC-4933
Jun 29, 2026
Merged

fix(hardened): preserve multiple recommendations per base image#637
a-oren merged 1 commit into
guacsec:mainfrom
a-oren:TC-4933

Conversation

@a-oren

@a-oren a-oren commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix HashMap.put() overwrite bug in HardenedImageResponseHandler.parseAndInvertMapping() — when multiple hardened images share the same compare_to base image, all recommendations are now preserved instead of only the last one
  • Changed data structures from single-value to list-based (Map<String, IndexedRecommendation>Map<String, List<IndexedRecommendation>>) throughout the hardened image pipeline
  • Fixed ProviderResponseHandler.filterCves() to preserve allRecommendations when creating filtered PackageItem copies, preventing data loss on CVE-filtered responses

Jira

TC-4933

Test plan

  • All 57 related tests pass (HardenedImageProviderTest, HardenedImageIntegrationTest, RecommendationAggregationTest, ProviderResponseHandlerTest)
  • New test testParseAndInvertMappingPreservesMultipleRecommendationsPerBase validates 1→N mapping
  • New test testLookupBySbomIdReturnsMultipleRecommendations validates end-to-end multi-recommendation lookup
  • Manually tested against real Hummingbird data — confirmed 4 hardened nginx recommendations appear in response
  • Single-recommendation cases continue to work (backward-compatible constructors)
  • Pre-existing test failure (testInvalidGzipDeflatedContentEncoding) is unrelated — fails on main as well

🤖 Generated with Claude Code

Summary by Sourcery

Support multiple hardened image recommendations per base image and propagate them through the provider and aggregation pipeline.

Bug Fixes:

  • Fix hardened image response parsing to avoid overwriting entries when multiple hardened images share the same compare_to base image.
  • Ensure CVE filtering preserves allRecommendations in PackageItem so multi-recommendation data is not lost in filtered responses.

Enhancements:

  • Change hardened image index and lookup APIs to store and return lists of recommendations per base image instead of single values.
  • Normalize recommendation maps in RecommendationAggregation to handle both legacy single-value and new list-based formats, while preserving a primary recommendation.
  • Extend PackageItem to carry allRecommendations and update aggregation/reporting logic to use this list when building provider-level recommendation reports.

Tests:

  • Update hardened image provider tests to work with list-based recommendations and add coverage for multiple recommendations per base image and SBOM ID lookups.

@sourcery-ai

sourcery-ai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR fixes hardened-image recommendation loss when multiple hardened images share the same base image, by making the hardened index and downstream pipeline list-valued instead of single-valued, and ensures filtered responses preserve all recommendations.

Sequence diagram for multi-recommendation lookupBySbomId

sequenceDiagram
  actor Client
  participant HardenedImageProvider
  participant HardenedImageIndex

  Client->>HardenedImageProvider: lookupBySbomId(sbomId)
  HardenedImageProvider->>HardenedImageProvider: extract baseImageRef from sbomId
  HardenedImageProvider->>HardenedImageIndex: get(baseImageRef)
  HardenedImageIndex-->>HardenedImageProvider: List<IndexedRecommendation>
  HardenedImageProvider-->>Client: Map<PackageRef, List<IndexedRecommendation>>
Loading

File-Level Changes

Change Details Files
Allow multiple hardened recommendations per base image and per SBOM lookup by making the hardened index and response handler list-valued.
  • Change HardenedImageIndex to store Map<String, List> and return empty lists for missing keys instead of null.
  • Update HardenedImageProvider.refresh, lookup, lookupBySbomId, and fetchAndParseData to use List and propagate list-valued maps.
  • Modify HardenedImageResponseHandler.parseAndInvertMapping to build Map<String, List> and accumulate recommendations via computeIfAbsent rather than overwriting with put().
  • Adjust hardened provider tests and integration tests to expect list-valued lookups and indexes, and add tests covering multiple recommendations per base image and multi-result lookupBySbomId.
src/main/java/io/github/guacsec/trustifyda/model/trustify/HardenedImageIndex.java
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProvider.java
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageResponseHandler.java
src/test/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProviderTest.java
src/test/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageIntegrationTest.java
Normalize recommendation aggregation to handle both legacy single recommendation and new multi-recommendation formats, and propagate all recommendations into PackageItem and provider summaries.
  • Change RecommendationAggregation to operate on Map<PackageRef, List> and introduce normalizeRecommendations to adapt legacy Map<PackageRef, IndexedRecommendation> into list form.
  • Update setTrustedContent to iterate over all recommendations for a dependency, preserving all in PackageItem.allRecommendations while still using the first as the primary recommendation and for vulnerability remediation mapping.
  • Extend PackageItem to include an allRecommendations field with a compact constructor that defaults it to a singleton list, plus backward-compatible constructors that keep existing call sites working.
  • Update ProviderResponseHandler.filterCves to carry over PackageItem.allRecommendations when creating filtered copies, avoiding data loss on CVE-filtered responses.
  • Change ProviderResponseHandler.buildRecommendationsMap to use PackageItem.allRecommendations and emit one RecommendationReport per stored recommendation.
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/RecommendationAggregation.java
src/main/java/io/github/guacsec/trustifyda/model/PackageItem.java
src/main/java/io/github/guacsec/trustifyda/integration/providers/ProviderResponseHandler.java

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've found 2 issues, and left some high level feedback:

  • In RecommendationAggregation.normalizeRecommendations, any map entries whose values are neither List nor IndexedRecommendation are silently discarded; consider logging or handling this case explicitly so unexpected payloads don't get dropped without visibility.
  • HardenedImageIndex.get now returns an empty list by default, but HardenedImageProvider.lookup delegates directly to it; if callers distinguish between 'no entry' and 'entry with zero recommendations', you may want a method or contract comment that clarifies this behavior or exposes a null-vs-empty distinction.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In RecommendationAggregation.normalizeRecommendations, any map entries whose values are neither List nor IndexedRecommendation are silently discarded; consider logging or handling this case explicitly so unexpected payloads don't get dropped without visibility.
- HardenedImageIndex.get now returns an empty list by default, but HardenedImageProvider.lookup delegates directly to it; if callers distinguish between 'no entry' and 'entry with zero recommendations', you may want a method or contract comment that clarifies this behavior or exposes a null-vs-empty distinction.

## Individual Comments

### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/model/trustify/HardenedImageIndex.java" line_range="31-40" />
<code_context>
+  private volatile Map<String, List<IndexedRecommendation>> index = new ConcurrentHashMap<>();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Returning an unmodifiable map still exposes mutable lists, allowing external mutation of the index.

In `HardenedImageIndex#getAll()`, `Collections.unmodifiableMap(index)` only protects the map structure; the `List<IndexedRecommendation>` values remain mutable and can be changed by callers, undermining the index’s invariants and thread-safety. If external code should not modify the index, also make the lists unmodifiable (e.g. via `Collections.unmodifiableList`) or return a fully defensive copy of both the map and its lists.

Suggested implementation:

```java
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

```

```java
  public void replaceAll(Map<String, List<IndexedRecommendation>> newData) {
    ConcurrentHashMap<String, List<IndexedRecommendation>> newIndex = new ConcurrentHashMap<>();

    for (Map.Entry<String, List<IndexedRecommendation>> entry : newData.entrySet()) {
      List<IndexedRecommendation> recommendations = entry.getValue();

      // Normalize null to an empty, unmodifiable list to keep invariants simple.
      if (recommendations == null) {
        recommendations = Collections.emptyList();
      }

      // Store an unmodifiable view so external code cannot mutate the internal index.
      newIndex.put(entry.getKey(), Collections.unmodifiableList(recommendations));
    }

    this.index = newIndex;
  }

  /**
   * Returns a snapshot of the current index. The returned map and its lists are
   * unmodifiable, so callers cannot mutate the internal state.
   */
  public Map<String, List<IndexedRecommendation>> getAll() {
    Map<String, List<IndexedRecommendation>> snapshot = new HashMap<>();

    for (Map.Entry<String, List<IndexedRecommendation>> entry : index.entrySet()) {
      // Lists in the index are already unmodifiable, but we still wrap defensively
      // to ensure the snapshot cannot be mutated even if the internal representation changes.
      snapshot.put(entry.getKey(), Collections.unmodifiableList(entry.getValue()));
    }

    return Collections.unmodifiableMap(snapshot);
  }

```

I assumed an existing `getAll()` that previously returned `Collections.unmodifiableMap(index)`. If its signature or name differ, adjust the `getAll` method accordingly. Also, if `null` lists should be treated differently (e.g., preserved as `null` instead of normalized to empty lists), tweak the `replaceAll` handling to match your domain requirements.
</issue_to_address>

### Comment 2
<location path="src/test/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProviderTest.java" line_range="443-452" />
<code_context>
+  /// Verifies that lookupBySbomId returns all recommendations when multiple hardened images match.
</code_context>
<issue_to_address>
**suggestion (testing):** Strengthen assertions in `testLookupBySbomIdReturnsMultipleRecommendations` to verify the actual recommendations returned.

Right now the test only checks that two recommendations are returned, which verifies the 1→N behavior but not that the actual recommendations are correct. Please also assert on the contents—for example, extract the package names from `recommendations` and verify they match `hardened-nginx` and `hardened-nginx-v2` (order-independent). This will catch cases where duplicates or wrong recommendations are returned but the `size() == 2` assertion still passes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov-commenter

codecov-commenter commented Jun 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.78261% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.86%. Comparing base (97fd6d6) to head (74b0b4f).

Files with missing lines Patch % Lines
.../providers/trustify/RecommendationAggregation.java 77.27% 1 Missing and 4 partials ⚠️
...integration/providers/ProviderResponseHandler.java 77.77% 1 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #637      +/-   ##
============================================
+ Coverage     56.77%   56.86%   +0.09%     
- Complexity      815      823       +8     
============================================
  Files            94       94              
  Lines          4793     4815      +22     
  Branches        618      624       +6     
============================================
+ Hits           2721     2738      +17     
- Misses         1800     1802       +2     
- Partials        272      275       +3     
Flag Coverage Δ
integration-tests 56.86% <84.78%> (+0.09%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...iders/trustify/hardened/HardenedImageProvider.java 67.14% <100.00%> (ø)
...rustify/hardened/HardenedImageResponseHandler.java 75.47% <100.00%> (ø)
...o/github/guacsec/trustifyda/model/PackageItem.java 100.00% <100.00%> (ø)
.../trustifyda/model/trustify/HardenedImageIndex.java 85.71% <100.00%> (ø)
...integration/providers/ProviderResponseHandler.java 68.96% <77.77%> (-0.03%) ⬇️
.../providers/trustify/RecommendationAggregation.java 83.87% <77.27%> (-2.21%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@a-oren

a-oren commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @sourcery-ai[bot] review —

  1. normalizeRecommendations silent discard: Classified as suggestion — proposes adding logging for unexpected payload types. No convention in CONVENTIONS.md requiring logging of unrecognized types in type-dispatching methods. The method Javadoc documents the two supported formats. No sub-task created.

  2. HardenedImageIndex.get empty list vs no-entry: Classified as suggestion — proposes API clarification for null-vs-empty distinction. No convention in CONVENTIONS.md for return-type contracts on index lookups. No sub-task created.

@a-oren

a-oren commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-4933 (commit 354d54b)

Check Result Details
Review Feedback PASS 4 suggestions from sourcery-ai, none upgraded to code change requests
Root-Cause Investigation N/A No sub-tasks created
Scope Containment WARN 5 out-of-scope files (all justified: pipeline propagation + test updates)
Diff Size PASS 335 lines across 8 files, proportionate for pipeline-wide type refactor
Commit Traceability FAIL Commit 354d54b does not reference TC-4933
Sensitive Patterns PASS No secrets detected
CI Status PASS 3/3 checks pass (Integration Tests, Sourcery review, commitlint)
Acceptance Criteria PASS 4/4 criteria met
Test Quality PASS No repetitive tests, all doc comments present, Eval Quality: N/A
Test Change Classification ADDITIVE 2 new tests added, existing test modifications are mechanical type adaptations
Verification Commands N/A None specified

Overall: FAIL

Commit 354d54b ("fix(hardened): preserve multiple recommendations per base image") does not reference the Jira task ID TC-4933 anywhere in its subject or body. All other checks pass.


This comment was AI-generated by sdlc-workflow/verify-pr v0.11.0.

@ruromero

ruromero commented Jun 25, 2026

Copy link
Copy Markdown
Collaborator

PR Verification Report — TC-4933

Task: TC-4933 — Preserve multiple hardened image recommendations per base image
Commit: 354d54bfix(hardened): preserve multiple recommendations per base image
Overall: FAIL (Commit Traceability)

Verdicts

Check Verdict Summary
Scope Containment ⚠️ WARN All 3 task files present; 5 additional files changed (model/test updates)
Diff Size ✅ PASS 222 additions / 100 deletions across 8 files — proportionate
Commit Traceability ❌ FAIL Commit message does not reference TC-4933
Sensitive Patterns ✅ PASS No secrets or credentials detected
CI Status ✅ PASS 3/3 checks pass (Integration Tests, Sourcery, commitlint)
Acceptance Criteria ✅ PASS All 4 criteria verified in code
Verification Commands ➖ N/A No verification commands specified
Test Quality ⚠️ WARN /// doc comments used instead of /** */ Javadoc convention
Test Change Classification ✅ ADDITIVE 2 new test methods; existing tests mechanically adapted

Findings

Scope Containment — WARN

All 3 task-specified files are present. 5 additional files are reasonable supporting changes for the type signature refactor:

  • RecommendationAggregation.java — normalizes legacy/new format
  • PackageItem.java — extended with allRecommendations field
  • HardenedImageIndex.java — updated internal index type
  • HardenedImageProviderTest.java — tests updated + 2 new
  • HardenedImageIntegrationTest.java — adapted to list type
Commit Traceability — FAIL

The single commit 354d54b has message "fix(hardened): preserve multiple recommendations per base image" — does not contain TC-4933. The commit should be amended to include the task reference (e.g., fix(hardened): preserve multiple recommendations per base image [TC-4933]).

Test Quality — WARN

Test methods in HardenedImageProviderTest.java use /// (Java 23 markdown doc comments, JEP 467) instead of /** */ Javadoc style. The project targets Java 21 and all other test files use /** */.

Test Change Classification — ADDITIVE
  • 2 new test methods: testParseAndInvertMappingPreservesMultipleRecommendationsPerBase, testLookupBySbomIdReturnsMultipleRecommendations
  • 16 existing tests mechanically adapted (single → list types); no assertions weakened, no tests removed

This comment was AI-generated by sdlc-workflow/verify-pr v0.11.0.

@ruromero ruromero 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.

Fix the commit traceability and the /// comment. Please make sure to add it to the conventions file if it isn't there to prevent it from happening again.

…933)

HashMap.put() in HardenedImageResponseHandler silently overwrote
earlier entries when multiple hardened images shared the same
compare_to base image. Changed to list-based data structures
throughout the pipeline so all recommendations are preserved.

Also fixed filterCves to carry allRecommendations through to
buildRecommendationsMap, preventing data loss on CVE-filtered
responses.

Added test comment style convention to CONVENTIONS.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@a-oren

a-oren commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

[sdlc-workflow/verify-pr] Re: @ruromero review — Classified as code change request — reviewer directs fixing commit traceability (missing TC-4933 reference), reverting /// comments to //, and adding the convention to CONVENTIONS.md. All three items have been addressed in commit 74b0b4f.

@a-oren

a-oren commented Jun 28, 2026

Copy link
Copy Markdown
Contributor Author

Verification Report for TC-4933 (commit 74b0b4f)

Check Result Details
Review Feedback PASS 4 classified items (3 suggestions, 1 code change request addressed in 74b0b4f)
Root-Cause Investigation N/A No sub-tasks created
Scope Containment ⚠️ WARN All 3 task files present; 6 additional files changed (model/test updates, CONVENTIONS.md per reviewer request)
Diff Size ⚠️ WARN 303 lines across 9 files — proportionate for pipeline-wide type refactor
Commit Traceability ✅ PASS Commit 74b0b4f references TC-4933 in headline
Sensitive Patterns ✅ PASS No secrets or credentials detected
CI Status ✅ PASS 3/3 checks pass (Integration Tests, Sourcery review, commitlint)
Acceptance Criteria ✅ PASS 4/4 criteria met
Test Quality ✅ PASS No repetitive tests, all doc comments present, Eval Quality: N/A
Test Change Classification ✅ ADDITIVE 2 new tests added, 16 existing tests mechanically adapted
Verification Commands ➖ N/A None specified

Overall: WARN

Scope Containment and Diff Size are WARN because 6 files beyond the 3 task-specified files were changed. All additional files are justified: RecommendationAggregation.java, PackageItem.java, and HardenedImageIndex.java are required for the type-signature refactor; test files were updated accordingly; CONVENTIONS.md was added per reviewer request (ruromero).

Scope Containment — WARN

All 3 task-specified files are present. 6 additional files:

  • CONVENTIONS.md — test comment convention added per reviewer request
  • RecommendationAggregation.java — normalizes legacy/new recommendation format
  • PackageItem.java — extended with allRecommendations field
  • HardenedImageIndex.java — updated internal index type
  • HardenedImageProviderTest.java — tests updated + 2 new
  • HardenedImageIntegrationTest.java — adapted to list type
Acceptance Criteria — PASS
  1. ✅ Multiple recommendations preserved: computeIfAbsent().add() replaces put()
  2. ✅ Single-recommendation cases work: tests verify lists of size 1
  3. ✅ All existing tests pass: CI Integration Tests pass (12m15s)
  4. ✅ New 1→N tests: testParseAndInvertMappingPreservesMultipleRecommendationsPerBase and testLookupBySbomIdReturnsMultipleRecommendations
Test Change Classification — ADDITIVE
  • 2 new test methods added
  • 16 existing tests mechanically adapted (single → list types); no assertions weakened, no tests removed
  • 1 line changed in HardenedImageIntegrationTest.java (mechanical adaptation)

This comment was AI-generated by sdlc-workflow/verify-pr v0.11.0.

@a-oren a-oren requested a review from ruromero June 29, 2026 09:09
@a-oren a-oren merged commit f0ee562 into guacsec:main Jun 29, 2026
3 checks passed
@a-oren a-oren deleted the TC-4933 branch June 29, 2026 09:53
ruromero pushed a commit to ruromero/trustify-dependency-analytics that referenced this pull request Jun 30, 2026
…933) (guacsec#637)

HashMap.put() in HardenedImageResponseHandler silently overwrote
earlier entries when multiple hardened images shared the same
compare_to base image. Changed to list-based data structures
throughout the pipeline so all recommendations are preserved.

Also fixed filterCves to carry allRecommendations through to
buildRecommendationsMap, preventing data loss on CVE-filtered
responses.

Added test comment style convention to CONVENTIONS.md.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
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.

3 participants