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
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
caffeine = "3.1.8"
commons-compress = "1.21"
commons-io = "2.16.1"
trustify-da-api-spec = "2.0.7"
trustify-da-api-spec = "2.0.8"
trustify-da-java-client = "0.0.17"
github-api = "1.314"
junit = "4.13.2"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.intellij.util.IncorrectOperationException;
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
import io.github.guacsec.trustifyda.api.v5.Issue;
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
import org.jboss.tools.intellij.exhort.TelemetryService;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
Expand All @@ -34,15 +35,26 @@ public abstract class CAIntentionAction implements IntentionAction {
protected @SafeFieldForPreview PsiElement element;
protected @SafeFieldForPreview VulnerabilitySource source;
protected @SafeFieldForPreview DependencyReport report;
protected @SafeFieldForPreview String recommendationSourceName;
protected @SafeFieldForPreview RecommendationReport recommendationReport;

protected CAIntentionAction(PsiElement element, VulnerabilitySource source, DependencyReport report) {
this.element = element;
this.source = source;
this.report = report;
}

/** Sets provider-level recommendation data for source-attributed quick-fixes. */
void setRecommendationData(String sourceName, RecommendationReport recReport) {
this.recommendationSourceName = sourceName;
this.recommendationReport = recReport;
}

@Override
public @IntentionName @NotNull String getText() {
if (recommendationSourceName != null) {
return getQuickFixTextForRecommendation(recommendationSourceName);
}
return getQuickFixText(this.source, this.report);
}

Expand All @@ -53,12 +65,24 @@ protected CAIntentionAction(PsiElement element, VulnerabilitySource source, Depe

@Override
public void invoke(@NotNull Project project, Editor editor, PsiFile file) throws IncorrectOperationException {
String recommendedVersion = getRecommendedVersion(this.report);
String recommendedVersion;
if (recommendationReport != null && recommendationReport.getRecommendation() != null) {
recommendedVersion = recommendationReport.getRecommendation().version();
} else {
recommendedVersion = getRecommendedVersion(this.report);
}
this.updateVersion(project, editor, file, recommendedVersion);
TelemetryService.sendPackageUpdateEvent(file, recommendedVersion, this.report.getRef().name(), "recommendation-accepted");
}

private String getRecommendationsRepo(DependencyReport dependency) {
// Check provider-level recommendation first
if (recommendationReport != null && recommendationReport.getRecommendation() != null
&& recommendationReport.getRecommendation().purl() != null
&& recommendationReport.getRecommendation().purl().getQualifiers() != null) {
return recommendationReport.getRecommendation().purl().getQualifiers().get("repository_url");
}

String repo=null;
if(thereAreNoIssues(dependency))
{
Expand Down Expand Up @@ -86,7 +110,11 @@ public boolean startInWriteAction() {

@Override
public @Nullable FileModifier getFileModifierForPreview(@NotNull PsiFile target) {
return this.createCAIntentionActionInCopy(PsiTreeUtil.findSameElementInCopy(this.element, target));
FileModifier modifier = this.createCAIntentionActionInCopy(PsiTreeUtil.findSameElementInCopy(this.element, target));
if (modifier instanceof CAIntentionAction copy && this.recommendationSourceName != null) {
copy.setRecommendationData(this.recommendationSourceName, this.recommendationReport);
}
return modifier;
}

protected abstract void updateVersion(@NotNull Project project, Editor editor, PsiFile file, String version);
Expand Down Expand Up @@ -129,6 +157,13 @@ static boolean thereIsRecommendation(DependencyReport dependency) {
return dependency.getRecommendation() != null && !dependency.getRecommendation().version().trim().isEmpty();
}

/** Checks if a provider-level recommendation report has a valid recommendation. */
static boolean thereIsRecommendation(RecommendationReport recReport) {
return recReport != null && recReport.getRecommendation() != null
&& recReport.getRecommendation().version() != null
&& !recReport.getRecommendation().version().trim().isEmpty();
}

static boolean thereAreNoIssues(DependencyReport dependency) {
return dependency.getIssues() == null || dependency.getIssues().size() == 0;
}
Expand All @@ -137,18 +172,22 @@ static boolean thereAreNoIssues(DependencyReport dependency) {
String text="";
if(thereAreNoIssues(dependency) && thereIsRecommendation(dependency))
{
text = "Quick-Fix suggestion - apply redhat Recommended version";
text = "Quick-Fix suggestion - apply Red Hat Recommended version";
}
else
{
if(thereIsTcRemediation(dependency))
{
text = "Quick-Fix suggestion - apply redhat remediation version";
text = "Quick-Fix suggestion - apply Red Hat remediation version";
}
}
return text;
}

private static @NotNull String getQuickFixTextForRecommendation(String recommendationSourceName) {
return "Quick-Fix suggestion (" + recommendationSourceName + ") - apply Red Hat Recommended version";
}

static boolean isQuickFixAvailable(DependencyReport dependency) {
boolean result=false;
if(thereAreNoIssues(dependency))
Expand All @@ -167,4 +206,9 @@ static boolean isQuickFixAvailable(DependencyReport dependency) {
}
return result;
}

/** Checks if a provider-level recommendation report has an available quick-fix. */
static boolean isQuickFixAvailable(RecommendationReport recReport) {
return thereIsRecommendation(recReport);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
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.RecommendationSource;
import io.github.guacsec.trustifyda.api.v5.Source;
import io.github.guacsec.trustifyda.license.LicenseCheck.LicenseSummary;
import org.jboss.tools.intellij.exhort.ApiService;
Expand All @@ -53,6 +55,10 @@ public static CAService getInstance() {
.maximumSize(100)
.build();

private final Cache<String, Map<Dependency, Map<String, RecommendationReport>>> recommendationCache = Caffeine.newBuilder()
.maximumSize(100)
.build();

private final Cache<String, Set<Dependency>> dependencyCache = Caffeine.newBuilder()
.expireAfterWrite(25, TimeUnit.SECONDS)
.maximumSize(100)
Expand All @@ -66,14 +72,21 @@ public static Map<Dependency, Map<VulnerabilitySource, DependencyReport>> getRep
return Collections.unmodifiableMap(getInstance().vulnerabilityCache.get(filePath, p -> Collections.emptyMap()));
}

/** Returns provider-level recommendations keyed by dependency and recommendation source name. */
public static Map<Dependency, Map<String, RecommendationReport>> getRecommendations(String filePath) {
return Collections.unmodifiableMap(getInstance().recommendationCache.get(filePath, p -> Collections.emptyMap()));
}

public static void deleteReports(String filePath) {
getInstance().vulnerabilityCache.invalidate(filePath);
getInstance().recommendationCache.invalidate(filePath);
getInstance().licenseCache.invalidate(filePath);
getInstance().dependencyCache.invalidate(filePath);
}

public static void invalidateAllCaches() {
getInstance().vulnerabilityCache.invalidateAll();
getInstance().recommendationCache.invalidateAll();
getInstance().licenseCache.invalidateAll();
getInstance().dependencyCache.invalidateAll();
}
Expand Down Expand Up @@ -126,6 +139,7 @@ public static boolean performAnalysis(String packageManager,
}

Map<Dependency, Map<VulnerabilitySource, DependencyReport>> resultMap = new ConcurrentHashMap<>();
Map<Dependency, Map<String, RecommendationReport>> recommendationMap = new ConcurrentHashMap<>();

if (report.getProviders() != null) {
// Avoid comparing the version of dependency
Expand Down Expand Up @@ -183,6 +197,24 @@ public static boolean performAnalysis(String packageManager,
}
});
}

// Process provider-level recommendations
if (providerReport.getRecommendations() != null) {
providerReport.getRecommendations().forEach((recSourceName, recSource) -> {
if (recSource.getDependencies() != null) {
for (RecommendationReport recReport : recSource.getDependencies()) {
if (recReport.getRef() != null) {
Dependency recDep = new Dependency(recReport.getRef().purl(), false);
dependencyMap.entrySet().stream()
.filter(e -> recDep.equals(e.getValue()))
.forEach(e -> recommendationMap
.computeIfAbsent(e.getKey(), key -> new ConcurrentHashMap<>())
.put(recSourceName, recReport));
}
}
}
});
}
});
}

Expand All @@ -191,6 +223,11 @@ public static boolean performAnalysis(String packageManager,
} else {
getInstance().vulnerabilityCache.invalidate(filePath);
}
if (!recommendationMap.isEmpty()) {
getInstance().recommendationCache.put(filePath, recommendationMap);
} else {
getInstance().recommendationCache.invalidate(filePath);
}
getInstance().dependencyCache.put(filePath, dependencies);
return true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import com.intellij.util.IncorrectOperationException;
import io.github.guacsec.trustifyda.api.PackageRef;
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
import org.jetbrains.annotations.NotNull;

import java.util.Objects;
Expand All @@ -36,6 +37,16 @@ protected static String getRepositoryUrl(DependencyReport dependency) {
return getRepositoryUrlFromPurl(dependency);
}

/** Extracts the repository URL from a provider-level recommendation report. */
protected static String getRepositoryUrl(RecommendationReport recReport) {
if (recReport != null && recReport.getRecommendation() != null
&& recReport.getRecommendation().purl() != null
&& recReport.getRecommendation().purl().getQualifiers() != null) {
return recReport.getRecommendation().purl().getQualifiers().get("repository_url");
}
return null;
}

protected static String getRepositoryUrlFromPurl(DependencyReport dependency) {
AtomicReference<PackageRef> packageRef = new AtomicReference<>();
if(Objects.nonNull(dependency.getRecommendation()))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*******************************************************************************
* Copyright (c) 2025 Red Hat, Inc.
* Distributed under license by Red Hat, Inc. All rights reserved.
* This program is made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v20.html
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
******************************************************************************/

package org.jboss.tools.intellij.componentanalysis;

import io.github.guacsec.trustifyda.api.PackageRef;
import io.github.guacsec.trustifyda.api.v5.DependencyReport;
import io.github.guacsec.trustifyda.api.v5.RecommendationReport;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
* Tests for provider-level recommendation support in CAIntentionAction.
* Placed in the base package to access package-private methods.
*/
public class CAIntentionActionRecommendationTest {

private static final String NEW_VERSION = "4.0.0";

private RecommendationReport createProviderLevelRecommendation() {
RecommendationReport recReport = new RecommendationReport();
recReport.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
recReport.setRecommendation(new PackageRef("pkg:maven/com.example/test@" + NEW_VERSION));
return recReport;
}

private DependencyReport createReportWithLegacyRecommendation() {
DependencyReport report = new DependencyReport();
report.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
report.setRecommendation(new PackageRef("pkg:maven/com.example/test@" + NEW_VERSION));
return report;
}

private DependencyReport createReportWithoutRecommendation() {
DependencyReport report = new DependencyReport();
report.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
return report;
}

// ── thereIsRecommendation tests ────────────────────────────────────────────

/** Verifies that thereIsRecommendation detects a valid provider-level recommendation. */
@Test
public void testThereIsRecommendationWithProviderLevel() {
RecommendationReport recReport = createProviderLevelRecommendation();
assertTrue("Should detect provider-level recommendation",
CAIntentionAction.thereIsRecommendation(recReport));
}

/** Verifies that thereIsRecommendation returns false for null RecommendationReport. */
@Test
public void testThereIsRecommendationWithNull() {
assertFalse("Should return false for null",
CAIntentionAction.thereIsRecommendation((RecommendationReport) null));
}

/** Verifies that thereIsRecommendation returns false for empty version. */
@Test
public void testThereIsRecommendationWithEmptyVersion() {
RecommendationReport recReport = new RecommendationReport();
recReport.setRef(new PackageRef("pkg:maven/com.example/test@1.0.0"));
recReport.setRecommendation(new PackageRef("pkg:maven/com.example/test@"));
assertFalse("Should return false for empty version",
CAIntentionAction.thereIsRecommendation(recReport));
}

/** Verifies that thereIsRecommendation still works with legacy DependencyReport path. */
@Test
public void testThereIsRecommendationWithLegacyReport() {
DependencyReport report = createReportWithLegacyRecommendation();
assertTrue("Should detect legacy recommendation",
CAIntentionAction.thereIsRecommendation(report));
}

/** Verifies that thereIsRecommendation returns false for DependencyReport without recommendation. */
@Test
public void testThereIsRecommendationFalseWithLegacyReport() {
DependencyReport report = createReportWithoutRecommendation();
assertFalse("Should return false when no recommendation set",
CAIntentionAction.thereIsRecommendation(report));
}

// ── isQuickFixAvailable tests ──────────────────────────────────────────────

/** Verifies that isQuickFixAvailable returns true for a valid provider-level recommendation. */
@Test
public void testIsQuickFixAvailableForProviderLevel() {
RecommendationReport recReport = createProviderLevelRecommendation();
assertTrue("Should be available for provider-level recommendation",
CAIntentionAction.isQuickFixAvailable(recReport));
}

/** Verifies that isQuickFixAvailable returns false for null RecommendationReport. */
@Test
public void testIsQuickFixAvailableForNull() {
assertFalse("Should not be available for null",
CAIntentionAction.isQuickFixAvailable((RecommendationReport) null));
}

/** Verifies that isQuickFixAvailable returns true for legacy recommendation (no issues). */
@Test
public void testIsQuickFixAvailableForLegacyRecommendation() {
DependencyReport report = createReportWithLegacyRecommendation();
assertTrue("Should be available for legacy recommendation",
CAIntentionAction.isQuickFixAvailable(report));
}

// ── Version extraction tests ───────────────────────────────────────────────

/** Verifies that provider-level recommendation version is correctly extractable. */
@Test
public void testRecommendationVersionExtraction() {
RecommendationReport recReport = createProviderLevelRecommendation();
assertNotNull("Recommendation should not be null", recReport.getRecommendation());
assertEquals("Version should match", NEW_VERSION, recReport.getRecommendation().version());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public class CargoCAIntentionActionTest extends BasePlatformTestCase {

private DependencyReport createReportWithRecommendation() {
DependencyReport report = new DependencyReport();
report.setRef(new PackageRef("pkg:cargo/test-crate@1.0.0"));
report.setRecommendation(new PackageRef("pkg:cargo/test-crate@" + CargoCAIntentionActionTest.NEW_VERSION));
return report;
}
Expand Down Expand Up @@ -376,4 +377,5 @@ public void testUpdateTargetSpecificComplexObjectDependency() {
assertTrue("winapi features should remain unchanged",
updatedText.contains("features = [\"winuser\"]"));
}

}
Loading
Loading