-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Find Nuget dependency vulnerabilities (#119)
* Find Nuget dependency vulnerabilities * Add option to patch Nuget dependency versions * Merge loops * Drop generate and show fix in two version ranges * Minor clean up * Show package with highest patch version picked up * Expand Vulnerability equals/hashCode to include introducedVersion As the same CVE can appear in multiple dependency version ranges. * Fix issue with duplicated dependencies at different versions * Partition vulnerabilities into those patched or not * Inline local variable to show symmetry
- Loading branch information
Showing
9 changed files
with
2,787 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,19 +37,24 @@ jobs: | |
- name: Create commit message | ||
run: | | ||
echo "MSG=[Auto] GitHub advisories as of $(date +'%Y-%m-%dT%H%M')" >> $GITHUB_ENV | ||
- name: Commit and push to rewrite-java-dependencies | ||
- name: Commit and push Maven dependency vulnerabilities to rewrite-java-dependencies | ||
run: | | ||
./gradlew parseGithubAdvisoryDatabase --args="./advisory-database Maven src/main/resources/advisories-maven.csv" | ||
sort --output=src/main/resources/advisories-maven.csv src/main/resources/advisories-maven.csv | ||
git diff-index --quiet HEAD src/main/resources/advisories-maven.csv || (git commit --message "${{ env.MSG }}" src/main/resources/advisories-maven.csv && git push origin main) | ||
- name: Commit and push Nuget dependency vulnerabilities to rewrite-java-dependencies | ||
run: | | ||
./gradlew parseGithubAdvisoryDatabase --args="./advisory-database Nuget src/main/resources/advisories-nuget.csv" | ||
sort --output=src/main/resources/advisories-nuget.csv src/main/resources/advisories-nuget.csv | ||
git diff-index --quiet HEAD src/main/resources/advisories-nuget.csv || (git commit --message "${{ env.MSG }}" src/main/resources/advisories-nuget.csv && git push origin main) | ||
# Load SSH deploy-key | ||
- uses: webfactory/[email protected] | ||
with: | ||
ssh-private-key: ${{ secrets.REWRITE_NODEJS_DEPLOY_KEY }} | ||
|
||
# Commit and push NPM advisories to rewrite-nodejs | ||
- name: Commit and push to rewrite-nodejs | ||
- name: Commit and push Npm dependency vulnerabilities to rewrite-nodejs | ||
run: | | ||
git clone --depth 1 [email protected]:openrewrite/rewrite-nodejs.git | ||
./gradlew parseGithubAdvisoryDatabase --args="./advisory-database NPM rewrite-nodejs/src/main/resources/advisories-npm.csv" | ||
|
201 changes: 201 additions & 0 deletions
201
src/main/java/org/openrewrite/csharp/dependencies/DependencyVulnerabilityCheck.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,201 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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 org.openrewrite.csharp.dependencies; | ||
|
||
import com.fasterxml.jackson.databind.MappingIterator; | ||
import com.fasterxml.jackson.dataformat.csv.CsvMapper; | ||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; | ||
import lombok.EqualsAndHashCode; | ||
import lombok.Value; | ||
import org.jspecify.annotations.Nullable; | ||
import org.openrewrite.ExecutionContext; | ||
import org.openrewrite.Option; | ||
import org.openrewrite.ScanningRecipe; | ||
import org.openrewrite.TreeVisitor; | ||
import org.openrewrite.csharp.dependencies.table.VulnerabilityReport; | ||
import org.openrewrite.csharp.dependencies.trait.PackageReference; | ||
import org.openrewrite.internal.StringUtils; | ||
import org.openrewrite.java.dependencies.Vulnerability; | ||
import org.openrewrite.java.dependencies.internal.StaticVersionComparator; | ||
import org.openrewrite.java.dependencies.internal.Version; | ||
import org.openrewrite.java.dependencies.internal.VersionParser; | ||
import org.openrewrite.marker.SearchResult; | ||
import org.openrewrite.semver.LatestPatch; | ||
import org.openrewrite.xml.tree.Xml; | ||
|
||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.util.*; | ||
|
||
import static java.util.Collections.emptySet; | ||
import static java.util.stream.Collectors.joining; | ||
import static java.util.stream.Collectors.partitioningBy; | ||
|
||
@Value | ||
@EqualsAndHashCode(callSuper = false) | ||
public class DependencyVulnerabilityCheck extends ScanningRecipe<DependencyVulnerabilityCheck.Accumulator> { | ||
transient VersionParser versionParser = new VersionParser(); | ||
transient VulnerabilityReport report = new VulnerabilityReport(this); | ||
|
||
@Option(displayName = "Add search markers", | ||
description = "Report each vulnerability as search result markers. " + | ||
"When enabled you can see which dependencies are bringing in vulnerable transitives in the diff view. " + | ||
"By default these markers are omitted, making it easier to see version upgrades within the diff.", | ||
required = false) | ||
@Nullable | ||
Boolean addMarkers; | ||
|
||
@Override | ||
public String getDisplayName() { | ||
return "Find and fix vulnerable Nuget dependencies"; | ||
} | ||
|
||
@Override | ||
public String getDescription() { | ||
//language=markdown | ||
return "This software composition analysis (SCA) tool detects and upgrades dependencies with publicly disclosed vulnerabilities. " + | ||
"This recipe both generates a report of vulnerable dependencies and upgrades to newer versions with fixes. " + | ||
"This recipe **only** upgrades to the latest **patch** version. If a minor or major upgrade is required to reach the fixed version, this recipe will not make any changes. " + | ||
"Vulnerability information comes from the [GitHub Security Advisory Database](https://docs.github.com/en/code-security/security-advisories/global-security-advisories/about-the-github-advisory-database), " + | ||
"which aggregates vulnerability data from several public databases, including the [National Vulnerability Database](https://nvd.nist.gov/) maintained by the United States government. " + | ||
"Dependencies following [Semantic Versioning](https://semver.org/) will see their _patch_ version updated where applicable."; | ||
} | ||
|
||
@Value | ||
public static class Accumulator { | ||
Map<String, List<Vulnerability>> db; | ||
Map<NameVersion, Set<Vulnerability>> vulnerabilities; | ||
|
||
@Value | ||
static class NameVersion { | ||
/** | ||
* The name of the package as specified in the package.json. | ||
*/ | ||
String name; | ||
|
||
/** | ||
* The resolved version actually in use, which may be different from the version specified in the package.json. | ||
*/ | ||
String version; | ||
} | ||
} | ||
|
||
@Override | ||
public Accumulator getInitialValue(ExecutionContext ctx) { | ||
CsvMapper csvMapper = new CsvMapper(); | ||
csvMapper.registerModule(new JavaTimeModule()); | ||
Map<String, List<Vulnerability>> db = new HashMap<>(); | ||
|
||
try (InputStream resourceAsStream = DependencyVulnerabilityCheck.class.getResourceAsStream("/advisories-nuget.csv"); | ||
MappingIterator<Vulnerability> vs = csvMapper.readerWithSchemaFor(Vulnerability.class).readValues(resourceAsStream)) { | ||
while (vs.hasNextValue()) { | ||
Vulnerability v = vs.nextValue(); | ||
db.computeIfAbsent(v.getGroupArtifact(), g -> new ArrayList<>()).add(v); | ||
} | ||
} catch (IOException e) { | ||
throw new RuntimeException(e); | ||
} | ||
|
||
return new Accumulator(db, new HashMap<>()); | ||
} | ||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getScanner(Accumulator acc) { | ||
return new PackageReference.Matcher().asVisitor((ref, ctx) -> { | ||
String dependencyName = ref.getInclude(); | ||
for (Vulnerability v : acc.db.getOrDefault(dependencyName, Collections.emptyList())) { | ||
String dependencyVersion = ref.getVersion(); | ||
if (isVulnerable(dependencyVersion, v)) { | ||
// Add all vulnerable dependencies to the accumulator | ||
acc.vulnerabilities | ||
.computeIfAbsent(new Accumulator.NameVersion(dependencyName, dependencyVersion), nv -> new LinkedHashSet<>()) | ||
.add(v); | ||
|
||
// Insert a row into the report for each vulnerability | ||
report.insertRow(ctx, new VulnerabilityReport.Row( | ||
v.getCve(), | ||
dependencyName, | ||
dependencyVersion, | ||
v.getFixedVersion(), | ||
isFixWithPatchVersionUpdateOnly(dependencyVersion, v), | ||
v.getSummary(), | ||
v.getSeverity().toString(), | ||
0, | ||
v.getCwes() | ||
)); | ||
} | ||
} | ||
return ref.getTree(); | ||
}); | ||
} | ||
|
||
|
||
private static final Comparator<Version> vc = new StaticVersionComparator(); | ||
|
||
private boolean isVulnerable(String dependencyVersion, Vulnerability v) { | ||
return vc.compare( | ||
versionParser.transform(dependencyVersion), | ||
versionParser.transform(v.getFixedVersion())) < 0; | ||
} | ||
|
||
private static final LatestPatch latestPatch = new LatestPatch(null); | ||
|
||
private static boolean isFixWithPatchVersionUpdateOnly(String dependencyVersion, Vulnerability v) { | ||
return !StringUtils.isBlank(v.getFixedVersion()) && | ||
latestPatch.isValid(dependencyVersion, v.getFixedVersion()) && | ||
latestPatch.compare(dependencyVersion, dependencyVersion, v.getFixedVersion()) < 0; | ||
} | ||
|
||
@Override | ||
public TreeVisitor<?, ExecutionContext> getVisitor(Accumulator acc) { | ||
return new PackageReference.Matcher().asVisitor((ref, ctx) -> { | ||
Xml.Tag tag = ref.getTree(); | ||
|
||
// Partition vulnerabilities into those that can be fixed with a patch version update and those that can't | ||
String dependencyVersion = ref.getVersion(); | ||
Map<Boolean, List<Vulnerability>> vulnerabilities = acc.vulnerabilities | ||
.getOrDefault(new Accumulator.NameVersion(ref.getInclude(), ref.getVersion()), emptySet()) | ||
.stream() | ||
.filter(v -> StringUtils.isBlank(v.getFixedVersion()) || isVulnerable(dependencyVersion, v)) | ||
.collect(partitioningBy(v -> isFixWithPatchVersionUpdateOnly(dependencyVersion, v))); | ||
|
||
// Bump to highest fixed patch version | ||
String highestFixedPatchVersion = vulnerabilities.get(true).stream() | ||
.max(Comparator.comparing(v -> versionParser.transform(v.getFixedVersion()), vc)) | ||
.map(Vulnerability::getFixedVersion) | ||
.orElse(null); | ||
if (highestFixedPatchVersion != null) { | ||
tag = ref.withVersion(highestFixedPatchVersion); | ||
} | ||
|
||
// Add marker of vulnerabilities not patched | ||
List<Vulnerability> remainingVulnerabilities = vulnerabilities.get(false); | ||
if (Boolean.TRUE.equals(addMarkers) && !remainingVulnerabilities.isEmpty()) { | ||
tag = SearchResult.found(tag, | ||
"This dependency has the following vulnerabilities:\n" + | ||
remainingVulnerabilities.stream() | ||
.map(v -> String.format("%s (%s severity%s) - %s", | ||
v.getCve(), | ||
v.getSeverity(), | ||
StringUtils.isBlank(v.getFixedVersion()) ? "" : ", fixed in " + v.getFixedVersion(), | ||
v.getSummary())) | ||
.collect(joining("\n"))); | ||
} | ||
|
||
return tag; | ||
}); | ||
} | ||
} |
74 changes: 74 additions & 0 deletions
74
src/main/java/org/openrewrite/csharp/dependencies/table/VulnerabilityReport.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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 org.openrewrite.csharp.dependencies.table; | ||
|
||
import com.fasterxml.jackson.annotation.JsonIgnoreType; | ||
import lombok.Value; | ||
import org.openrewrite.Column; | ||
import org.openrewrite.DataTable; | ||
import org.openrewrite.Recipe; | ||
|
||
@JsonIgnoreType | ||
public class VulnerabilityReport extends DataTable<VulnerabilityReport.Row> { | ||
|
||
public VulnerabilityReport(Recipe recipe) { | ||
super(recipe, | ||
"Vulnerability report", | ||
"A vulnerability report that includes detailed information about the affected artifact and the corresponding CVEs."); | ||
} | ||
|
||
@Value | ||
public static class Row { | ||
@Column(displayName = "CVE", | ||
description = "The CVE number.") | ||
String cve; | ||
|
||
@Column(displayName = "Package name", | ||
description = "The package name.") | ||
String packageName; | ||
|
||
@Column(displayName = "Version", | ||
description = "The resolved version.") | ||
String version; | ||
|
||
@Column(displayName = "Fixed in version", | ||
description = "The minimum version that is no longer vulnerable.") | ||
String fixedVersion; | ||
|
||
@Column(displayName = "Fixable with version update only", | ||
//language=markdown | ||
description = "Whether the vulnerability is likely to be fixed by increasing the dependency version only, " + | ||
"with no code modifications required. This is a heuristic which assumes that the dependency " + | ||
"is accurately versioned according to [semver](https://semver.org/).") | ||
boolean fixWithVersionUpdateOnly; | ||
|
||
@Column(displayName = "Summary", | ||
description = "The summary of the CVE.") | ||
String summary; | ||
|
||
@Column(displayName = "Base score", | ||
description = "The calculated base score.") | ||
String severity; | ||
|
||
@Column(displayName = "Depth", | ||
description = "Zero for direct dependencies.") | ||
Integer depth; | ||
|
||
@Column(displayName = "CWEs", | ||
description = "Common Weakness Enumeration (CWE) identifiers; semicolon separated.") | ||
String CWEs; | ||
} | ||
} |
21 changes: 21 additions & 0 deletions
21
src/main/java/org/openrewrite/csharp/dependencies/table/package-info.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
/* | ||
* Copyright 2024 the original author or authors. | ||
* <p> | ||
* 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 | ||
* <p> | ||
* https://www.apache.org/licenses/LICENSE-2.0 | ||
* <p> | ||
* 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. | ||
*/ | ||
@NonNullApi | ||
@NonNullFields | ||
package org.openrewrite.csharp.dependencies.table; | ||
|
||
import org.openrewrite.internal.lang.NonNullApi; | ||
import org.openrewrite.internal.lang.NonNullFields; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.