Skip to content

Commit 98066f7

Browse files
committed
fix: Fix exception handling for NVD data feed downloads to log correct error
Also refactors the codebase to avoid such issues and simplify the URL handling Signed-off-by: Chad Wilson <29788154+chadlwilson@users.noreply.github.com>
1 parent 713a7a6 commit 98066f7

2 files changed

Lines changed: 188 additions & 136 deletions

File tree

core/src/main/java/org/owasp/dependencycheck/data/update/NvdApiDataSource.java

Lines changed: 126 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@
2727
import java.io.FileOutputStream;
2828
import java.io.IOException;
2929
import java.io.StringReader;
30+
import java.net.MalformedURLException;
3031
import java.net.URI;
3132
import java.net.URISyntaxException;
3233
import java.net.URL;
33-
import java.nio.charset.StandardCharsets;
3434
import java.text.MessageFormat;
3535
import java.time.Duration;
3636
import java.time.ZoneId;
@@ -41,13 +41,17 @@
4141
import java.util.HashSet;
4242
import java.util.List;
4343
import java.util.Map;
44+
import java.util.Optional;
4445
import java.util.Properties;
4546
import java.util.Set;
4647
import java.util.concurrent.ExecutionException;
4748
import java.util.concurrent.ExecutorService;
4849
import java.util.concurrent.Executors;
4950
import java.util.concurrent.Future;
51+
import java.util.function.Function;
5052
import java.util.zip.GZIPOutputStream;
53+
54+
import org.jetbrains.annotations.NotNull;
5155
import org.owasp.dependencycheck.Engine;
5256
import org.owasp.dependencycheck.data.nvdcve.CveDB;
5357
import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
@@ -65,6 +69,8 @@
6569
import org.slf4j.Logger;
6670
import org.slf4j.LoggerFactory;
6771

