feat: add hardened image recommendations to DockerfileAnnotator (TC-4811)#261
Conversation
…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
Verification Report for TC-4811 (commit d38a838)
Overall: WARNScope Containment flagged 2 files from the task spec that don't appear in the diff — one is a path mismatch in the spec ( This comment was AI-generated by sdlc-workflow/verify-pr v0.11.0. |
ruromero
left a comment
There was a problem hiding this comment.
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 backThe 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
left a comment
There was a problem hiding this comment.
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); | ||
| } | ||
| }) |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
[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()); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
[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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I won't change that now, since I'm removing the UBI recommendations.
There was a problem hiding this comment.
[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()); |
There was a problem hiding this comment.
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()); // ← sameNote: if withFix() mutates in place (mutable builder), this works by accident but is inconsistent with line 468's style.
There was a problem hiding this comment.
[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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
[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 -> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
[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>
|
[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 |
Verification Report for TC-4811 (commit 8f58a16)
Overall: WARNAcceptance 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 All 6 reviewer comments from @ruromero were classified and addressed:
This comment was AI-generated by sdlc-workflow/verify-pr v0.11.1. |
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>
|



Summary
recommendationsin the analysis reportHardenedImageIntentionActionquick fix to open the Red Hat Hardened Container Images catalogSettings > Dependency Analytics > Recommendations)isReportAvailable()to consider provider-level recommendations, not just vulnerability countsfullyDecode()Test plan
generateMessage/generateTooltipwith UBI, hardened, both, and nonegetRecommendation(source-level) andgetHardenedRecommendation(provider-level)isReportAvailablewith vulns, recommendations only, empty, zero vulnsApiSettingsStatedefaultsImplements TC-4811
🤖 Generated with Claude Code