Skip to content

feat(hardened): add deployment-time registry prefix replacement map (TC-5107) - #642

Merged
ruromero merged 5 commits into
guacsec:mainfrom
ruromero:TC-5107
Jul 9, 2026
Merged

feat(hardened): add deployment-time registry prefix replacement map (TC-5107)#642
ruromero merged 5 commits into
guacsec:mainfrom
ruromero:TC-5107

Conversation

@ruromero

@ruromero ruromero commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds configurable deployment-time registry prefix replacement for hardened image recommendations (TC-5107).

  • Accepts an ordered list of source→target prefix mappings via trustedcontent.recommendation.hardened.registry-map config property
  • Applies prefix replacement at serialization time (no cached data mutation) — new IndexedRecommendation objects are created with mapped PackageRef values
  • Returns bare image names when no map is configured
  • Preserves image tags unchanged during replacement
  • Skips recommendations for digest-only images (no tag qualifier, version is sha256:)
  • No vendor-specific strings in source code
  • Logs applied mappings at startup (INFO level)

Acceptance Criteria Coverage

  • Accept ordered list of source=target prefix mappings via app config
  • Apply prefix replacement at serialization time (no cached data mutation)
  • Return bare image names when no map configured
  • Preserve image tags unchanged
  • Skip recommendation when source image uses digest (@sha256:) without tag
  • No vendor-specific strings in source code
  • Log applied mappings at startup (INFO level)

Test Plan

  • 10 new unit tests in HardenedImageProviderTest
  • Digest-only PURL returns empty recommendations
  • Digest+tag PURL still gets recommendations (tag enables lookup)
  • Registry map replaces repository_url prefix in recommendations
  • Empty/null map returns unchanged PURLs
  • Tag qualifier preserved after replacement
  • Cached index data is not mutated
  • All 324 existing tests pass (mvn verify)

🤖 Generated with Claude Code

Summary by Sourcery

Introduce configurable registry prefix mapping for hardened image recommendations and adjust lookup behavior for digest-pinned images.

New Features:

  • Add a registry prefix replacement map configuration for hardened image recommendations, driven by application properties.
  • Log configured hardened image registry mappings at startup, including when the map is empty.

Enhancements:

  • Apply registry prefix mapping to recommended image PURLs at lookup/serialization time without mutating the cached index.
  • Ensure digest-only source images without tags are skipped while digest-plus-tag images remain eligible for recommendations.

Documentation:

  • Document the hardened registry prefix replacement map property in application.properties with usage notes.

Tests:

  • Extend hardened image provider tests to cover registry mapping behavior, digest-pinned image handling, tag preservation, and immutability of cached index data.

…TC-5107)

Add configurable registry prefix replacement for hardened image
recommendations, applied at response serialization time to avoid
mutating cached index data.

- Accept ordered map via `trustedcontent.recommendation.hardened.registry-map`
- Replace `repository_url` qualifier prefix at lookup time
- Skip digest-only images (no tag qualifier) from recommendations
- Preserve image tags unchanged during replacement
- Log configured mappings at startup (INFO level)
- 10 new unit tests covering all acceptance criteria

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sourcery-ai

sourcery-ai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Implements configurable deployment-time registry prefix remapping for hardened image recommendations, including digest-aware lookup behavior, non-mutating application of a registry map at serialization time, configuration surface changes, startup logging, and comprehensive unit tests around mapping and digest handling.

Sequence diagram for hardened image lookup with registry prefix mapping

sequenceDiagram
  actor Client
  participant HardenedImageProvider
  participant HummingbirdClient
  participant HardenedImageResponseHandler

  Client->>HardenedImageProvider: lookupBySbomId(sbomId)
  HardenedImageProvider->>HardenedImageProvider: buildDockerRef(pkgRef)
  alt [digest-only image]
    HardenedImageProvider-->>Client: emptyMap
  else [tag present or no digest]
    HardenedImageProvider->>HummingbirdClient: getHardenedRecommendations(baseImageRef)
    HummingbirdClient-->>HardenedImageProvider: List<IndexedRecommendation>
    alt [registryMap not empty]
      loop for each IndexedRecommendation
        HardenedImageProvider->>HardenedImageResponseHandler: applyRegistryMap(rec.packageName, registryMap)
        HardenedImageResponseHandler-->>HardenedImageProvider: PackageRef
        HardenedImageProvider->>HardenedImageProvider: build new IndexedRecommendation
      end
    end
    HardenedImageProvider-->>Client: Map<PackageRef, List<IndexedRecommendation>>
  end
