Skip to content
Open
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 pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<deploy-plugin.version>3.1.1</deploy-plugin.version>

<!-- Dependencies -->
<trustify-da-api.version>2.0.11</trustify-da-api.version>
<trustify-da-api.version>2.0.12</trustify-da-api.version>
<cyclonedx.version>11.0.1</cyclonedx.version>
<sentry.version>7.8.0</sentry.version>
<spdx.version>2.0.2</spdx.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -545,8 +545,7 @@ private void incrementCounter(PackageItem item, VulnerabilityCounter counter, bo

private boolean hasUpstreamRemediation(Remediation r) {
return (r.getFixedIn() != null && !r.getFixedIn().isEmpty())
|| (r.getVersionRanges() != null && !r.getVersionRanges().isEmpty())
|| (r.getRemediations() != null && !r.getRemediations().isEmpty());
|| (r.getAdvisories() != null && !r.getAdvisories().isEmpty());
}

private boolean hasTrustedContentRemediation(Remediation r) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@

import io.github.guacsec.trustifyda.api.PackageRef;
import io.github.guacsec.trustifyda.api.v5.AdvisoryInfo;
import io.github.guacsec.trustifyda.api.v5.AdvisoryRemediation;
import io.github.guacsec.trustifyda.api.v5.Issue;
import io.github.guacsec.trustifyda.api.v5.Remediation;
import io.github.guacsec.trustifyda.api.v5.RemediationCategory;
Expand Down Expand Up @@ -141,6 +142,15 @@ private List<Issue> toIssues(JsonNode response) {
setCvssData(issue, vuln, purlStatus);
issuesByCveSource.put(key, issue);
});
issuesByCveSource
.values()
.forEach(
issue -> {
var remediation = issue.getRemediation();
if (remediation != null && remediation.getFixedIn() != null) {
remediation.getFixedIn().sort(VersionComparator.INSTANCE);
}
});
issues.addAll(issuesByCveSource.values());
});

Expand Down Expand Up @@ -181,11 +191,13 @@ private void setCvssData(Issue issue, JsonNode vuln, JsonNode purlStatus) {
issue.setSeverity(SeverityUtils.fromScore(sd.score));
}

var r = new Remediation();
boolean hasRemediation = processVersionRange(purlStatus, r, false);
hasRemediation |= processRemediations(purlStatus, r);

if (hasRemediation) {
var advRem = createAdvisoryRemediation(purlStatus);
if (advRem != null) {
var r = new Remediation();
r.addAdvisoriesItem(advRem);
if (advRem.getFixedIn() != null) {
r.addFixedInItem(advRem.getFixedIn());
}
issue.setRemediation(r);
}
}
Expand All @@ -207,9 +219,11 @@ private void mergeIssueData(Issue existing, JsonNode vuln, JsonNode purlStatus)
}
}

var r = ensureRemediation(existing);
processVersionRange(purlStatus, r, true);
processRemediations(purlStatus, r);
var advRem = createAdvisoryRemediation(purlStatus);
if (advRem != null) {
var r = ensureRemediation(existing);
mergeAdvisoryRemediation(r, advRem);
}
}

private record ScoreData(Float score, String severity) {}
Expand Down Expand Up @@ -240,11 +254,99 @@ private ScoreData extractScoreData(JsonNode vuln, JsonNode purlStatus) {
return new ScoreData(score, severity);
}

