Skip to content

feat: add hardened image recommendations to DockerfileAnnotator (TC-4811)#261

Merged
a-oren merged 3 commits into
redhat-developer:mainfrom
a-oren:TC-4811
Jul 5, 2026
Merged

feat: add hardened image recommendations to DockerfileAnnotator (TC-4811)#261
a-oren merged 3 commits into
redhat-developer:mainfrom
a-oren:TC-4811

Conversation

@a-oren

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

Copy link
Copy Markdown
Collaborator

Summary

  • Add support for Red Hat Hardened Image recommendations in Dockerfile annotations, alongside existing UBI recommendations
  • Hardened recommendations are read from provider-level recommendations in the analysis report
  • Show blue (INFORMATION severity) wavy underlines when only recommendations exist (no vulnerabilities)
  • Add HardenedImageIntentionAction quick fix to open the Red Hat Hardened Container Images catalog
  • Add a recommendations toggle in plugin settings (Settings > Dependency Analytics > Recommendations)
  • Fix isReportAvailable() to consider provider-level recommendations, not just vulnerability counts
  • Fix NPE when source entry value is null in recommendation stream
  • Handle URL-encoded PURLs from the backend (double/triple encoding) via fullyDecode()

Test plan

  • Unit tests for generateMessage / generateTooltip with UBI, hardened, both, and none
  • Unit tests for getRecommendation (source-level) and getHardenedRecommendation (provider-level)
  • Unit tests for isReportAvailable with vulns, recommendations only, empty, zero vulns
  • Unit test for double-encoded PURL handling
  • Unit tests for ApiSettingsState defaults
  • Manual verification in IntelliJ with local backend

Implements TC-4811

🤖 Generated with Claude Code

…811)

Add support for Red Hat Hardened Image recommendations alongside existing
UBI recommendations. Hardened recommendations are read from provider-level
recommendations in the analysis report, while UBI recommendations continue
to come from source-level dependencies.

Key changes:
- Add getHardenedRecommendation() to read from ProviderReport.getRecommendations()
- Add toImageName() with URL decoding to handle encoded PURLs from the backend
- Show blue (INFORMATION) annotations when only recommendations exist (no vulns)
- Add HardenedImageIntentionAction for opening Red Hat Hardened Images catalog
- Add recommendations toggle in plugin settings
- Fix isReportAvailable() to also check provider-level recommendations
- Fix NPE when source entry value is null in getRecommendation stream
- Add comprehensive tests for recommendation and tooltip generation

Implements TC-4811

Assisted-by: Claude Code
@a-oren
a-oren requested a review from ruromero June 23, 2026 13:14
@a-oren

a-oren commented Jun 23, 2026

Copy link
Copy Markdown
Collaborator Author

Verification Report for TC-4811 (commit d38a838)

Check Result Details
Review Feedback N/A No review comments on this PR
Root-Cause Investigation N/A No sub-tasks created — nothing to investigate
Scope Containment WARN 2 files in task spec not modified in PR: image/ApiService.java (changes present in exhort/ApiService.java — path mismatch in spec), CAIntentionAction.java (not applicable — hardened action uses different class name). 2 extra test files justified by acceptance criteria.
Diff Size PASS 7 files changed, reasonable for feature scope
Commit Traceability PASS Single commit with conventional format referencing TC-4811
Sensitive Patterns PASS No secrets, credentials, or sensitive data detected
CI Status PASS 7/7 CI checks passed
Acceptance Criteria PASS 5/5 acceptance criteria met
Test Quality WARN Repetitive Test Detection: generateMessage and generateTooltip tests could be parameterized to reduce duplication. Test Documentation: PASS. Eval Quality: N/A.
Test Change Classification ADDITIVE All test files are new additions
Verification Commands PASS All verification commands executed successfully

Overall: WARN

Scope Containment flagged 2 files from the task spec that don't appear in the diff — one is a path mismatch in the spec (image/ApiService.java vs actual exhort/ApiService.java), and the other (CAIntentionAction.java) was not applicable since the hardened image action uses a different class name (HardenedImageIntentionAction). These are spec-level inaccuracies, not implementation gaps. Test Quality notes that some test methods with similar structure could be parameterized but this is a minor style observation.


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.

Code Review — Hardened Image Recommendations

Nice feature addition. The provider-level recommendation path, settings toggle, and informational annotation (blue wave) are well-structured, and the test coverage is solid. A few issues to address before merging:

Bugs

1. RuntimeException in stream filter crashes annotation pass (DockerfileAnnotator.java:290, also :262)

Both getRecommendation() and getHardenedRecommendation() wrap MalformedPackageURLException in RuntimeException inside a .filter() lambda. A single malformed PURL from the backend kills all annotations for every image in the Dockerfile. Catch and skip the entry instead:

.filter(r -> {
    try {
        return imageRef.getPackageURL().equals(r.getRef().purl());
    } catch (MalformedPackageURLException e) {
        LOG.warn("Skipping recommendation with malformed PURL", e);
        return false;
    }
})

2. UBI quick-fix regression (DockerfileAnnotator.java:469)

