@@ -118,7 +118,7 @@ private static boolean hasProviderRecommendations(ProviderReport provider) {
118118 }
119119
120120 static String generateMessage (String image , AnalysisReport report , String recommendation ,
121- String hardenedRecommendation ) {
121+ List < String > hardenedRecommendations ) {
122122 var messageBuilder = new StringBuilder (image );
123123
124124 Optional .ofNullable (report .getProviders ())
@@ -160,17 +160,17 @@ static String generateMessage(String image, AnalysisReport report, String recomm
160160 .append ("Replace your image with RedHat UBI: " )
161161 .append (recommendation );
162162 }
163- if (hardenedRecommendation != null ) {
163+ if (hardenedRecommendations != null && ! hardenedRecommendations . isEmpty () ) {
164164 messageBuilder .append (System .lineSeparator ())
165- .append ("A Red Hat Hardened Image is available: " )
166- .append (hardenedRecommendation );
165+ .append ("Red Hat Hardened Image available: " )
166+ .append (String . join ( ", " , hardenedRecommendations ) );
167167 }
168168
169169 return messageBuilder .toString ();
170170 }
171171
172172 static String generateTooltip (String image , AnalysisReport report , String recommendation ,
173- String hardenedRecommendation ) {
173+ List < String > hardenedRecommendations ) {
174174 var tooltipBuilder = new StringBuilder ("<html>" ).append ("<p>" ).append (image ).append ("</p>" );
175175
176176 Optional .ofNullable (report .getProviders ())
@@ -217,10 +217,10 @@ static String generateTooltip(String image, AnalysisReport report, String recomm
217217 .append (recommendation )
218218 .append ("</p>" );
219219 }
220- if (hardenedRecommendation != null ) {
220+ if (hardenedRecommendations != null && ! hardenedRecommendations . isEmpty () ) {
221221 tooltipBuilder .append ("<p/>" )
222- .append ("<p>A Red Hat Hardened Image is available: " )
223- .append (hardenedRecommendation )
222+ .append ("<p>Red Hat Hardened Image available: " )
223+ .append (String . join ( ", " , hardenedRecommendations ) )
224224 .append ("</p>" );
225225 }
226226
@@ -262,9 +262,13 @@ static String getRecommendation(AnalysisReport report, ImageRef imageRef) {
262262 return findMatchingRecommendation (deps , imageRef , DependencyReport ::getRef , DependencyReport ::getRecommendation );
263263 }
264264
265- /** Returns the hardened image recommendation (from provider-level recommendations), or null if none. */
266- static String getHardenedRecommendation (AnalysisReport report , ImageRef imageRef ) {
267- var deps = Optional .ofNullable (report .getProviders ())
265+ /**
266+ * Returns hardened image references from provider-level recommendations matching the given image.
267+ * Each entry is a displayable image reference suitable for Dockerfile FROM line replacement.
268+ * Supports the 1→N case where multiple hardened alternatives exist for a single image.
269+ */
270+ static List <String > getHardenedRecommendations (AnalysisReport report , ImageRef imageRef ) {
271+ return Optional .ofNullable (report .getProviders ())
268272 .stream ()
269273 .flatMap (provider -> provider .values ().stream ())
270274 .filter (Objects ::nonNull )
@@ -274,10 +278,22 @@ static String getHardenedRecommendation(AnalysisReport report, ImageRef imageRef
274278 .filter (entry -> entry .getValue () != null )
275279 .map (entry -> entry .getValue ().getDependencies ())
276280 .filter (Objects ::nonNull )
277- .flatMap (Collection ::stream );
278- return findMatchingRecommendation (deps , imageRef ,
279- io .github .guacsec .trustifyda .api .v5 .RecommendationReport ::getRef ,
280- io .github .guacsec .trustifyda .api .v5 .RecommendationReport ::getRecommendation );
281+ .flatMap (Collection ::stream )
282+ .filter (r -> r .getRef () != null )
283+ .filter (r -> {
284+ try {
285+ return imageRef .getPackageURL ().equals (r .getRef ().purl ());
286+ } catch (MalformedPackageURLException e ) {
287+ LOG .warn ("Skipping recommendation with malformed PURL" , e );
288+ return false ;
289+ }
290+ })
291+ .map (io .github .guacsec .trustifyda .api .v5 .RecommendationReport ::getRecommendation )
292+ .filter (Objects ::nonNull )
293+ .map (DockerfileAnnotator ::toImageName )
294+ .filter (Objects ::nonNull )
295+ .distinct ()
296+ .collect (Collectors .toList ());
281297 }
282298
283299 private static <T > String findMatchingRecommendation (Stream <T > items , ImageRef imageRef ,
@@ -337,11 +353,12 @@ private static String fullyDecode(String value) {
337353
338354 @ NotNull
339355 private static HighlightSeverity getHighlightSeverity (AnalysisReport report , String recommendation ,
340- String hardenedRecommendation , boolean hasIssue ,
356+ List < String > hardenedRecommendations , boolean hasIssue ,
341357 @ NotNull PsiElement context ) {
342358 // Recommendation-only (no vulnerabilities): use INFORMATION severity (blue)
343359 if (!hasIssue ) {
344- boolean hasAnyRecommendation = recommendation != null || hardenedRecommendation != null ;
360+ boolean hasAnyRecommendation = recommendation != null
361+ || (hardenedRecommendations != null && !hardenedRecommendations .isEmpty ());
345362 if (hasAnyRecommendation ) {
346363 return HighlightSeverity .INFORMATION ;
347364 }
@@ -446,16 +463,17 @@ public void apply(@NotNull PsiFile file, Map<BaseImage, Result> annotationResult
446463 boolean recommendationsEnabled = ApiSettingsState .getInstance ().recommendationsEnabled ;
447464 var recommendation = recommendationsEnabled
448465 ? getRecommendation (report , value .getImageRef ()) : null ;
449- var hardenedRecommendation = recommendationsEnabled
450- ? getHardenedRecommendation (report , value .getImageRef ()) : null ;
466+ var hardenedRecommendations = recommendationsEnabled
467+ ? getHardenedRecommendations (report , value .getImageRef ()) : List .< String > of () ;
451468
452469 var message = generateMessage (key .getImageName (), report ,
453- recommendation , hardenedRecommendation );
470+ recommendation , hardenedRecommendations );
454471 var tooltip = generateTooltip (key .getImageName (), report ,
455- recommendation , hardenedRecommendation );
472+ recommendation , hardenedRecommendations );
456473
457474 elements .forEach (e -> {
458- var severity = getHighlightSeverity (report , recommendation , hardenedRecommendation , hasIssue , e );
475+ var severity = getHighlightSeverity (report , recommendation ,
476+ hardenedRecommendations , hasIssue , e );
459477 if (e != null ) {
460478 var builder = holder
461479 .newAnnotation (severity , message )
@@ -469,8 +487,9 @@ public void apply(@NotNull PsiFile file, Map<BaseImage, Result> annotationResult
469487 }
470488 builder = builder .withFix (new ImageReportIntentionAction ());
471489 builder = builder .withFix (new UBIIntentionAction ());
472- if (hardenedRecommendation != null ) {
473- builder = builder .withFix (new HardenedImageIntentionAction ());
490+ for (String hardenedImage : hardenedRecommendations ) {
491+ builder = builder .withFix (
492+ new HardenedImageIntentionAction (hardenedImage ));
474493 }
475494 builder .create ();
476495 }
0 commit comments