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(); } });