private boolean processVersionRange(JsonNode purlStatus, Remediation r, boolean dedup) {
private AdvisoryRemediation createAdvisoryRemediation(JsonNode purlStatus) {
var advisoryInfo = buildAdvisoryInfo(purlStatus);
var advRem = new AdvisoryRemediation();
advRem.setAdvisory(advisoryInfo);

var status = JsonUtils.getTextValue(purlStatus, "status");
if (status != null) {
advRem.setStatus(status);
}

var versionRange = purlStatus.get("version_range");
if (versionRange == null || versionRange.isNull()) {
return false;
if (versionRange != null && !versionRange.isNull()) {
advRem.addVersionRangesItem(buildVersionRange(versionRange));
var highVersion = JsonUtils.getTextValue(versionRange, "high_version");
var highInclusive = JsonUtils.getBooleanValue(versionRange, "high_inclusive");
if (highVersion != null && (highInclusive == null || !highInclusive)) {
advRem.setFixedIn(highVersion);
}
}

var remediations = (ArrayNode) purlStatus.get("remediations");
if (remediations != null && !remediations.isEmpty()) {
remediations.forEach(
rem -> {
var info = buildRemediationInfoFromNode(rem, advisoryInfo);
if (info != null) {
advRem.addRemediationsItem(info);
}
});
} else if (advisoryInfo != null) {
var info = new RemediationInfo();
if (advRem.getFixedIn() != null) {
info.category(RemediationCategory.VENDOR_FIX);
}
advRem.addRemediationsItem(info);
}

if (advRem.getVersionRanges() != null
|| advRem.getRemediations() != null
|| advRem.getAdvisory() != null) {
return advRem;
}
return null;
}

private void mergeAdvisoryRemediation(Remediation r, AdvisoryRemediation advRem) {
var existingAdvisories = r.getAdvisories();
if (existingAdvisories != null && advRem.getAdvisory() != null) {
var existingMatch =
existingAdvisories.stream()
.filter(
a ->
a.getAdvisory() != null
&& Objects.equals(a.getAdvisory().getId(), advRem.getAdvisory().getId()))
.findFirst();
if (existingMatch.isPresent()) {
var existing = existingMatch.get();
if (advRem.getVersionRanges() != null) {
for (var vr : advRem.getVersionRanges()) {
if (!isDuplicateVersionRange(existing, vr)) {
existing.addVersionRangesItem(vr);
}
}
}
if (advRem.getRemediations() != null) {
for (var rem : advRem.getRemediations()) {
if (!isDuplicateRemediationInfo(existing.getRemediations(), rem)) {
existing.addRemediationsItem(rem);
}
}
}
if (existing.getFixedIn() == null && advRem.getFixedIn() != null) {
existing.setFixedIn(advRem.getFixedIn());
}
if (advRem.getFixedIn() != null) {
var fixedIn = r.getFixedIn();
if (fixedIn == null || !fixedIn.contains(advRem.getFixedIn())) {
r.addFixedInItem(advRem.getFixedIn());
}
}
return;
}
}
r.addAdvisoriesItem(advRem);
if (advRem.getFixedIn() != null) {
var fixedIn = r.getFixedIn();
if (fixedIn == null || !fixedIn.contains(advRem.getFixedIn())) {
r.addFixedInItem(advRem.getFixedIn());
}
}
}

private VersionRange buildVersionRange(JsonNode versionRange) {
var vr = new VersionRange();
var schemeId = JsonUtils.getTextValue(versionRange, "version_scheme_id");
if (schemeId != null) {
Expand All @@ -259,75 +361,57 @@ private boolean processVersionRange(JsonNode purlStatus, Remediation r, boolean
vr.lowInclusive(lowInclusive);
}
var highVersion = JsonUtils.getTextValue(versionRange, "high_version");
var highInclusive = JsonUtils.getBooleanValue(versionRange, "high_inclusive");
if (highVersion != null) {
vr.highVersion(highVersion);
if (highInclusive == null || !highInclusive) {
if (dedup) {
var fixedIn = r.getFixedIn();
if (fixedIn == null || !fixedIn.contains(highVersion)) {
r.addFixedInItem(highVersion);
}
} else {
r.addFixedInItem(highVersion);
}
}
}
var highInclusive = JsonUtils.getBooleanValue(versionRange, "high_inclusive");
if (highInclusive != null) {
vr.highInclusive(highInclusive);
}
r.addVersionRangesItem(vr);
return true;
return vr;
}

private boolean processRemediations(JsonNode purlStatus, Remediation r) {
var remediations = (ArrayNode) purlStatus.get("remediations");
var advisoryInfo = buildAdvisoryInfo(purlStatus);
if (remediations != null && !remediations.isEmpty()) {
remediations.forEach(
rem -> {
var info = new RemediationInfo();
var category = JsonUtils.getTextValue(rem, "category");
if (category != null) {
try {
info.category(RemediationCategory.fromValue(category.toUpperCase()));
} catch (IllegalArgumentException e) {
LOGGER.infof("Unknown remediation category: %s", category);
}
}
var details = JsonUtils.getTextValue(rem, "details");
if (details != null) {
info.details(details);
}
var url = JsonUtils.getTextValue(rem, "url");
if (url != null) {
try {
info.url(URI.create(url));
} catch (IllegalArgumentException e) {
LOGGER.infof("Invalid remediation URL: %s", url);
}
}
if (advisoryInfo != null) {
info.advisory(advisoryInfo);
}
if (!isDuplicateRemediation(r, info)) {
r.addRemediationsItem(info);
}
});
return true;
} else {
var info =
buildRemediationInfo(purlStatus, r.getFixedIn() != null && !r.getFixedIn().isEmpty());
if (info != null && !isDuplicateRemediation(r, info)) {
r.addRemediationsItem(info);
return true;
private RemediationInfo buildRemediationInfoFromNode(JsonNode rem, AdvisoryInfo advisoryInfo) {
var info = new RemediationInfo();
var category = JsonUtils.getTextValue(rem, "category");
if (category != null) {
try {
info.category(RemediationCategory.fromValue(category.toUpperCase()));
} catch (IllegalArgumentException e) {
LOGGER.infof("Unknown remediation category: %s", category);
}
}
return false;
var details = JsonUtils.getTextValue(rem, "details");
if (details != null) {
info.details(details);
}
var url = JsonUtils.getTextValue(rem, "url");
if (url != null) {
try {
info.url(URI.create(url));
} catch (IllegalArgumentException e) {
LOGGER.infof("Invalid remediation URL: %s", url);
}
}
return info;
}

private boolean isDuplicateRemediation(Remediation r, RemediationInfo info) {
var existing = r.getRemediations();
private boolean isDuplicateVersionRange(AdvisoryRemediation advRem, VersionRange vr) {
var existing = advRem.getVersionRanges();
if (existing == null) {
return false;
}
return existing.stream()
.anyMatch(
e ->
Objects.equals(e.getLowVersion(), vr.getLowVersion())
&& Objects.equals(e.getHighVersion(), vr.getHighVersion())
&& Objects.equals(e.getLowInclusive(), vr.getLowInclusive())
&& Objects.equals(e.getHighInclusive(), vr.getHighInclusive())
&& Objects.equals(e.getVersionSchemeId(), vr.getVersionSchemeId()));
}

private boolean isDuplicateRemediationInfo(List<RemediationInfo> existing, RemediationInfo info) {
if (existing == null) {
return false;
}
Expand All @@ -336,9 +420,7 @@ private boolean isDuplicateRemediation(Remediation r, RemediationInfo info) {
e ->
Objects.equals(e.getCategory(), info.getCategory())
&& Objects.equals(e.getDetails(), info.getDetails())
&& Objects.equals(
e.getAdvisory() != null ? e.getAdvisory().getId() : null,
info.getAdvisory() != null ? info.getAdvisory().getId() : null));
&& Objects.equals(e.getUrl(), info.getUrl()));
}

private Remediation ensureRemediation(Issue issue) {
Expand Down Expand Up @@ -371,18 +453,10 @@ private AdvisoryInfo buildAdvisoryInfo(JsonNode purlStatus) {
} catch (IllegalArgumentException e) {
LOGGER.infof("Invalid advisory URL: %s", identifier);
}
}
return info;
}

private RemediationInfo buildRemediationInfo(JsonNode purlStatus, boolean hasFixedVersions) {
var advisoryInfo = buildAdvisoryInfo(purlStatus);
if (advisoryInfo == null) {
return null;
}
var info = new RemediationInfo().advisory(advisoryInfo);
if (hasFixedVersions) {
info.category(RemediationCategory.VENDOR_FIX);
} else if (identifier != null && identifier.startsWith("GHSA-")) {
info.url(URI.create("https://github.com/advisories/" + identifier));
} else if (documentId.startsWith("GHSA-")) {
info.url(URI.create("https://github.com/advisories/" + documentId));
}
return info;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/*
* Copyright 2023-2025 Trustify Dependency Analytics Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.github.guacsec.trustifyda.integration.providers.trustify;

import java.util.Comparator;
import java.util.regex.Pattern;

enum VersionComparator implements Comparator<String> {
INSTANCE;

private static final Pattern SPLIT = Pattern.compile("[.\\-]");

@Override
public int compare(String a, String b) {
if (a == null && b == null) return 0;
if (a == null) return -1;
if (b == null) return 1;

String[] partsA = SPLIT.split(a);
String[] partsB = SPLIT.split(b);
int len = Math.max(partsA.length, partsB.length);

for (int i = 0; i < len; i++) {
String sa = i < partsA.length ? partsA[i] : "";
String sb = i < partsB.length ? partsB[i] : "";
int cmp = comparePart(sa, sb);
if (cmp != 0) return cmp;
}
return 0;
}

private static int comparePart(String a, String b) {
boolean aNum = isNumeric(a);
boolean bNum = isNumeric(b);
if (aNum && bNum) {
return Long.compare(Long.parseLong(a), Long.parseLong(b));
}
if (aNum) return -1;
if (bNum) return 1;
return a.compareToIgnoreCase(b);
}

private static boolean isNumeric(String s) {
if (s.isEmpty()) return false;
for (int i = 0; i < s.length(); i++) {
if (!Character.isDigit(s.charAt(i))) return false;
}
return true;
}
}
Loading
Loading