Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ private static boolean hasProviderRecommendations(ProviderReport provider) {
}

static String generateMessage(String image, AnalysisReport report, String recommendation,
String hardenedRecommendation) {
List<String> hardenedRecommendations) {
var messageBuilder = new StringBuilder(image);

Optional.ofNullable(report.getProviders())
Expand Down Expand Up @@ -160,17 +160,17 @@ static String generateMessage(String image, AnalysisReport report, String recomm
.append("Replace your image with RedHat UBI: ")
.append(recommendation);
}
if (hardenedRecommendation != null) {
if (hardenedRecommendations != null && !hardenedRecommendations.isEmpty()) {
messageBuilder.append(System.lineSeparator())
.append("A Red Hat Hardened Image is available: ")
.append(hardenedRecommendation);
.append("Red Hat Hardened Image available: ")
.append(String.join(", ", hardenedRecommendations));
}

return messageBuilder.toString();
}

static String generateTooltip(String image, AnalysisReport report, String recommendation,
String hardenedRecommendation) {
List<String> hardenedRecommendations) {
var tooltipBuilder = new StringBuilder("<html>").append("<p>").append(image).append("</p>");

Optional.ofNullable(report.getProviders())
Expand Down Expand Up @@ -217,10 +217,10 @@ static String generateTooltip(String image, AnalysisReport report, String recomm
.append(recommendation)
.append("</p>");
}
if (hardenedRecommendation != null) {
if (hardenedRecommendations != null && !hardenedRecommendations.isEmpty()) {
tooltipBuilder.append("<p/>")
.append("<p>A Red Hat Hardened Image is available: ")
.append(hardenedRecommendation)
.append("<p>Red Hat Hardened Image available: ")
.append(String.join(", ", hardenedRecommendations))
.append("</p>");
}

Expand Down Expand Up @@ -262,9 +262,13 @@ static String getRecommendation(AnalysisReport report, ImageRef imageRef) {
return findMatchingRecommendation(deps, imageRef, DependencyReport::getRef, DependencyReport::getRecommendation);
}

/** Returns the hardened image recommendation (from provider-level recommendations), or null if none. */
static String getHardenedRecommendation(AnalysisReport report, ImageRef imageRef) {
var deps = Optional.ofNullable(report.getProviders())
/**
* Returns hardened image references from provider-level recommendations matching the given image.
* Each entry is a displayable image reference suitable for Dockerfile FROM line replacement.
* Supports the 1→N case where multiple hardened alternatives exist for a single image.
*/
static List<String> getHardenedRecommendations(AnalysisReport report, ImageRef imageRef) {
return Optional.ofNullable(report.getProviders())
.stream()
.flatMap(provider -> provider.values().stream())
.filter(Objects::nonNull)
Expand All @@ -274,10 +278,22 @@ static String getHardenedRecommendation(AnalysisReport report, ImageRef imageRef
.filter(entry -> entry.getValue() != null)
.map(entry -> entry.getValue().getDependencies())
.filter(Objects::nonNull)
.flatMap(Collection::stream);
return findMatchingRecommendation(deps, imageRef,
io.github.guacsec.trustifyda.api.v5.RecommendationReport::getRef,
io.github.guacsec.trustifyda.api.v5.RecommendationReport::getRecommendation);
.flatMap(Collection::stream)
.filter(r -> r.getRef() != null)
.filter(r -> {
try {
return imageRef.getPackageURL().equals(r.getRef().purl());
} catch (MalformedPackageURLException e) {
LOG.warn("Skipping recommendation with malformed PURL", e);
return false;
}
})
.map(io.github.guacsec.trustifyda.api.v5.RecommendationReport::getRecommendation)
.filter(Objects::nonNull)
.map(DockerfileAnnotator::toImageName)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}

private static <T> String findMatchingRecommendation(Stream<T> items, ImageRef imageRef,
Expand Down Expand Up @@ -337,11 +353,12 @@ private static String fullyDecode(String value) {

@NotNull
private static HighlightSeverity getHighlightSeverity(AnalysisReport report, String recommendation,
String hardenedRecommendation, boolean hasIssue,
List<String> hardenedRecommendations, boolean hasIssue,
@NotNull PsiElement context) {
// Recommendation-only (no vulnerabilities): use INFORMATION severity (blue)
if (!hasIssue) {
boolean hasAnyRecommendation = recommendation != null || hardenedRecommendation != null;
boolean hasAnyRecommendation = recommendation != null
|| (hardenedRecommendations != null && !hardenedRecommendations.isEmpty());
if (hasAnyRecommendation) {
return HighlightSeverity.INFORMATION;
}
Expand Down Expand Up @@ -446,16 +463,17 @@ public void apply(@NotNull PsiFile file, Map<BaseImage, Result> annotationResult
boolean recommendationsEnabled = ApiSettingsState.getInstance().recommendationsEnabled;
var recommendation = recommendationsEnabled
? getRecommendation(report, value.getImageRef()) : null;
var hardenedRecommendation = recommendationsEnabled
? getHardenedRecommendation(report, value.getImageRef()) : null;
var hardenedRecommendations = recommendationsEnabled
? getHardenedRecommendations(report, value.getImageRef()) : List.<String>of();

var message = generateMessage(key.getImageName(), report,
recommendation, hardenedRecommendation);
recommendation, hardenedRecommendations);
var tooltip = generateTooltip(key.getImageName(), report,
recommendation, hardenedRecommendation);
recommendation, hardenedRecommendations);

elements.forEach(e -> {
var severity = getHighlightSeverity(report, recommendation, hardenedRecommendation, hasIssue, e);
var severity = getHighlightSeverity(report, recommendation,
hardenedRecommendations, hasIssue, e);
if (e != null) {
var builder = holder
.newAnnotation(severity, message)
Expand All @@ -469,8 +487,9 @@ public void apply(@NotNull PsiFile file, Map<BaseImage, Result> annotationResult
}
builder = builder.withFix(new ImageReportIntentionAction());
builder = builder.withFix(new UBIIntentionAction());
if (hardenedRecommendation != null) {
builder = builder.withFix(new HardenedImageIntentionAction());
for (String hardenedImage : hardenedRecommendations) {
builder = builder.withFix(
new HardenedImageIntentionAction(hardenedImage));
}
builder.create();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,34 @@
import com.intellij.codeInsight.intention.IntentionAction;
import com.intellij.codeInspection.util.IntentionFamilyName;
import com.intellij.codeInspection.util.IntentionName;
import org.jboss.tools.intellij.image.build.filetype.DockerfileFileType;
import com.intellij.ide.BrowserUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.IncorrectOperationException;
import org.jboss.tools.intellij.image.build.filetype.DockerfileFileType;
import org.jboss.tools.intellij.image.build.psi.DockerfileFromInstruction;
import org.jboss.tools.intellij.image.build.psi.DockerfileImageName;
import org.jetbrains.annotations.NotNull;

import java.net.URI;

/** Intention action that directs users to the Red Hat Hardened Container Images catalog. */
/**
* Intention action that replaces a Dockerfile FROM line's image reference
* with a recommended Red Hat Hardened Image.
*/
public class HardenedImageIntentionAction implements IntentionAction {

public static final String HARDENED_IMAGE_LINK = "https://catalog.redhat.com/software/containers/search?gs&q=hardened";
private final String imageReference;

public HardenedImageIntentionAction(String imageReference) {
this.imageReference = imageReference;
}

@Override
public @IntentionName @NotNull String getText() {
return "Switch to a Red Hat Hardened Image for enhanced security";
return "Replace with Red Hat Hardened Image: " + imageReference;
}

@Override
Expand All @@ -46,11 +56,22 @@ public boolean isAvailable(@NotNull Project project, Editor editor, PsiFile psiF

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile psiFile) throws IncorrectOperationException {
BrowserUtil.browse(URI.create(HARDENED_IMAGE_LINK));
int offset = editor.getCaretModel().getOffset();
PsiElement element = psiFile.findElementAt(offset);
DockerfileFromInstruction fromInstruction =
PsiTreeUtil.getParentOfType(element, DockerfileFromInstruction.class, false);
if (fromInstruction != null) {
DockerfileImageName imageName = fromInstruction.getImageName();
if (imageName != null) {
Document document = editor.getDocument();
TextRange range = imageName.getTextRange();
document.replaceString(range.getStartOffset(), range.getEndOffset(), imageReference);
}
}
}

@Override
public boolean startInWriteAction() {
return false;
return true;
}
}
Loading
Loading