Skip to content

Commit

Permalink
POC for vuln analysis via OSV API
Browse files Browse the repository at this point in the history
Signed-off-by: nscuro <[email protected]>
  • Loading branch information
nscuro committed Jul 3, 2022
1 parent 58dae19 commit d9fc2ee
Show file tree
Hide file tree
Showing 9 changed files with 798 additions and 2 deletions.
33 changes: 33 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@
<lib.jdbc-driver.mssql.version>11.1.2.jre11-preview</lib.jdbc-driver.mssql.version>
<lib.jdbc-driver.mysql.version>8.0.29</lib.jdbc-driver.mysql.version>
<lib.jdbc-driver.postgresql.version>42.4.0</lib.jdbc-driver.postgresql.version>
<!-- Maven Plugin Versions -->
<plugin.openapi-generator.version>6.0.0</plugin.openapi-generator.version>
<!-- Maven Plugin Properties -->
<plugin.cyclonedx.projectType>application</plugin.cyclonedx.projectType>
<plugin.retirejs.breakOnFailure>false</plugin.retirejs.breakOnFailure>
Expand Down Expand Up @@ -269,6 +271,12 @@
<artifactId>commons-compress</artifactId>
<version>1.21</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${lib.jackson-databind.version}</version>
</dependency>
<!-- Test Dependencies -->
<dependency>
<groupId>junit</groupId>
Expand Down Expand Up @@ -330,6 +338,31 @@
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${plugin.openapi-generator.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/osv_service_v1.swagger.json</inputSpec>
<generatorName>java</generatorName>
<generateApiTests>false</generateApiTests>
<generateModelTests>false</generateModelTests>
<configOptions>
<sourceFolder>src/gen/java/main</sourceFolder>
<library>apache-httpclient</library>
<apiPackage>com.google.osv.api</apiPackage>
<modelPackage>com.google.osv.model</modelPackage>
<serializationLibrary>jackson</serializationLibrary>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.dependencytrack.tasks.repositories.RepositoryMetaAnalyzerTask;
import org.dependencytrack.tasks.scanners.InternalAnalysisTask;
import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask;
import org.dependencytrack.tasks.scanners.OsvAnalysisTask;
import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask;

import javax.servlet.ServletContextEvent;
Expand Down Expand Up @@ -80,6 +81,7 @@ public void contextInitialized(final ServletContextEvent event) {
EVENT_SERVICE.subscribe(InternalAnalysisEvent.class, InternalAnalysisTask.class);
EVENT_SERVICE.subscribe(OssIndexAnalysisEvent.class, OssIndexAnalysisTask.class);
EVENT_SERVICE.subscribe(GitHubAdvisoryMirrorEvent.class, GitHubAdvisoryMirrorTask.class);
EVENT_SERVICE.subscribe(OsvAnalysisEvent.class, OsvAnalysisTask.class);
EVENT_SERVICE.subscribe(VulnDbSyncEvent.class, VulnDbSyncTask.class);
EVENT_SERVICE.subscribe(VulnDbAnalysisEvent.class, VulnDbAnalysisTask.class);
EVENT_SERVICE.subscribe(VulnerabilityAnalysisEvent.class, VulnerabilityAnalysisTask.class);
Expand Down Expand Up @@ -114,6 +116,7 @@ public void contextDestroyed(final ServletContextEvent event) {
EVENT_SERVICE.unsubscribe(InternalAnalysisTask.class);
EVENT_SERVICE.unsubscribe(OssIndexAnalysisTask.class);
EVENT_SERVICE.unsubscribe(GitHubAdvisoryMirrorTask.class);
EVENT_SERVICE.unsubscribe(OsvAnalysisTask.class);
EVENT_SERVICE.unsubscribe(VulnDbSyncTask.class);
EVENT_SERVICE.unsubscribe(VulnDbAnalysisTask.class);
EVENT_SERVICE.unsubscribe(VulnerabilityAnalysisTask.class);
Expand Down
43 changes: 43 additions & 0 deletions src/main/java/org/dependencytrack/event/OsvAnalysisEvent.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* This file is part of Dependency-Track.
*
* 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.
*
* SPDX-License-Identifier: Apache-2.0
* Copyright (c) Steve Springett. All Rights Reserved.
*/
package org.dependencytrack.event;

import org.dependencytrack.model.Component;

import java.util.List;

/**
* Defines an event used to start an analysis via Sonatype OSS Index REST API.
*
* @author Steve Springett
* @since 3.2.0
*/
public class OsvAnalysisEvent extends VulnerabilityAnalysisEvent {

public OsvAnalysisEvent() { }

public OsvAnalysisEvent(final Component component) {
super(component);
}

public OsvAnalysisEvent(final List<Component> components) {
super(components);
}

}
3 changes: 2 additions & 1 deletion src/main/java/org/dependencytrack/model/Vulnerability.java
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,8 @@ public enum Source {
VULNDB, // VulnDB from Risk Based Security
OSSINDEX, // Sonatype OSS Index
RETIREJS, // Retire.js
INTERNAL // Internally-managed (and manually entered) vulnerability
INTERNAL, // Internally-managed (and manually entered) vulnerability
OSV
}

@PrimaryKey
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.dependencytrack.event.InternalAnalysisEvent;
import org.dependencytrack.event.MetricsUpdateEvent;
import org.dependencytrack.event.OssIndexAnalysisEvent;
import org.dependencytrack.event.OsvAnalysisEvent;
import org.dependencytrack.event.PortfolioVulnerabilityAnalysisEvent;
import org.dependencytrack.event.VulnDbAnalysisEvent;
import org.dependencytrack.event.VulnerabilityAnalysisEvent;
Expand All @@ -35,6 +36,7 @@
import org.dependencytrack.tasks.scanners.CacheableScanTask;
import org.dependencytrack.tasks.scanners.InternalAnalysisTask;
import org.dependencytrack.tasks.scanners.OssIndexAnalysisTask;
import org.dependencytrack.tasks.scanners.OsvAnalysisTask;
import org.dependencytrack.tasks.scanners.ScanTask;
import org.dependencytrack.tasks.scanners.VulnDbAnalysisTask;

Expand All @@ -49,6 +51,7 @@ public class VulnerabilityAnalysisTask implements Subscriber {

private final List<Component> internalCandidates = new ArrayList<>();
private final List<Component> ossIndexCandidates = new ArrayList<>();
private final List<Component> osvCandidates = new ArrayList<>();
private final List<Component> vulnDbCandidates = new ArrayList<>();

/**
Expand Down Expand Up @@ -103,10 +106,12 @@ private void analyzeComponents(final QueryManager qm, final List<Component> comp
*/
final InternalAnalysisTask internalAnalysisTask = new InternalAnalysisTask();
final OssIndexAnalysisTask ossIndexAnalysisTask = new OssIndexAnalysisTask();
final var osvAnalysisTask = new OsvAnalysisTask();
final VulnDbAnalysisTask vulnDbAnalysisTask = new VulnDbAnalysisTask();
for (final Component component : components) {
inspectComponentReadiness(component, internalAnalysisTask, internalCandidates);
inspectComponentReadiness(component, ossIndexAnalysisTask, ossIndexCandidates);
inspectComponentReadiness(component, osvAnalysisTask, osvCandidates);
inspectComponentReadiness(component, vulnDbAnalysisTask, vulnDbCandidates);
}

Expand All @@ -117,6 +122,7 @@ private void analyzeComponents(final QueryManager qm, final List<Component> comp
// from interrupting the successful execution of all analyzers.
performAnalysis(internalAnalysisTask, new InternalAnalysisEvent(internalCandidates));
performAnalysis(ossIndexAnalysisTask, new OssIndexAnalysisEvent(ossIndexCandidates));
performAnalysis(osvAnalysisTask, new OsvAnalysisEvent(osvCandidates));
performAnalysis(vulnDbAnalysisTask, new VulnDbAnalysisEvent(vulnDbCandidates));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ public enum AnalyzerIdentity {
OSSINDEX_ANALYZER,
NPM_AUDIT_ANALYZER,
VULNDB_ANALYZER,
OSV_ANALYZER,
NONE
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public AnalyzerIdentity getAnalyzerIdentity() {
*/
public void inform(final Event e) {
if (e instanceof OssIndexAnalysisEvent) {
if (!super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) {
if (super.isEnabled(ConfigPropertyConstants.SCANNER_OSSINDEX_ENABLED)) {
return;
}
try (QueryManager qm = new QueryManager()) {
Expand Down
195 changes: 195 additions & 0 deletions src/main/java/org/dependencytrack/tasks/scanners/OsvAnalysisTask.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package org.dependencytrack.tasks.scanners;

import alpine.common.logging.Logger;
import alpine.event.framework.Event;
import alpine.event.framework.Subscriber;
import com.google.osv.ApiClient;
import com.google.osv.ApiException;
import com.google.osv.api.ApiApi;
import com.google.osv.model.OsvVulnerability;
import com.google.osv.model.V1BatchQuery;
import com.google.osv.model.V1BatchVulnerabilityList;
import com.google.osv.model.V1Query;
import com.google.osv.model.V1QueryPackage;
import com.google.osv.model.V1VulnerabilityList;
import org.apache.commons.lang3.StringUtils;
import org.dependencytrack.common.HttpClientPool;
import org.dependencytrack.event.OsvAnalysisEvent;
import org.dependencytrack.model.Component;
import org.dependencytrack.model.Vulnerability;
import org.dependencytrack.persistence.QueryManager;
import org.dependencytrack.util.NotificationUtil;

import java.util.List;
import java.util.Optional;

public class OsvAnalysisTask extends BaseComponentAnalyzerTask implements Subscriber {

private static final Logger LOGGER = Logger.getLogger(OsvAnalysisTask.class);

@Override
public void inform(final Event e) {
if (!(e instanceof OsvAnalysisEvent)) {
return;
}

final var analysisEvent = (OsvAnalysisEvent) e;
if (!analysisEvent.getComponents().isEmpty()) {
analyze(analysisEvent.getComponents());
}
}

@Override
public AnalyzerIdentity getAnalyzerIdentity() {
return AnalyzerIdentity.OSV_ANALYZER;
}

@Override
public void analyze(final List<Component> components) {
final var osvApiClient = new ApiClient(HttpClientPool.getClient());
final var osvApi = new ApiApi(osvApiClient);

// TODO: Limit batch size to <= 1000
final var batchQuery = new V1BatchQuery();
for (final Component component : components) {
batchQuery.addQueriesItem(new V1Query()
._package(new V1QueryPackage().purl(component.getPurl().toString())));
}

final List<V1VulnerabilityList> vulnerabilityLists;
try {
final V1BatchVulnerabilityList queryResult = osvApi.oSVQueryAffectedBatch(batchQuery);

vulnerabilityLists = queryResult.getResults();
if (vulnerabilityLists == null || vulnerabilityLists.isEmpty()) {
LOGGER.warn("OSV returned no results");
return;
}
} catch (ApiException ex) {
LOGGER.error("Performing batch query failed", ex);
return;
}

// OSV will return a list of vulnerabilities for each query in the batch we submitted.
// If no vulnerabilities are found, the respective list will be empty.
// Correlation between input queries / components and results is based on their order.
if (vulnerabilityLists.size() != components.size()) {
LOGGER.warn("OSV returned " + vulnerabilityLists.size() + " results, but we expected " + components.size());
return;
}

for (int i = 0; i < components.size(); i++) {
final V1VulnerabilityList vulnerabilityList = vulnerabilityLists.get(i);
if (vulnerabilityList.getVulns() == null || vulnerabilityList.getVulns().isEmpty()) {
LOGGER.info("No vulnerabilities found for component " + components.get(i).getPurl());
continue;
}

try (final var qm = new QueryManager()) {
final var component = qm.getObjectById(Component.class, components.get(i).getId());

for (OsvVulnerability osvVuln : vulnerabilityList.getVulns()) {
// Do we know this vulnerability already?
Vulnerability vuln = resolveVulnerability(qm, osvVuln.getId());

if (vuln == null) {
try {
// Vulnerabilities in batch query responses only contain their respective ID.
// If we need more fields, we have to explicitly request them.
// TODO: Cache these responses?
osvVuln = osvApi.oSVGetVulnById(osvVuln.getId());
} catch (ApiException e) {
LOGGER.error("Requesting details for vulnerability " + osvVuln.getId() + " failed", e);
continue;
}

// Primary ID of the vulnerability is unknown, but maybe we know one of its aliases?
if (osvVuln.getAliases() != null && !osvVuln.getAliases().isEmpty()) {
for (final String alias : osvVuln.getAliases()) {
vuln = resolveVulnerability(qm, alias);
if (vuln != null) {
break;
}
}
}

// Vulnerability is not known to us yet, so we have to create it.
if (vuln == null) {
vuln = new Vulnerability();

// Similar to how it's done in OssIndexAnalysisTask, we prefer using the ID
// of the authoritative vulnerability source. We also prioritize CVE/NVD over GHSA.
// Vulnerability data will ultimately be overridden by those sources once
// Dependency-Track starts mirroring them.
final Optional<String> optCve = resolveCve(osvVuln);
final Optional<String> optGhsaId = resolveGhsaId(osvVuln);
if (optCve.isPresent()) {
vuln.setSource(Vulnerability.Source.NVD);
vuln.setVulnId(optCve.get());
} else if (optGhsaId.isPresent()) {
vuln.setSource(Vulnerability.Source.GITHUB);
vuln.setVulnId(optGhsaId.get());
} else {
vuln.setSource(Vulnerability.Source.OSV);
vuln.setVulnId(osvVuln.getId());
}

vuln.setTitle(StringUtils.truncate(osvVuln.getSummary(), 255));
vuln.setDescription(osvVuln.getDetails());

// TODO: Parse more details

vuln = qm.createVulnerability(vuln, false);
}
}

LOGGER.info(osvVuln.getId() + " matched to " + vuln.getVulnId() + " (" + vuln.getSource() + ")");
NotificationUtil.analyzeNotificationCriteria(qm, vuln, component);
qm.addVulnerability(vuln, component, this.getAnalyzerIdentity(), osvVuln.getId(), "https://osv.dev/vulnerability/" + osvVuln.getId());
}
}
}
}

@Override
public boolean isCapable(final Component component) {
return component.getPurl() != null
&& component.getPurl().getName() != null
&& component.getPurl().getVersion() != null;
}

private Vulnerability resolveVulnerability(final QueryManager qm, final String vulnerabilityId) {
return qm.getVulnerabilityByVulnId(resolveVulnerabilitySource(vulnerabilityId), vulnerabilityId);
}

private Vulnerability.Source resolveVulnerabilitySource(final String vulnerabilityId) {
if (StringUtils.startsWith(vulnerabilityId, "CVE-")) {
return Vulnerability.Source.NVD;
} else if (StringUtils.startsWith(vulnerabilityId, "GHSA-")) {
return Vulnerability.Source.GITHUB;
}

return Vulnerability.Source.OSV;
}

private Optional<String> resolveCve(final OsvVulnerability osvVuln) {
if (resolveVulnerabilitySource(osvVuln.getId()) == Vulnerability.Source.NVD) {
return Optional.ofNullable(osvVuln.getId());
}

return Optional.ofNullable(osvVuln.getAliases()).orElseGet(List::of).stream()
.filter(alias -> resolveVulnerabilitySource(alias) == Vulnerability.Source.NVD)
.findFirst();
}

private Optional<String> resolveGhsaId(final OsvVulnerability osvVuln) {
if (resolveVulnerabilitySource(osvVuln.getId()) == Vulnerability.Source.GITHUB) {
return Optional.ofNullable(osvVuln.getId());
}

return Optional.ofNullable(osvVuln.getAliases()).orElseGet(List::of).stream()
.filter(alias -> resolveVulnerabilitySource(alias) == Vulnerability.Source.GITHUB)
.findFirst();
}

}
Loading

0 comments on commit d9fc2ee

Please sign in to comment.