Skip to content

Commit 35b5713

Browse files
committed
Migrate NVD mirroring from hyades mirror-service
* Adds a new `VulnDataSource` extension to mirror contents of the NVD. * Uses new JSON 2.0 feeds instead of NVD REST API. * Modifies `NistMirrorTask` to no longer emit Kafka events, but instead invoke the new NVD `VulnDataSource`. Closes DependencyTrack/hyades#1856 Signed-off-by: nscuro <[email protected]>
1 parent af36f06 commit 35b5713

File tree

41 files changed

+4214
-101
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+4214
-101
lines changed

alpine/pom.xml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,6 @@
8686
<lib.javassist.version>3.30.2-GA</lib.javassist.version>
8787
<lib.jaxb-runtime.version>4.0.5</lib.jaxb-runtime.version>
8888
<lib.jdo.api.version>3.2.1</lib.jdo.api.version>
89-
<lib.json-unit.version>4.1.1</lib.json-unit.version>
9089
<lib.jsonwebtoken.version>0.13.0</lib.jsonwebtoken.version>
9190
<lib.jsr305.version>3.0.2</lib.jsr305.version>
9291
<lib.logback.version>1.5.18</lib.logback.version>
@@ -268,12 +267,6 @@
268267
<version>${lib.mockito.version}</version>
269268
<scope>test</scope>
270269
</dependency>
271-
<dependency>
272-
<groupId>net.javacrumbs.json-unit</groupId>
273-
<artifactId>json-unit-assertj</artifactId>
274-
<version>${lib.json-unit.version}</version>
275-
<scope>test</scope>
276-
</dependency>
277270
<dependency>
278271
<groupId>com.github.tomakehurst</groupId>
279272
<artifactId>wiremock-jre8-standalone</artifactId>

api/src/main/openapi/components/schemas/extension-config-type.yaml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ description: |-
2424
* `INTEGER` represents an integer, e.g. `666`.
2525
* `PATH` represents a file path, e.g. `/foo/bar.baz`.
2626
* `STRING` represents a simple string, e.g. `foo bar baz`.
27+
* `URL` represents a uniform resource locator, e.g. `https://example.com`.
2728
enum:
2829
- BOOLEAN
2930
- DURATION
3031
- INSTANT
3132
- INTEGER
3233
- PATH
33-
- STRING
34+
- STRING
35+
- URL

api/src/main/openapi/components/schemas/update-extension-configs-request-item.yaml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,5 +29,4 @@ properties:
2929
to provide the clear text secret again.
3030
type: string
3131
required:
32-
- name
33-
- value
32+
- name

apiserver/pom.xml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
<lib.awaitility.version>4.3.0</lib.awaitility.version>
2222
<lib.cloud-sql-postgres-socket-factory.version>1.25.3</lib.cloud-sql-postgres-socket-factory.version>
2323
<lib.commons-compress.version>1.28.0</lib.commons-compress.version>
24-
<lib.cpe-parser.version>3.0.0</lib.cpe-parser.version>
2524
<lib.cvss-calculator.version>1.4.3</lib.cvss-calculator.version>
2625
<lib.owasp-rr-calculator.version>1.0.1</lib.owasp-rr-calculator.version>
2726
<lib.cyclonedx-java.version>10.2.1</lib.cyclonedx-java.version>
@@ -40,7 +39,6 @@
4039
<lib.swagger.version>2.2.37</lib.swagger.version>
4140
<lib.swagger-parser.version>2.1.34</lib.swagger-parser.version>
4241
<lib.system-rules.version>1.19.0</lib.system-rules.version>
43-
<lib.versatile.version>0.7.0</lib.versatile.version>
4442
<lib.woodstox.version>7.1.1</lib.woodstox.version>
4543
<lib.junit-params.version>1.1.1</lib.junit-params.version>
4644
<lib.log4j-over-slf4j.version>2.0.17</lib.log4j-over-slf4j.version>
@@ -109,6 +107,12 @@
109107
<version>${project.version}</version>
110108
</dependency>
111109

