Skip to content

Commit

Permalink
Find Nuget dependency vulnerabilities (#119)
Browse files Browse the repository at this point in the history
* 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
timtebeek authored Aug 26, 2024
1 parent 08c1c78 commit fae2e8e
Show file tree
Hide file tree
Showing 9 changed files with 2,787 additions and 2 deletions.
9 changes: 7 additions & 2 deletions .github/workflows/advisories.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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;
});
}
}
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;
}
}
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;
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@

import lombok.Value;
import org.openrewrite.Cursor;
import org.openrewrite.InMemoryExecutionContext;
import org.openrewrite.trait.SimpleTraitMatcher;
import org.openrewrite.trait.Trait;
import org.openrewrite.xml.ChangeTagAttribute;
import org.openrewrite.xml.XPathMatcher;
import org.openrewrite.xml.tree.Xml;

import java.util.List;
import java.util.Map;
import java.util.Objects;

import static java.util.stream.Collectors.toMap;

Expand All @@ -35,6 +38,18 @@ public class PackageReference implements Trait<Xml.Tag> {
String include;
String version;

public Xml.Tag withVersion(String newVersion) {
Xml.Tag tag = getTree();
if (!Objects.equals(this.version, newVersion)) {
InMemoryExecutionContext ctx = new InMemoryExecutionContext();
tag = (Xml.Tag) new ChangeTagAttribute("//PackageReference", "Version", newVersion, this.version, null)
.getVisitor().visitNonNull(tag, ctx);
tag = (Xml.Tag) new ChangeTagAttribute("/packages/package", "version", newVersion, this.version, null)
.getVisitor().visitNonNull(tag, ctx);
}
return tag;
}

public static class Matcher extends SimpleTraitMatcher<PackageReference> {
XPathMatcher packageReference = new XPathMatcher("//PackageReference");
XPathMatcher packageConfig = new XPathMatcher("/packages/package");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public class Vulnerability {
ZonedDateTime publishedAt;
String summary;
String groupArtifact;
@EqualsAndHashCode.Include
String introducedVersion;
String fixedVersion;
Severity severity;
Expand Down
Loading

0 comments on commit fae2e8e

Please sign in to comment.