Loading

File-Level Changes

Change Details Files
Add registry prefix mapping application to hardened image recommendations at lookup/serialization time without mutating the cached index.
  • In HardenedImageProvider.lookupBySbomId, fetch registryMap() from config and, when non-empty, wrap each IndexedRecommendation with a new one whose PackageRef has had the registry map applied via HardenedImageResponseHandler.applyRegistryMap.
  • Use an ArrayList copy of the recommendations list to hold mapped recommendations, preserving vulnerabilities and sourceName while replacing only packageName.
  • Expose HardenedImageIndex via getIndex() (existing) and rely on new tests to assert it remains unmodified after mapping.
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProvider.java
Introduce a reusable utility to apply an ordered registry prefix replacement map to PackageRef repository_url qualifiers.
  • Add HardenedImageResponseHandler.applyRegistryMap(PackageRef, Map<String,String>) that no-ops on null/empty maps, missing qualifiers, or missing repository_url.
  • Iterate registryMap entries in insertion order, detect the first source prefix that matches the beginning of repository_url, and build a new PackageURL with the replaced prefix and a TreeMap copy of qualifiers.
  • Log a warning and return the original ref if PackageURL reconstruction fails, ensuring robustness against malformed data.
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageResponseHandler.java
Add digest-aware behavior to hardened recommendation lookup to avoid recommending for digest-only images while still supporting digest+tag inputs.
  • In HardenedImageProvider.lookupBySbomId, extract version and tag from the PackageRef PURL and short-circuit to an empty result when version starts with "sha256:" and tag is null/blank.
  • Leave lookup behavior unchanged when both a digest version and a tag qualifier are present, allowing recommendations to be resolved by tag.
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProvider.java
Extend hardened image recommendation configuration to support a registry prefix replacement map and log its effective configuration at startup.
  • Add registryMap() to HardenedImageRecommendation interface to expose the registry prefix map as Map<String,String>.
  • In HardenedImageProvider constructor, invoke logRegistryMap() to log either an informative message when the map is empty or the number of entries and each source→target mapping at INFO level.
  • Document the new trustedcontent.recommendation.hardened.registry-map.* properties and quoting rules for keys with dots or slashes in application.properties.
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProvider.java
src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageRecommendation.java
src/main/resources/application.properties
Add and adjust unit tests to cover digest-only behavior, registry map behavior, tag preservation, and non-mutation of cached index data.
  • Default HardenedImageRecommendation.registryMap() to Collections.emptyMap() in the test setup to ensure deterministic behavior.
  • Add tests to HardenedImageProviderTest verifying: digest-only PURLs return empty recommendations; digest+tag PURLs still resolve; registry map replacement of repository_url; bare-name behavior when map is empty; tag preservation; and that the backing index is not mutated by registry map application.
  • Add focused tests around HardenedImageResponseHandler.applyRegistryMap for matching prefix, no match, empty map, null map, and tag preservation, using assertNotEquals/equals on PackageRef refs and qualifiers.
src/test/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProviderTest.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 3 issues, and left some high level feedback:

  • Consider treating config.registryMap() as potentially null in both lookupBySbomId and logRegistryMap (e.g., normalizing to an empty map when null) to avoid NPEs if the configuration provider ever returns a null map.
  • In lookupBySbomId, you may want to avoid calling config.registryMap() on every lookup by resolving and caching a normalized (possibly immutable) map once at construction time, which would also ensure a consistent, ordered map for applyRegistryMap.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Consider treating `config.registryMap()` as potentially null in both `lookupBySbomId` and `logRegistryMap` (e.g., normalizing to an empty map when null) to avoid NPEs if the configuration provider ever returns a null map.
- In `lookupBySbomId`, you may want to avoid calling `config.registryMap()` on every lookup by resolving and caching a normalized (possibly immutable) map once at construction time, which would also ensure a consistent, ordered map for `applyRegistryMap`.

## Individual Comments