UBIIntentionAction was previously added unconditionally on every Docker image. Now it's gated by recommendation != null. Users who relied on the UBI quick-fix lose it when the backend doesn't return a specific UBI recommendation.

3. recommendationsEnabled retroactively gates UBI recommendations (DockerfileAnnotator.java:444)

The new toggle controls both UBI recommendations (previously always-on) and hardened recommendations. Users who disable recommendations to stop hardened suggestions also lose pre-existing UBI behavior. Consider either: (a) separate toggles, or (b) only gating hardened recommendations behind the toggle while leaving UBI unconditional.

Code quality

4. Inconsistent builder pattern (DockerfileAnnotator.java:470)

builder = builder.withFix(new ImageReportIntentionAction());  // assigns back
// ...
builder.withFix(new UBIIntentionAction());                    // does NOT assign back
builder.withFix(new HardenedImageIntentionAction());          // does NOT assign back

The existing CAAnnotator uses the same non-assignment pattern (suggesting withFix() mutates in place), but the inconsistency within the same method is confusing. Either always assign back or never do.

5. Redundant condition (DockerfileAnnotator.java:341)

!hasIssue && !hasIssue(report) — the hasIssue parameter is already computed as hasIssue(report) at the call site (line 443). The second check is redundant.

6. Duplicated PURL matching logic (DockerfileAnnotator.java:259, :287)

getRecommendation() and getHardenedRecommendation() share ~30 lines of near-identical code, including the same 8-line PURL matching filter. Consider extracting the shared filter to avoid divergence if matching logic changes.

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

Review Summary

This PR adds hardened image recommendations to the DockerfileAnnotator, including a new quick-fix action and settings toggle. The feature works end-to-end, but there are several issues that should be addressed before merging — a crash-path bug, a behavioral regression, and some code quality concerns.

Thread safety note (minor): ApiSettingsState.recommendationsEnabled is a non-volatile public field on a singleton read from background threads (doAnnotate) and written from the EDT (settings UI). In practice IntelliJ's settings persistence model makes tearing unlikely for a boolean, but it's worth noting for future fields.

} catch (MalformedPackageURLException e) {
throw new RuntimeException(e);
}
})

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.

Bug — RuntimeException crashes entire annotation pass

Wrapping MalformedPackageURLException in RuntimeException inside a stream .filter() means a single malformed PURL in the recommendations list kills the entire annotation pipeline for the file. The same pattern exists in getRecommendation() at line 262.

getHardenedRecommendation() should catch-and-skip (return false or log a warning), not re-throw:

.filter(r -> {
    try {
        return imageRef.getPackageURL().equals(r.getRef().purl());
    } catch (MalformedPackageURLException e) {
        LOG.warn("Skipping recommendation with malformed PURL", e);
        return false;
    }
})