110+
<dependency>
111+
<groupId>org.dependencytrack</groupId>
112+
<artifactId>vuln-data-source-nvd</artifactId>
113+
<version>${project.version}</version>
114+
</dependency>
115+
112116
<!-- Alpine -->
113117
<dependency>
114118
<groupId>org.dependencytrack</groupId>
@@ -199,7 +203,6 @@
199203
<dependency>
200204
<groupId>us.springett</groupId>
201205
<artifactId>cpe-parser</artifactId>
202-
<version>${lib.cpe-parser.version}</version>
203206
</dependency>
204207
<!-- CycloneDX -->
205208
<dependency>
@@ -330,7 +333,6 @@
330333
<dependency>
331334
<groupId>io.github.nscuro</groupId>
332335
<artifactId>versatile</artifactId>
333-
<version>${lib.versatile.version}</version>
334336
</dependency>
335337
<dependency>
336338
<groupId>org.apache.maven</groupId>

apiserver/src/main/java/org/dependencytrack/event/kafka/KafkaEventConverter.java

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import org.dependencytrack.event.ComponentVulnerabilityAnalysisEvent;
2626
import org.dependencytrack.event.EpssMirrorEvent;
2727
import org.dependencytrack.event.GitHubAdvisoryMirrorEvent;
28-
import org.dependencytrack.event.NistMirrorEvent;
2928
import org.dependencytrack.event.OsvMirrorEvent;
3029
import org.dependencytrack.event.kafka.KafkaTopics.Topic;
3130
import org.dependencytrack.model.Vulnerability;
@@ -71,7 +70,6 @@ private KafkaEventConverter() {
7170
case ComponentRepositoryMetaAnalysisEvent e -> convert(e);
7271
case ComponentVulnerabilityAnalysisEvent e -> convert(e);
7372
case GitHubAdvisoryMirrorEvent e -> convert(e);
74-
case NistMirrorEvent e -> convert(e);
7573
case OsvMirrorEvent e -> convert(e);
7674
case EpssMirrorEvent e -> convert(e);
7775
default -> throw new IllegalArgumentException("Unable to convert event " + event);
@@ -153,11 +151,6 @@ static KafkaEvent<String, String> convert(final GitHubAdvisoryMirrorEvent ignore
153151
return new KafkaEvent<>(KafkaTopics.VULNERABILITY_MIRROR_COMMAND, key, null);
154152
}
155153

156-
static KafkaEvent<String, String> convert(final NistMirrorEvent ignored) {
157-
final String key = Vulnerability.Source.NVD.name();
158-
return new KafkaEvent<>(KafkaTopics.VULNERABILITY_MIRROR_COMMAND, key, null);
159-
}
160-
161154
static KafkaEvent<String, String> convert(final OsvMirrorEvent event) {
162155
final String key = Vulnerability.Source.OSV.name();
163156
final String value = event.ecosystem();

apiserver/src/main/java/org/dependencytrack/model/ConfigPropertyConstants.java

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,6 @@ public enum ConfigPropertyConstants {
6767
SCANNER_SNYK_CVSS_SOURCE("scanner", "snyk.cvss.source", "NVD", PropertyType.STRING, "Type of source to be prioritized for cvss calculation", ConfigPropertyAccessMode.READ_WRITE),
6868
SCANNER_SNYK_BASE_URL("scanner", "snyk.base.url", "https://api.snyk.io", PropertyType.URL, "Base Url pointing to the hostname and path for Snyk analysis", ConfigPropertyAccessMode.READ_WRITE),
6969
VULNERABILITY_POLICY_FILE_LAST_MODIFIED_HASH("vulnerability-policy", "vulnerability.policy.file.last.modified.hash", null, PropertyType.STRING, "Hash value or etag of the last fetched bundle if any", ConfigPropertyAccessMode.READ_ONLY),
70-
VULNERABILITY_SOURCE_NVD_ENABLED("vuln-source", "nvd.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable National Vulnerability Database", ConfigPropertyAccessMode.READ_WRITE),
71-
VULNERABILITY_SOURCE_NVD_API_URL("vuln-source", "nvd.api.url", "https://services.nvd.nist.gov/rest/json/cves/2.0", PropertyType.URL, "REST API URL for the NVD's CVE API 2.0", ConfigPropertyAccessMode.READ_WRITE),
72-
VULNERABILITY_SOURCE_NVD_API_KEY("vuln-source", "nvd.api.key", null, PropertyType.ENCRYPTEDSTRING, "API key for the NVD REST API", ConfigPropertyAccessMode.READ_WRITE),
7370
VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED("vuln-source", "github.advisories.enabled", "false", PropertyType.BOOLEAN, "Flag to enable/disable GitHub Advisories", ConfigPropertyAccessMode.READ_WRITE),
7471
VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ALIAS_SYNC_ENABLED("vuln-source", "github.advisories.alias.sync.enabled", "true", PropertyType.BOOLEAN, "Flag to enable/disable alias synchronization for GitHub Advisories", ConfigPropertyAccessMode.READ_WRITE),
7572
VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ACCESS_TOKEN("vuln-source", "github.advisories.access.token", null, PropertyType.STRING, "The access token used for GitHub API authentication", ConfigPropertyAccessMode.READ_WRITE),

apiserver/src/main/java/org/dependencytrack/resources/v2/ExtensionsResource.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ public Response listExtensionConfigs(
157157
case ConfigType.Integer ignored -> ExtensionConfigType.INTEGER;
158158
case ConfigType.Path ignored -> ExtensionConfigType.PATH;
159159
case ConfigType.String ignored -> ExtensionConfigType.STRING;
160+
case ConfigType.URL ignored -> ExtensionConfigType.URL;
160161
})
161162
.isRequired(configDef.isRequired())
162163
.isSecret(configDef.isSecret())

apiserver/src/main/java/org/dependencytrack/tasks/NistMirrorTask.java

Lines changed: 135 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,40 +18,154 @@
1818
*/
1919
package org.dependencytrack.tasks;
2020

21-
import alpine.common.logging.Logger;
2221
import alpine.event.framework.Event;
2322
import alpine.event.framework.LoggableSubscriber;
24-
import alpine.model.ConfigProperty;
23+
import net.javacrumbs.shedlock.core.LockConfiguration;
24+
import net.javacrumbs.shedlock.core.LockExtender;
25+
import net.javacrumbs.shedlock.core.LockingTaskExecutor;
26+
import org.cyclonedx.proto.v1_6.Bom;
2527
import org.dependencytrack.event.NistMirrorEvent;
26-
import org.dependencytrack.event.kafka.KafkaEventDispatcher;
28+
import org.dependencytrack.model.Vulnerability;
29+
import org.dependencytrack.model.VulnerableSoftware;
30+
import org.dependencytrack.parser.dependencytrack.BovModelConverter;
2731
import org.dependencytrack.persistence.QueryManager;
32+
import org.dependencytrack.plugin.PluginManager;
33+
import org.dependencytrack.plugin.api.datasource.vuln.VulnDataSource;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
2836

29-
import static org.dependencytrack.model.ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED;
37+
import java.time.Duration;
38+
import java.time.Instant;
39+
import java.util.ArrayList;
40+
import java.util.Collection;
41+
import java.util.HashMap;
42+
import java.util.List;
3043

44+
import static org.datanucleus.PropertyNames.PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT;
45+
import static org.dependencytrack.util.LockProvider.executeWithLock;
46+
import static org.dependencytrack.util.LockProvider.isTaskLockToBeExtended;
47+
import static org.dependencytrack.util.TaskUtil.getLockConfigForTask;
48+
49+
/**
50+
* Task for mirroring vulnerability data from the national vulnerability database (NVD).
51+
* <p>
52+
* The logic in this task is not NVD-specific and should eventually be used for all
53+
* vulnerability data sources.
54+
*/
3155
public class NistMirrorTask implements LoggableSubscriber {
3256

33-
private static final Logger LOGGER = Logger.getLogger(NistMirrorTask.class);
57+
private static final Logger LOGGER = LoggerFactory.getLogger(NistMirrorTask.class);
58+
59+
private final PluginManager pluginManager;
60+
private LockConfiguration lockConfig;
61+
private Instant lockAcquiredAt;
62+
63+
NistMirrorTask(PluginManager pluginManager) {
64+
this.pluginManager = pluginManager;
65+
}
66+
67+
@SuppressWarnings("unused")
68+
public NistMirrorTask() {
69+
this(PluginManager.getInstance());
70+
}
3471

35-
/**
36-
* {@inheritDoc}
37-
*/
3872
public void inform(final Event e) {
39-
if (e instanceof NistMirrorEvent) {
40-
try (final QueryManager qm = new QueryManager()) {
41-
final ConfigProperty enabled = qm.getConfigProperty(VULNERABILITY_SOURCE_NVD_ENABLED.getGroupName(), VULNERABILITY_SOURCE_NVD_ENABLED.getPropertyName());
42-
final boolean isEnabled = enabled != null && Boolean.valueOf(enabled.getPropertyValue());
43-
if (!isEnabled) {
44-
return;
73+
if (!(e instanceof NistMirrorEvent)) {
74+
return;
75+
}
76+
77+
lockConfig = getLockConfigForTask(getClass());
78+
79+
try {
80+
executeWithLock(
81+
this.lockConfig,
82+
(LockingTaskExecutor.Task) this::informLocked);
83+
} catch (Throwable ex) {
84+
LOGGER.error("Failed to acquire lock or execute task", ex);
85+
}
86+
}
87+
88+
private void informLocked() {
89+
lockAcquiredAt = Instant.now();
90+
91+
try (final var dataSource = pluginManager.getExtension(VulnDataSource.class, "nvd")) {
92+
if (dataSource == null) {
93+
return; // Likely disabled.
94+
}
95+
96+
final var bovBatch = new ArrayList<Bom>(25);
97+
while (dataSource.hasNext()) {
98+
if (Thread.currentThread().isInterrupted()) {
99+
LOGGER.warn("Interrupted before all BOVs could be consumed");
100+
break;
45101
}
46102

47-
final long start = System.currentTimeMillis();
48-
LOGGER.info("Starting NIST mirroring task");
49-
new KafkaEventDispatcher().dispatchEvent(new NistMirrorEvent()).join();
50-
final long end = System.currentTimeMillis();
51-
LOGGER.info("NIST mirroring complete. Time spent (total): " + (end - start) + "ms");
52-
} catch (Exception ex) {
53-
LOGGER.error("An unexpected error occurred while triggering NIST mirroring", ex);
103+
maybeExtendLock();
104+
105+
bovBatch.add(dataSource.next());
106+
if (bovBatch.size() == 25) {
107+
processBatch(dataSource, bovBatch);
108+
bovBatch.clear();
109+
}
110+
}
111+
112+
if (!bovBatch.isEmpty()) {
113+
maybeExtendLock();
114+
processBatch(dataSource, bovBatch);
115+
bovBatch.clear();
54116
}
55117
}
56118
}
119+
120+
private void processBatch(final VulnDataSource dataSource, final Collection<Bom> bovs) {
121+
LOGGER.debug("Processing batch of {} BOVs", bovs.size());
122+
123+
final var vulns = new ArrayList<Vulnerability>(bovs.size());
124+
final var vsListByVulnId = new HashMap<String, List<VulnerableSoftware>>(bovs.size());
125+
126+
for (final Bom bov : bovs) {
127+
if (bov.getVulnerabilitiesCount() == 0) {
128+
LOGGER.warn("BOV contains no vulnerabilities; Skipping");
129+
continue;
130+
}
131+
132+
if (bov.getVulnerabilitiesCount() > 1) {
133+
LOGGER.warn("BOV contains more than one vulnerability; Skipping");
134+
continue;
135+
}
136+
137+
final Vulnerability vuln = BovModelConverter.convert(bov, bov.getVulnerabilities(0), true);
138+
final List<VulnerableSoftware> vsList = BovModelConverter.extractVulnerableSoftware(bov);
139+
140+
vulns.add(vuln);
141+
vsListByVulnId.put(vuln.getVulnId(), vsList);
142+
}
143+
144+
try (final var qm = new QueryManager()) {
145+
qm.getPersistenceManager().setProperty(PROPERTY_PERSISTENCE_BY_REACHABILITY_AT_COMMIT, "false");
146+
147+
qm.runInTransaction(() -> {
148+
for (final Vulnerability vuln : vulns) {
149+
LOGGER.debug("Synchronizing vulnerability {}", vuln.getVulnId());
150+
final Vulnerability persistentVuln = qm.synchronizeVulnerability(vuln, false);
151+
final List<VulnerableSoftware> vsList = vsListByVulnId.get(persistentVuln.getVulnId());
152+
qm.synchronizeVulnerableSoftware(persistentVuln, vsList, Vulnerability.Source.NVD);
153+
}
154+
});
155+
}
156+
157+
for (final Bom bov : bovs) {
158+
dataSource.markProcessed(bov);
159+
}
160+
}
161+
162+
private void maybeExtendLock() {
163+
final var lockAge = Duration.between(lockAcquiredAt, Instant.now());
164+
if (isTaskLockToBeExtended(lockAge.toMillis(), getClass())) {
165+
LOGGER.warn("Extending lock by {}", lockConfig.getLockAtMostFor());
166+
LockExtender.extendActiveLock(lockConfig.getLockAtMostFor(), this.lockConfig.getLockAtLeastFor());
167+
lockAcquiredAt = Instant.now();
168+
}
169+
}
170+
57171
}

apiserver/src/main/java/org/dependencytrack/util/VulnerabilityUtil.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
import org.dependencytrack.model.Vulnerability;
2727
import org.dependencytrack.model.VulnerabilityAlias;
2828
import org.dependencytrack.persistence.QueryManager;
29+
import org.dependencytrack.plugin.NoSuchExtensionException;
30+
import org.dependencytrack.plugin.PluginManager;
31+
import org.dependencytrack.plugin.api.datasource.vuln.VulnDataSource;
2932

3033
import java.math.BigDecimal;
3134
import java.security.SecureRandom;
@@ -276,9 +279,15 @@ public static boolean canBeMirrored(final Vulnerability vulnerability) {
276279
*/
277280
public static boolean isMirroringEnabled(final Vulnerability vulnerability) {
278281
final Vulnerability.Source source = Vulnerability.Source.valueOf(vulnerability.getSource());
282+
if (Vulnerability.Source.NVD == source) {
283+
try (final var dataSource = PluginManager.getInstance().getExtension(VulnDataSource.class, "nvd")) {
284+
return dataSource != null;
285+
} catch (NoSuchExtensionException e) {
286+
return false;
287+
}
288+
}
279289

280290
final ConfigPropertyConstants toggleConfigPropertyConstant = switch (source) {
281-
case NVD -> ConfigPropertyConstants.VULNERABILITY_SOURCE_NVD_ENABLED;
282291
case GITHUB -> ConfigPropertyConstants.VULNERABILITY_SOURCE_GITHUB_ADVISORIES_ENABLED;
283292
case OSV -> ConfigPropertyConstants.VULNERABILITY_SOURCE_GOOGLE_OSV_ENABLED;
284293
default -> null;

apiserver/src/main/resources/application.properties

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,24 @@ task.osv.mirror.cron=0 3 * * *
10501050
# @required
10511051
task.nist.mirror.cron=0 4 * * *
10521052

1053+
# Maximum duration in ISO 8601 format for which the NIST mirror task will hold a lock.
1054+
# <br/><br/>
1055+
# The duration should be long enough to cover the task's execution duration.
1056+
#
1057+
# @category: Task Scheduling
1058+
# @type: duration
1059+
# @required
1060+
task.nist.mirror.lock.max.duration=PT15M
1061+
1062+
# Minimum duration in ISO 8601 format for which the NIST mirror task will hold a lock.
1063+
# <br/><br/>
1064+
# The duration should be long enough to cover eventual clock skew across API server instances.
1065+
#
1066+
# @category: Task Scheduling
1067+
# @type: duration
1068+
# @required
1069+
task.nist.mirror.lock.min.duration=PT1M
1070+
10531071
# Cron expression of the EPSS mirroring task.
10541072
#
10551073
# @category: Task Scheduling

0 commit comments

Comments
 (0)