72+
import static java.nio.charset.StandardCharsets.UTF_8;
73+
6874
/**
6975
*
7076
* @author Jeremy Long
@@ -117,44 +123,22 @@ public boolean update(Engine engine) throws UpdateException {
117123
return processApi();
118124
}
119125

120-
protected UrlData extractUrlData(String nvdDataFeedUrl) {
121-
String url;
122-
String pattern = null;
123-
if (nvdDataFeedUrl.endsWith(".json.gz")) {
124-
final int lio = nvdDataFeedUrl.lastIndexOf("/");
125-
pattern = nvdDataFeedUrl.substring(lio + 1);
126-
url = nvdDataFeedUrl.substring(0, lio);
127-
} else {
128-
url = nvdDataFeedUrl;
129-
}
130-
if (!url.endsWith("/")) {
131-
url += "/";
132-
}
133-
return new UrlData(url, pattern);
134-
}
135-
136126
private boolean processDatafeed(String nvdDataFeedUrl) throws UpdateException {
137127
boolean updatesMade = false;
138128
try {
139129
dbProperties = cveDb.getDatabaseProperties();
140130
if (checkUpdate()) {
141-
final UrlData data = extractUrlData(nvdDataFeedUrl);
142-
final String url = data.getUrl();
143-
String pattern = data.getPattern();
144-
final Properties cacheProperties = getRemoteCacheProperties(url, pattern);
145-
if (pattern == null) {
146-
final String prefix = cacheProperties.getProperty("prefix", "nvdcve-");
147-
pattern = prefix + "{0}.json.gz";
148-
}
131+
FeedUrl urlData = FeedUrl.extractFromUrlOptionalPattern(nvdDataFeedUrl);
132+
final Properties cacheProperties = getRemoteDataFeedCacheProperties(urlData);
133+
urlData = urlData.withPattern(p -> p.orElse(cacheProperties.getProperty("prefix", FeedUrl.DEFAULT_FILE_PATTERN_PREFIX) + FeedUrl.DEFAULT_FILE_PATTERN_SUFFIX));
149134

150135
final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
151-
final Map<String, String> updateable = getUpdatesNeeded(url, pattern, cacheProperties, now);
136+
final Map<String, String> updateable = getUpdatesNeeded(urlData, cacheProperties, now);
152137
if (!updateable.isEmpty()) {
153138
final int max = settings.getInt(Settings.KEYS.MAX_DOWNLOAD_THREAD_POOL_SIZE, 1);
154139
final int downloadPoolSize = Math.min(Runtime.getRuntime().availableProcessors(), max);
155140
// going over 2 threads does not appear to improve performance
156-
final int maxExec = PROCESSING_THREAD_POOL_SIZE;
157-
final int execPoolSize = Math.min(maxExec, 2);
141+
final int execPoolSize = Math.min(PROCESSING_THREAD_POOL_SIZE, 2);
158142

159143
ExecutorService processingExecutorService = null;
160144
ExecutorService downloadExecutorService = null;
@@ -530,17 +514,14 @@ private boolean dataExists() {
530514
* be refreshed this method will return the NvdCveUrl for the files that
531515
* need to be updated.
532516
*
533-
* @param url the URL of the NVD API cache
534-
* @param filePattern the string format pattern for the cached files (e.g.
535-
* "nvdcve-{0}.json.gz")
536-
* @param cacheProperties the properties from the remote NVD API cache
517+
* @param feedUrl a parsed NVD cache / data feed URL
518+
* @param cacheProperties the properties from the remote NVD API cache or data feed
537519
* @param now the start time of the update process
538520
* @return the map of key to URLs - where the key is the year or `modified`
539521
* @throws UpdateException Is thrown if there is an issue with the last
540522
* updated properties file
541523
*/
542-
protected final Map<String, String> getUpdatesNeeded(String url, String filePattern,
543-
Properties cacheProperties, ZonedDateTime now) throws UpdateException {
524+
protected final Map<String, String> getUpdatesNeeded(FeedUrl feedUrl, Properties cacheProperties, ZonedDateTime now) throws UpdateException {
544525
LOGGER.debug("starting getUpdatesNeeded() ...");
545526
final Map<String, String> updates = new HashMap<>();
546527
if (dbProperties != null && !dbProperties.isEmpty()) {
@@ -563,11 +544,11 @@ protected final Map<String, String> getUpdatesNeeded(String url, String filePatt
563544
if (!needsFullUpdate && lastUpdated.equals(DatabaseProperties.getTimestamp(cacheProperties, NVD_API_CACHE_MODIFIED_DATE))) {
564545
return updates;
565546
} else {
566-
updates.put("modified", url + MessageFormat.format(filePattern, "modified"));
547+
updates.put("modified", feedUrl.toFormattedUrlString("modified"));
567548
if (needsFullUpdate) {
568549
for (int i = startYear; i <= endYear; i++) {
569550
if (cacheProperties.containsKey(NVD_API_CACHE_MODIFIED_DATE + "." + i)) {
570-
updates.put(String.valueOf(i), url + MessageFormat.format(filePattern, String.valueOf(i)));
551+
updates.put(String.valueOf(i), feedUrl.toFormattedUrlString(i));
571552
}
572553
}
573554
} else if (!DateUtil.withinDateRange(lastUpdated, now, days)) {
@@ -576,79 +557,100 @@ protected final Map<String, String> getUpdatesNeeded(String url, String filePatt
576557
final ZonedDateTime lastModifiedCache = DatabaseProperties.getTimestamp(cacheProperties,
577558
NVD_API_CACHE_MODIFIED_DATE + "." + i);
578559
final ZonedDateTime lastModifiedDB = dbProperties.getTimestamp(DatabaseProperties.NVD_CACHE_LAST_MODIFIED + "." + i);
579-
if (lastModifiedDB == null || lastModifiedCache.compareTo(lastModifiedDB) > 0) {
580-
updates.put(String.valueOf(i), url + MessageFormat.format(filePattern, String.valueOf(i)));
560+
if (lastModifiedDB == null || (lastModifiedCache != null && lastModifiedCache.compareTo(lastModifiedDB) > 0)) {
561+
updates.put(String.valueOf(i), feedUrl.toFormattedUrlString(i));
581562
}
582563
}
583564
}
584565
}
585566
}
586567
}
587568
if (updates.size() > 3) {
588-
LOGGER.info("NVD API Cache requires several updates; this could take a couple of minutes.");
569+
LOGGER.info("NVD API Cache / Data Feed requires several updates; this could take a couple of minutes.");
589570
}
590571
return updates;
591572
}
592573

593574
/**
594-
* Downloads the metadata properties of the NVD API cache.
575+
* Downloads the metadata properties of the NVD API cache / data feed.
595576
*
596-
* @param url the base URL to the NVD API cache
597-
* @param pattern the pattern of the datafile name for the NVD API cache
577+
* @param dataFeedUrl a parsed NVD cache / data feed URL
598578
* @return the cache properties
599-
* @throws UpdateException thrown if the properties file could not be
600-
* downloaded
579+
* @throws UpdateException thrown if the properties file could not be downloaded
601580
*/
602-
protected final Properties getRemoteCacheProperties(String url, String pattern) throws UpdateException {
603-
final Properties properties = new Properties();
581+
protected final Properties getRemoteDataFeedCacheProperties(FeedUrl dataFeedUrl) throws UpdateException {
604582
try {
605-
final URL u = new URI(url + "cache.properties").toURL();
606-
final String content = Downloader.getInstance().fetchContent(u, StandardCharsets.UTF_8);
583+
final Properties properties = new Properties();
584+
final String content = Downloader.getInstance().fetchContent(dataFeedUrl.toSuffixedUrl("cache.properties"), UTF_8);
607585
properties.load(new StringReader(content));
586+
return properties;
608587

609-
} catch (URISyntaxException ex) {
610-
throw new UpdateException("Invalid NVD Cache URL", ex);
611588
} catch (DownloadFailedException | ResourceNotFoundException ex) {
612-
final String metaPattern;
613-
if (pattern == null) {
614-
metaPattern = "nvdcve-{0}.meta";
615-
} else {
616-
metaPattern = pattern.replace(".json.gz", ".meta");
617-
}
618-
try {
619-
URL metaUrl = new URI(url + MessageFormat.format(metaPattern, "modified")).toURL();
620-
String content = Downloader.getInstance().fetchContent(metaUrl, StandardCharsets.UTF_8);
621-
final Properties props = new Properties();
622-
props.load(new StringReader(content));
623-
ZonedDateTime lmd = DatabaseProperties.getIsoTimestamp(props, "lastModifiedDate");
624-
DatabaseProperties.setTimestamp(properties, "lastModifiedDate.modified", lmd);
625-
DatabaseProperties.setTimestamp(properties, "lastModifiedDate", lmd);
626-
final int startYear = settings.getInt(Settings.KEYS.NVD_API_DATAFEED_START_YEAR, 2002);
627-
final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
628-
final int endYear = now.withZoneSameInstant(ZoneId.of("UTC+14:00")).getYear();
629-
for (int y = startYear; y <= endYear; y++) {
630-
metaUrl = new URI(url + MessageFormat.format(metaPattern, String.valueOf(y))).toURL();
631-
content = Downloader.getInstance().fetchContent(metaUrl, StandardCharsets.UTF_8);
632-
props.clear();
633-
props.load(new StringReader(content));
634-
lmd = DatabaseProperties.getIsoTimestamp(props, "lastModifiedDate");
635-
DatabaseProperties.setTimestamp(properties, "lastModifiedDate." + String.valueOf(y), lmd);
636-
}
637-
} catch (URISyntaxException | TooManyRequestsException | ResourceNotFoundException | IOException ex1) {
638-
throw new UpdateException("Unable to download the data feed META files", ex);
639-
}
589+
LOGGER.debug("Unable to download the NVD API cache.properties due to [{}]; attempting to build from data feed metadata files instead...", ex.toString());
590+
return generateRemoteDataFeedCachePropertiesFromMetadata(dataFeedUrl);
591+
} catch (URISyntaxException | MalformedURLException ex) {
592+
throw new UpdateException("Invalid NVD Cache / Data Feed URL", ex);
640593
} catch (TooManyRequestsException ex) {
641594
throw new UpdateException("Unable to download the NVD API cache.properties", ex);
642595
} catch (IOException ex) {
643-
throw new UpdateException("Invalid NVD Cache Properties file contents", ex);
596+
throw new UpdateException("Invalid NVD Cache properties file contents", ex);
644597
}
645-
return properties;
646598
}
647599

648-
protected static class UrlData {
600+
/**
601+
* Builds the metadata properties from individual metadata fields within the data feed
602+
*
603+
* @param dataFeedUrl a parsed NVD cache / data feed URL
604+
* @return the cache properties
605+
* @throws UpdateException thrown if the metadata files could not be downloaded to build cache properties
606+
*/
607+
private Properties generateRemoteDataFeedCachePropertiesFromMetadata(FeedUrl dataFeedUrl) throws UpdateException {
608+
FeedUrl metaFeedUrl = dataFeedUrl.withPattern(p -> p
609+
.orElse(FeedUrl.DEFAULT_FILE_PATTERN)
610+
.replace(".json.gz", ".meta")
611+
);
612+
613+
final Properties properties = new Properties();
614+
try {
615+
String content = Downloader.getInstance().fetchContent(metaFeedUrl.toFormattedUrl("modified"), UTF_8);
616+
final Properties props = new Properties();
617+
props.load(new StringReader(content));
618+
ZonedDateTime lmd = DatabaseProperties.getIsoTimestamp(props, "lastModifiedDate");
619+
DatabaseProperties.setTimestamp(properties, "lastModifiedDate.modified", lmd);
620+
DatabaseProperties.setTimestamp(properties, "lastModifiedDate", lmd);
621+
final int startYear = settings.getInt(Settings.KEYS.NVD_API_DATAFEED_START_YEAR, 2002);
622+
final ZonedDateTime now = ZonedDateTime.now(ZoneId.of("UTC"));
623+
final int endYear = now.withZoneSameInstant(ZoneId.of("UTC+14:00")).getYear();
624+
for (int y = startYear; y <= endYear; y++) {
625+
content = Downloader.getInstance().fetchContent(metaFeedUrl.toFormattedUrl(y), UTF_8);
626+
props.clear();
627+
props.load(new StringReader(content));
628+
lmd = DatabaseProperties.getIsoTimestamp(props, "lastModifiedDate");
629+
DatabaseProperties.setTimestamp(properties, "lastModifiedDate." + y, lmd);
630+
}
631+
return properties;
632+
} catch (URISyntaxException | TooManyRequestsException | ResourceNotFoundException | IOException ex) {
633+
throw new UpdateException("Unable to download the data feed META files", ex);
634+
}
635+
}
636+
637+
protected static class FeedUrl {
638+
639+
/**
640+
* Default file pattern prefix for NVD caches; generally those generated by vulnz / Open Vulnerability Clients
641+
*/
642+
static final String DEFAULT_FILE_PATTERN_PREFIX = "nvdcve-";
643+
/**
644+
* Default file pattern suffix for NVD caches; generally those generated by vulnz / Open Vulnerability Clients
645+
*/
646+
static final String DEFAULT_FILE_PATTERN_SUFFIX = "{0}.json.gz";
647+
/**
648+
* Default file pattern for NVD caches; generally those generated by vulnz / Open Vulnerability Clients
649+
*/
650+
static final String DEFAULT_FILE_PATTERN = DEFAULT_FILE_PATTERN_PREFIX + DEFAULT_FILE_PATTERN_SUFFIX;
649651

650652
/**
651-
* The URL to download resources from.
653+
* The base URL to download resources from.
652654
*/
653655
private final String url;
654656

@@ -657,28 +659,56 @@ protected static class UrlData {
657659
*/
658660
private final String pattern;
659661

660-
public UrlData(String url, String pattern) {
662+
public FeedUrl(String url, String pattern) {
661663
this.url = url;
662664
this.pattern = pattern;
663665
}
664666

665-
/**
666-
* Get the value of pattern
667-
*
668-
* @return the value of pattern
669-
*/
670-
public String getPattern() {
671-
return pattern;
667+
public FeedUrl withPattern(Function<Optional<String>, String> patternTransformer) {
668+
return new FeedUrl(url, patternTransformer.apply(Optional.ofNullable(pattern)));
669+
}
670+
671+
@NotNull String toFormattedUrlString(String formatArg) {
672+
return url + MessageFormat.format(Optional.ofNullable(pattern).orElseThrow(), formatArg);
673+
}
674+
675+
@NotNull String toFormattedUrlString(int formatArg) {
676+
return toFormattedUrlString(String.valueOf(formatArg));
677+
}
678+
679+
@NotNull URL toFormattedUrl(@NotNull String formatArg) throws MalformedURLException, URISyntaxException {
680+
return new URI(toFormattedUrlString(formatArg)).toURL();
681+
}
682+
683+
@NotNull URL toFormattedUrl(int formatArg) throws MalformedURLException, URISyntaxException {
684+
return toFormattedUrl(String.valueOf(formatArg));
685+
}
686+
687+
@SuppressWarnings("SameParameterValue")
688+
@NotNull URL toSuffixedUrl(String suffix) throws MalformedURLException, URISyntaxException {
689+
return new URI(url + suffix).toURL();
672690
}
673691

674692
/**
675-
* Get the value of url
676-
*
677-
* @return the value of url
693+
* @param url A NVD data feed URL which may be just a base URL such as https://my-nvd-cache/nvd_cache or
694+
* may include a formatted URL ending with .json.gz such as https://nvd.nist.gov/feeds/json/cve/2.0/nvdcve-2.0-{0}.json.gz
695+
* @return A constructed URLData object
678696
*/
679-
public String getUrl() {
680-
return url;
697+
@SuppressWarnings("JavadocLinkAsPlainText")
698+
protected static FeedUrl extractFromUrlOptionalPattern(String url) {
699+
String baseUrl;
700+
String pattern = null;
701+
if (url.endsWith(".json.gz")) {
702+
final int lio = url.lastIndexOf("/");
703+
pattern = url.substring(lio + 1);
704+
baseUrl = url.substring(0, lio);
705+
} else {
706+
baseUrl = url;
707+
}
708+
if (!baseUrl.endsWith("/")) {
709+
baseUrl += "/";
710+
}
711+
return new FeedUrl(baseUrl, pattern);
681712
}
682-
683713
}
684714
}

0 commit comments

Comments
 (0)