Same fix needed in getRecommendation() (line 262).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as code change request — addressed in commit 8f58a16 (catch-and-skip MalformedPackageURLException + extracted into shared findMatchingRecommendation() helper). No sub-task created.

}
builder = builder.withFix(new ImageReportIntentionAction());
if (recommendation != null) {
builder.withFix(new UBIIntentionAction());

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.

Regression — UBI quick-fix now conditional on recommendation lookup succeeding

Before this PR, UBIIntentionAction was always added unconditionally. Now it's gated by recommendation != null (line 470), which depends on the recommendations API call succeeding AND recommendationsEnabled being true.

If the recommendations API is down or the toggle is off, users lose the UBI quick-fix entirely — a regression from the previous behavior where UBI suggestions were always available.

Consider decoupling the UBI quick-fix from the recommendation lookup result.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as code change request — addressed in commit 8f58a16 (UBIIntentionAction restored as unconditional). No sub-task created.

if (isReportAvailable(report)) {
var hasIssue = hasIssue(report);
var recommendation = getRecommendation(report, value.getImageRef());
boolean recommendationsEnabled = ApiSettingsState.getInstance().recommendationsEnabled;

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.

Design — single toggle gates two independent features

One recommendationsEnabled boolean controls both UBI recommendations (line 446) and hardened image recommendations (line 448). Users may want UBI suggestions without hardened-image suggestions, or vice versa. Consider separate toggles, or at minimum document that this is intentional.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I won't change that now, since I'm removing the UBI recommendations.

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.

Sounds good

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as suggestion — proposes separate toggles for UBI vs hardened recommendations. No matching convention in CONVENTIONS.md; existing codebase uses coarse-grained toggles (e.g., licenseCheckEnabled). Author declined, reviewer accepted. No sub-task created.

}
builder = builder.withFix(new ImageReportIntentionAction());
if (recommendation != null) {
builder.withFix(new UBIIntentionAction());

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.

Bug — inconsistent withFix() builder pattern drops quick-fix actions

Line 468 correctly reassigns: builder = builder.withFix(...). But lines 470 and 473 call builder.withFix(...) without assigning back. If AnnotationBuilder.withFix() returns a new builder (immutable pattern), the UBI and Hardened quick-fixes are silently discarded.

// Line 468 — correct
builder = builder.withFix(new ImageReportIntentionAction());
// Line 470 — missing assignment
builder.withFix(new UBIIntentionAction());        // ← should be: builder = builder.withFix(...)
// Line 473 — missing assignment  
builder.withFix(new HardenedImageIntentionAction()); // ← same

Note: if withFix() mutates in place (mutable builder), this works by accident but is inconsistent with line 468's style.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as code change request — addressed in commit 8f58a16 (consistent builder = builder.withFix(...) reassignment). No sub-task created.

private static HighlightSeverity getHighlightSeverity(AnalysisReport report, String recommendation,
String hardenedRecommendation, boolean hasIssue,
@NotNull PsiElement context) {
// Recommendation-only (no vulnerabilities): use INFORMATION severity (blue)

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.

Cleanup — redundant guard condition

!hasIssue && !hasIssue(report) is redundant: at the call site (line 443), hasIssue is computed as hasIssue(report), so both operands are always equal. Simplify to just !hasIssue.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as code change request — addressed in commit 8f58a16 (simplified to !hasIssue). No sub-task created.

.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(r -> r.getRef() != null)
.filter(r -> {

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.

Cleanup — duplicated PURL matching logic

getHardenedRecommendation() (lines 287–298) and getRecommendation() (lines 257–268) share nearly identical stream-filter-find logic, differing only in the report accessor (getHardenedImageRecommendation() vs getImageRecommendation()). Consider extracting a shared helper to reduce duplication and ensure bug fixes (like the RuntimeException issue above) are applied consistently.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

[sdlc-workflow/verify-pr] Classified as suggestion — proposes extracting shared PURL matching logic. Already addressed in commit 8f58a16 (generic findMatchingRecommendation() helper extracted). No sub-task created.

- Catch MalformedPackageURLException in PURL filters instead of re-throwing
- Restore UBIIntentionAction as unconditional quick-fix (regression fix)
- Fix inconsistent withFix() builder pattern (missing reassignment)
- Remove redundant hasIssue(report) guard condition
- Extract shared PURL matching logic into generic findMatchingRecommendation()

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

a-oren commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

[sdlc-workflow/verify-pr] Re: @ruromero review (review-body-4613812997) — This review body contains 6 items that duplicate the inline comment threads. Classifications: items 1,2,4,5 are code change requests (all addressed in commit 8f58a16), items 3,6 are suggestions (item 3 declined by author and accepted by reviewer, item 6 addressed in commit 8f58a16). No sub-tasks created.

[sdlc-workflow/verify-pr] Re: @ruromero review (review-body-4613840810) — Classified as nit — thread safety observation about recommendationsEnabled field visibility; noted for future fields. No sub-task created.

@a-oren

a-oren commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator Author

Verification Report for TC-4811 (commit 8f58a16)

Check Result Details
Review Feedback PASS 4 code change requests + 2 suggestions + 1 nit — all code change requests addressed in commit 8f58a16
Root-Cause Investigation N/A No sub-tasks created — all review feedback addressed
Scope Containment PASS All task-specified files covered; 2 extra test files are justified additions
Diff Size PASS 8 files, +545 -41, proportionate to feature scope
Commit Traceability PASS Both commits reference TC-4811
Sensitive Patterns PASS No secrets, credentials, or sensitive data detected
CI Status PASS 7/7 CI checks passed
Acceptance Criteria WARN 4/5 criteria met; criterion 2 partially met — TRUSTIFY_DA_RECOMMENDATIONS_ENABLED system property is set but Java client v0.0.17 does not consume it
Test Quality PASS Repetitive Test Detection: PASS. Test Documentation: PASS. Eval Quality: N/A.
Test Change Classification ADDITIVE Both test files are new additions
Verification Commands PASS ./gradlew build and ./gradlew test succeed

Overall: WARN

Acceptance Criteria flagged criterion 2 ("When recommendationsEnabled is false, the Java client sends recommend=false to the backend") as partially met. The IntelliJ plugin correctly sets the TRUSTIFY_DA_RECOMMENDATIONS_ENABLED system property, but the current Java client v0.0.17 does not read this property — the backend always receives recommendations regardless of the toggle. The UI-level suppression (criterion 3) works correctly, so users don't see recommendations when disabled, but the backend request is not modified. This may require a Java client update to fully implement.

All 6 reviewer comments from @ruromero were classified and addressed:

  • 4 code change requests (RuntimeException crash, UBI regression, builder pattern, redundant condition) — all fixed in commit 8f58a16
  • 2 suggestions (separate toggles, deduplicated logic) — toggle suggestion declined by author and accepted by reviewer; deduplication suggestion proactively addressed

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

@a-oren
a-oren requested a review from ruromero July 2, 2026 08:52
Use TRUSTIFY_DA_RECOMMEND to match what trustify-da-java-client reads,
instead of the incorrect TRUSTIFY_DA_RECOMMENDATIONS_ENABLED.

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

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

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

LGTM

@a-oren
a-oren merged commit 1db7556 into redhat-developer:main Jul 5, 2026
7 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