### Comment 1
<location path="src/main/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageResponseHandler.java" line_range="114-123" />
<code_context>
+   * Iterates through the map entries in order; the first matching source prefix is replaced with
+   * the target prefix. Returns the original ref unchanged if no entry matches or the map is empty.
+   */
+  static PackageRef applyRegistryMap(PackageRef ref, Map<String, String> registryMap) {
+    if (registryMap == null || registryMap.isEmpty()) {
+      return ref;
+    }
+
+    var qualifiers = ref.purl().getQualifiers();
+    if (qualifiers == null) {
+      return ref;
+    }
+    String repoUrl = qualifiers.get("repository_url");
+    if (repoUrl == null) {
+      return ref;
+    }
+
+    for (var entry : registryMap.entrySet()) {
+      String sourcePrefix = entry.getKey();
+      if (repoUrl.startsWith(sourcePrefix)) {
</code_context>
<issue_to_address>
**issue (bug_risk):** Relying on `Map.entrySet()` order may lead to non-deterministic mapping if the provided map is not ordered.

The Javadoc promises an "in order" scan with the first matching prefix winning, but `Map` does not guarantee iteration order unless an ordered implementation is used (e.g., `LinkedHashMap`). If `registryMap()` ever returns a `HashMap` or similar, the chosen prefix may be non-deterministic. To ensure deterministic priority, either require an ordered map type at the API boundary or copy into a known-ordered structure before iterating.
</issue_to_address>

### Comment 2
<location path="src/test/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProviderTest.java" line_range="613-610" />
<code_context>
+    assertEquals(ref.ref(), result.ref());
+  }
+
+  // Verifies that applyRegistryMap handles an empty map gracefully.
+  @Test
+  void testApplyRegistryMapEmptyMap() {
+    // Given a PackageRef and an empty registry map
+    PackageRef ref = new PackageRef(HARDENED_NGINX_PURL);
+
+    // When applying the empty map
+    PackageRef result = HardenedImageResponseHandler.applyRegistryMap(ref, Collections.emptyMap());
+
+    // Then the original ref is returned unchanged
+    assertEquals(ref.ref(), result.ref());
+  }
+
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for PackageRefs missing qualifiers or repository_url in applyRegistryMap.

One remaining edge case is when the `PackageRef` qualifiers are `null` or don’t contain `"repository_url"`. Since the method short-circuits in those cases, please add tests where `ref.purl().getQualifiers()` returns `null` and where the qualifiers map exists but lacks `"repository_url"`, asserting that the original `ref` (or at least `ref.ref()`) is returned unchanged.

Suggested implementation:

```java
    // Then the original ref is returned unchanged
    assertEquals(ref.ref(), result.ref());
  }

  // Verifies that applyRegistryMap handles null qualifiers gracefully.
  @Test
  void testApplyRegistryMapNullQualifiers() {
    // Given a PackageRef whose PURL has null qualifiers
    PackageRef ref = mock(PackageRef.class);
    PackageURL purl = mock(PackageURL.class);

    when(ref.purl()).thenReturn(purl);
    when(ref.ref()).thenReturn(HARDENED_NGINX_PURL);
    when(purl.getQualifiers()).thenReturn(null);

    Map<String, String> registryMap =
        Map.of("quay.io/hummingbird", "registry.access.redhat.com/hi");

    // When applying the registry map
    PackageRef result = HardenedImageResponseHandler.applyRegistryMap(ref, registryMap);

    // Then the original ref is returned unchanged
    assertEquals(ref.ref(), result.ref());
  }

  // Verifies that applyRegistryMap handles missing repository_url qualifier gracefully.
  @Test
  void testApplyRegistryMapMissingRepositoryUrlQualifier() {
    // Given a PackageRef whose PURL qualifiers do not contain repository_url
    PackageRef ref = mock(PackageRef.class);
    PackageURL purl = mock(PackageURL.class);

    when(ref.purl()).thenReturn(purl);
    when(ref.ref()).thenReturn(HARDENED_NGINX_PURL);
    when(purl.getQualifiers()).thenReturn(Map.of("some_other_key", "some_value"));

    Map<String, String> registryMap =
        Map.of("quay.io/hummingbird", "registry.access.redhat.com/hi");

    // When applying the registry map
    PackageRef result = HardenedImageResponseHandler.applyRegistryMap(ref, registryMap);

    // Then the original ref is returned unchanged
    assertEquals(ref.ref(), result.ref());
  }


import static org.junit.jupiter.api.Assertions.assertEquals;

```

1. Ensure the following imports exist at the top of `HardenedImageProviderTest.java` (add them if they are missing):
   - `import com.github.packageurl.PackageURL;`
   - `import java.util.Map;`
   - `import org.junit.jupiter.api.Test;`
   - `import static org.mockito.Mockito.mock;`
   - `import static org.mockito.Mockito.when;`
2. If `HARDENED_NGINX_PURL` is not already a constant in this test class, replace it in the new tests with an appropriate ref string consistent with the rest of the tests.
</issue_to_address>

### Comment 3
<location path="src/test/java/io/github/guacsec/trustifyda/integration/providers/trustify/hardened/HardenedImageProviderTest.java" line_range="77" />
<code_context>
+  void setUp() {
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests to verify the logging behavior for the registry map configuration.

The acceptance criteria require logging the applied registry map at startup, but this behavior isn’t covered by tests. Please add tests that construct a `HardenedImageProvider` with (1) an empty registry map and (2) a non-empty map, and assert the expected INFO log messages (e.g., via a log appender or log-capturing extension). This will help catch regressions in the registry map observability behavior.
</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 Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 79.26829% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.19%. Comparing base (758ae5e) to head (0d17c34).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...iders/trustify/hardened/HardenedImageProvider.java 71.42% 8 Missing and 2 partials ⚠️
...rustify/hardened/HardenedImageResponseHandler.java 85.10% 5 Missing and 2 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@             Coverage Diff              @@
##               main     #642      +/-   ##
============================================
+ Coverage     56.67%   57.19%   +0.52%     
- Complexity      836      878      +42     
============================================
  Files            92       92              
  Lines          4863     5021     +158     
  Branches        655      691      +36     
============================================
+ Hits           2756     2872     +116     
- Misses         1814     1841      +27     
- Partials        293      308      +15     
Flag Coverage Δ
integration-tests 57.19% <79.26%> (+0.52%) ⬆️

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

Files with missing lines Coverage Δ
...rustify/hardened/HardenedImageResponseHandler.java 79.79% <85.10%> (+4.32%) ⬆️
...iders/trustify/hardened/HardenedImageProvider.java 68.57% <71.42%> (+1.42%) ⬆️

... and 1 file with indirect coverage changes

🚀 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.

@ruromero
ruromero requested a review from a-oren July 8, 2026 15:06
… time (TC-5107)

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

ruromero commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Verification Report — TC-5107

Check Verdict
Scope Containment ✅ PASS
Diff Size ✅ PASS
Commit Traceability ✅ PASS
Sensitive Patterns ✅ PASS
CI Status ✅ PASS
Acceptance Criteria ✅ PASS
Verification Commands N/A
Test Quality ✅ PASS
Test Change Classification ADDITIVE

Details

Scope Containment — All 5 changed files are within the hardened image provider package, directly implementing the task requirements.

Diff Size — ~236 additions / 4 deletions across 5 files (~65% test code). Proportionate for the feature scope.

Commit Traceability — All 3 commits reference TC-5107 in their subject lines.

Sensitive Patterns — No secrets, credentials, or sensitive data detected.

CI Status — All checks passed (Integration Tests, Sourcery review, commitlint).

Acceptance Criteria — All 7 criteria satisfied: ConfigMapping interface with registryMap(), load-time application via applyRegistryMapToData(), static applyRegistryMap method, bare names passthrough, digest-pinned image skip, config documentation with SmallRye quoted-key syntax, comprehensive test coverage (matching prefix, no match, empty map, null map, tag preservation, digest skip, refresh with/without map).

Test Quality — Three no-change scenarios correctly parameterized with @ParameterizedTest + @MethodSource. All test methods have // comments per conventions. Eval Quality N/A.

Test Change Classification — 8 new test methods added to existing file. No existing tests modified or removed.


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

…s (TC-5107)

Docker Hub images can appear as bare names (mariadb), docker.io/library/mariadb,
or docker.io/mariadb. Normalize all variants to docker.io/<name> at both ingest
and lookup time to prevent silent recommendation misses.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread src/test/resources/cyclonedx/hardened-images-batch-sbom.json Outdated
@ruromero
ruromero requested a review from a-oren July 9, 2026 13:41
… (TC-5107)

The digest-pinned skip logic only matched sha256: prefixes. OCI supports
sha512 and sha384 as well — use contains(":") to catch any algorithm.
Also removes unreferenced test fixture.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ruromero
ruromero merged commit 0d846da into guacsec:main Jul 9, 2026
3 checks passed
@ruromero
ruromero deleted the TC-5107 branch July 9, 2026 22:43
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