2727import java .io .FileOutputStream ;
2828import java .io .IOException ;
2929import java .io .StringReader ;
30+ import java .net .MalformedURLException ;
3031import java .net .URI ;
3132import java .net .URISyntaxException ;
3233import java .net .URL ;
33- import java .nio .charset .StandardCharsets ;
3434import java .text .MessageFormat ;
3535import java .time .Duration ;
3636import java .time .ZoneId ;
4141import java .util .HashSet ;
4242import java .util .List ;
4343import java .util .Map ;
44+ import java .util .Optional ;
4445import java .util .Properties ;
4546import java .util .Set ;
4647import java .util .concurrent .ExecutionException ;
4748import java .util .concurrent .ExecutorService ;
4849import java .util .concurrent .Executors ;
4950import java .util .concurrent .Future ;
51+ import java .util .function .Function ;
5052import java .util .zip .GZIPOutputStream ;
53+
54+ import org .jetbrains .annotations .NotNull ;
5155import org .owasp .dependencycheck .Engine ;
5256import org .owasp .dependencycheck .data .nvdcve .CveDB ;
5357import org .owasp .dependencycheck .data .nvdcve .DatabaseException ;
6569import org .slf4j .Logger ;
6670import 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