From cf76cc5fbbc19800a3464026cdf7bf9508461b07 Mon Sep 17 00:00:00 2001 From: Ruben Romero Montes Date: Thu, 25 Jun 2026 01:24:55 +0200 Subject: [PATCH] feat(image): add hardened image recommendation support Introduce RecommendationInfo record to distinguish hardened vs UBI recommendations. The annotator now checks the recommendations section first (Hummingbird hardened path) before falling back to legacy sources. Hover text shows "Switch to hardened image" vs "Switch to Red Hat UBI" and the UBI intention action is conditionally hidden for hardened images. Co-Authored-By: Claude Opus 4.6 --- gradle/libs.versions.toml | 4 +- .../intellij/image/DockerfileAnnotator.java | 91 ++++++++++++++++--- 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c9788b4..ce80102 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,8 +3,8 @@ caffeine = "3.1.8" commons-compress = "1.21" commons-io = "2.16.1" -trustify-da-api-spec = "2.0.2" -trustify-da-java-client = "0.0.15" +trustify-da-api-spec = "2.0.10-SNAPSHOT" +trustify-da-java-client = "0.0.19-SNAPSHOT" github-api = "1.314" junit = "4.13.2" mockito = "4.11.0" diff --git a/src/main/java/org/jboss/tools/intellij/image/DockerfileAnnotator.java b/src/main/java/org/jboss/tools/intellij/image/DockerfileAnnotator.java index a333063..cb195be 100644 --- a/src/main/java/org/jboss/tools/intellij/image/DockerfileAnnotator.java +++ b/src/main/java/org/jboss/tools/intellij/image/DockerfileAnnotator.java @@ -28,6 +28,7 @@ import io.github.guacsec.trustifyda.api.v5.AnalysisReport; import io.github.guacsec.trustifyda.api.v5.DependencyReport; import io.github.guacsec.trustifyda.api.v5.ProviderReport; +import io.github.guacsec.trustifyda.api.v5.RecommendationReport; import io.github.guacsec.trustifyda.api.v5.Severity; import io.github.guacsec.trustifyda.api.v5.Source; import io.github.guacsec.trustifyda.image.ImageRef; @@ -93,7 +94,7 @@ static boolean isReportAvailable(AnalysisReport report) { .orElse(false); } - static String generateMessage(String image, AnalysisReport report, String recommendation) { + static String generateMessage(String image, AnalysisReport report, RecommendationInfo recommendation) { var messageBuilder = new StringBuilder(image); Optional.ofNullable(report.getProviders()) @@ -131,15 +132,19 @@ static String generateMessage(String image, AnalysisReport report, String recomm })); if (recommendation != null) { - messageBuilder.append(System.lineSeparator()) - .append("Replace your image with RedHat UBI: ") - .append(recommendation); + messageBuilder.append(System.lineSeparator()); + if (recommendation.isHardened()) { + messageBuilder.append("Switch to hardened image ").append(recommendation.imageRef()); + } else { + messageBuilder.append("Switch to Red Hat UBI ").append(recommendation.imageRef()) + .append(" for enhanced security and enterprise-grade stability"); + } } return messageBuilder.toString(); } - static String generateTooltip(String image, AnalysisReport report, String recommendation) { + static String generateTooltip(String image, AnalysisReport report, RecommendationInfo recommendation) { var tooltipBuilder = new StringBuilder("").append("

").append(image).append("

"); Optional.ofNullable(report.getProviders()) @@ -181,10 +186,16 @@ static String generateTooltip(String image, AnalysisReport report, String recomm })); if (recommendation != null) { - tooltipBuilder.append("

") - .append("

Replace your image with RedHat UBI: ") - .append(recommendation) - .append("

"); + tooltipBuilder.append("

"); + if (recommendation.isHardened()) { + tooltipBuilder.append("

Switch to hardened image ") + .append(recommendation.imageRef()) + .append("

"); + } else { + tooltipBuilder.append("

Switch to Red Hat UBI ") + .append(recommendation.imageRef()) + .append(" for enhanced security and enterprise-grade stability

"); + } } return tooltipBuilder.toString(); @@ -208,7 +219,55 @@ static boolean hasIssue(AnalysisReport report) { .orElse(false); } - static String getRecommendation(AnalysisReport report, ImageRef imageRef) { + record RecommendationInfo(String imageRef, String sourceId) { + boolean isHardened() { + return "hardened".equals(sourceId); + } + } + + static String extractImageRefFromPurl(com.github.packageurl.PackageURL purl) { + var qualifiers = purl.getQualifiers(); + if (qualifiers == null) { + return null; + } + String repoUrl = qualifiers.get("repository_url"); + if (repoUrl == null || repoUrl.isBlank()) { + return null; + } + String tag = qualifiers.get("tag"); + if (tag != null && !tag.isBlank()) { + return repoUrl + ":" + tag; + } + return repoUrl; + } + + static RecommendationInfo getRecommendation(AnalysisReport report, ImageRef imageRef) { + // Check recommendations section first (Hummingbird hardened images, trusted-content) + var fromRecommendations = Optional.ofNullable(report.getProviders()) + .flatMap(providers -> providers.values().stream() + .filter(Objects::nonNull) + .filter(p -> p.getRecommendations() != null) + .flatMap(p -> p.getRecommendations().entrySet().stream()) + .flatMap(entry -> { + var sourceName = entry.getKey(); + var recSource = entry.getValue(); + if (recSource.getDependencies() == null) { + return java.util.stream.Stream.empty(); + } + return recSource.getDependencies().stream() + .filter(r -> r.getRecommendation() != null) + .map(r -> new RecommendationInfo( + extractImageRefFromPurl(r.getRecommendation().purl()), + sourceName)) + .filter(r -> r.imageRef() != null); + }) + .findFirst()); + + if (fromRecommendations.isPresent()) { + return fromRecommendations.get(); + } + + // Fall back to sources section (legacy path) return Optional.ofNullable(report.getProviders()) .flatMap(provider -> provider.values() .stream() @@ -231,12 +290,14 @@ static String getRecommendation(AnalysisReport report, ImageRef imageRef) { .map(DependencyReport::getRecommendation) .filter(Objects::nonNull) .findAny()) - .map(r -> new ImageRef(r.purl()).getImage().getNameWithoutTag()) + .map(r -> new RecommendationInfo( + new ImageRef(r.purl()).getImage().getNameWithoutTag(), + "trusted-content")) .orElse(null); } @NotNull - private static HighlightSeverity getHighlightSeverity(AnalysisReport report, String recommendation, boolean hasIssue, @NotNull PsiElement context) { + private static HighlightSeverity getHighlightSeverity(AnalysisReport report, RecommendationInfo recommendation, boolean hasIssue, @NotNull PsiElement context) { // Get the configured severity from the inspection settings final InspectionProfileEntry inspection = getInspection(context); if (inspection != null) { @@ -345,8 +406,10 @@ public void apply(@NotNull PsiFile file, Map annotationResult .newAnnotation(severity, message) .tooltip(tooltip) .range(e) - .withFix(new ImageReportIntentionAction()) - .withFix(new UBIIntentionAction()); + .withFix(new ImageReportIntentionAction()); + if (recommendation == null || !recommendation.isHardened()) { + builder = builder.withFix(new UBIIntentionAction()); + } builder.create(); } });