View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.data.update;
19  
20  import java.net.MalformedURLException;
21  import java.util.Calendar;
22  import java.util.HashSet;
23  import java.util.Set;
24  import java.util.concurrent.ExecutionException;
25  import java.util.concurrent.ExecutorService;
26  import java.util.concurrent.Executors;
27  import java.util.concurrent.Future;
28  import org.owasp.dependencycheck.data.nvdcve.CveDB;
29  import org.owasp.dependencycheck.data.nvdcve.DatabaseException;
30  import org.owasp.dependencycheck.data.nvdcve.DatabaseProperties;
31  import static org.owasp.dependencycheck.data.nvdcve.DatabaseProperties.MODIFIED;
32  import org.owasp.dependencycheck.data.update.exception.InvalidDataException;
33  import org.owasp.dependencycheck.data.update.exception.UpdateException;
34  import org.owasp.dependencycheck.data.update.nvd.DownloadTask;
35  import org.owasp.dependencycheck.data.update.nvd.NvdCveInfo;
36  import org.owasp.dependencycheck.data.update.nvd.ProcessTask;
37  import org.owasp.dependencycheck.data.update.nvd.UpdateableNvdCve;
38  import org.owasp.dependencycheck.exception.NoDataException;
39  import org.owasp.dependencycheck.utils.DateUtil;
40  import org.owasp.dependencycheck.utils.DownloadFailedException;
41  import org.owasp.dependencycheck.utils.InvalidSettingException;
42  import org.owasp.dependencycheck.utils.Settings;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  /**
47   * Class responsible for updating the NVD CVE data.
48   *
49   * @author Jeremy Long
50   */
51  public class NvdCveUpdater extends BaseUpdater implements CachedWebDataSource {
52  
53      /**
54       * The logger
55       */
56      private static final Logger LOGGER = LoggerFactory.getLogger(NvdCveUpdater.class);
57      /**
58       * The max thread pool size to use when downloading files.
59       */
60      public static final int MAX_THREAD_POOL_SIZE = Settings.getInt(Settings.KEYS.MAX_DOWNLOAD_THREAD_POOL_SIZE, 3);
61  
62      /**
63       * <p>
64       * Downloads the latest NVD CVE XML file from the web and imports it into the current CVE Database.</p>
65       *
66       * @throws UpdateException is thrown if there is an error updating the database
67       */
68      @Override
69      public void update() throws UpdateException {
70          try {
71              openDataStores();
72              if (checkUpdate()) {
73                  final UpdateableNvdCve updateable = getUpdatesNeeded();
74                  if (updateable.isUpdateNeeded()) {
75                      performUpdate(updateable);
76                  }
77              }
78          } catch (MalformedURLException ex) {
79              LOGGER.warn(
80                      "NVD CVE properties files contain an invalid URL, unable to update the data to use the most current data.");
81              LOGGER.debug("", ex);
82          } catch (DownloadFailedException ex) {
83              LOGGER.warn(
84                      "Unable to download the NVD CVE data; the results may not include the most recent CPE/CVEs from the NVD.");
85              if (Settings.getString(Settings.KEYS.PROXY_SERVER) == null) {
86                  LOGGER.info(
87                          "If you are behind a proxy you may need to configure dependency-check to use the proxy.");
88              }
89              LOGGER.debug("", ex);
90          } finally {
91              closeDataStores();
92          }
93      }
94  
95      /**
96       * Checks if the NVD CVE XML files were last checked recently. As an optimization, we can avoid repetitive checks against the
97       * NVD. Setting CVE_CHECK_VALID_FOR_HOURS determines the duration since last check before checking again. A database property
98       * stores the timestamp of the last check.
99       *
100      * @return true to proceed with the check, or false to skip.
101      * @throws UpdateException thrown when there is an issue checking for updates.
102      */
103     private boolean checkUpdate() throws UpdateException {
104         boolean proceed = true;
105         // If the valid setting has not been specified, then we proceed to check...
106         final int validForHours = Settings.getInt(Settings.KEYS.CVE_CHECK_VALID_FOR_HOURS, 0);
107         if (dataExists() && 0 < validForHours) {
108             // ms Valid = valid (hours) x 60 min/hour x 60 sec/min x 1000 ms/sec
109             final long msValid = validForHours * 60L * 60L * 1000L;
110             final long lastChecked = Long.parseLong(getProperties().getProperty(DatabaseProperties.LAST_CHECKED, "0"));
111             final long now = System.currentTimeMillis();
112             proceed = (now - lastChecked) > msValid;
113             if (proceed) {
114                 getProperties().save(DatabaseProperties.LAST_CHECKED, Long.toString(now));
115             } else {
116                 LOGGER.info("Skipping NVD check since last check was within {} hours.", validForHours);
117                 LOGGER.debug("Last NVD was at {}, and now {} is within {} ms.",
118                         lastChecked, now, msValid);
119             }
120         }
121         return proceed;
122     }
123 
124     /**
125      * Checks the CPE Index to ensure documents exists.
126      */
127     private boolean dataExists() {
128         CveDB cve = null;
129         try {
130             cve = new CveDB();
131             cve.open();
132             return cve.dataExists();
133         } catch (DatabaseException ex) {
134             return false;
135         } finally {
136             if (cve != null) {
137                 cve.close();
138             }
139         }
140     }
141 
142     /**
143      * Downloads the latest NVD CVE XML file from the web and imports it into the current CVE Database.
144      *
145      * @param updateable a collection of NVD CVE data file references that need to be downloaded and processed to update the
146      * database
147      * @throws UpdateException is thrown if there is an error updating the database
148      */
149     public void performUpdate(UpdateableNvdCve updateable) throws UpdateException {
150         int maxUpdates = 0;
151         try {
152             for (NvdCveInfo cve : updateable) {
153                 if (cve.getNeedsUpdate()) {
154                     maxUpdates += 1;
155                 }
156             }
157             if (maxUpdates <= 0) {
158                 return;
159             }
160             if (maxUpdates > 3) {
161                 LOGGER.info(
162                         "NVD CVE requires several updates; this could take a couple of minutes.");
163             }
164             if (maxUpdates > 0) {
165                 openDataStores();
166             }
167 
168             final int poolSize = (MAX_THREAD_POOL_SIZE < maxUpdates) ? MAX_THREAD_POOL_SIZE : maxUpdates;
169 
170             final ExecutorService downloadExecutors = Executors.newFixedThreadPool(poolSize);
171             final ExecutorService processExecutor = Executors.newSingleThreadExecutor();
172             final Set<Future<Future<ProcessTask>>> downloadFutures = new HashSet<Future<Future<ProcessTask>>>(maxUpdates);
173             for (NvdCveInfo cve : updateable) {
174                 if (cve.getNeedsUpdate()) {
175                     final DownloadTask call = new DownloadTask(cve, processExecutor, getCveDB(), Settings.getInstance());
176                     downloadFutures.add(downloadExecutors.submit(call));
177                 }
178             }
179             downloadExecutors.shutdown();
180 
181             //next, move the future future processTasks to just future processTasks
182             final Set<Future<ProcessTask>> processFutures = new HashSet<Future<ProcessTask>>(maxUpdates);
183             for (Future<Future<ProcessTask>> future : downloadFutures) {
184                 Future<ProcessTask> task = null;
185                 try {
186                     task = future.get();
187                 } catch (InterruptedException ex) {
188                     downloadExecutors.shutdownNow();
189                     processExecutor.shutdownNow();
190 
191                     LOGGER.debug("Thread was interrupted during download", ex);
192                     throw new UpdateException("The download was interrupted", ex);
193                 } catch (ExecutionException ex) {
194                     downloadExecutors.shutdownNow();
195                     processExecutor.shutdownNow();
196 
197                     LOGGER.debug("Thread was interrupted during download execution", ex);
198                     throw new UpdateException("The execution of the download was interrupted", ex);
199                 }
200                 if (task == null) {
201                     downloadExecutors.shutdownNow();
202                     processExecutor.shutdownNow();
203                     LOGGER.debug("Thread was interrupted during download");
204                     throw new UpdateException("The download was interrupted; unable to complete the update");
205                 } else {
206                     processFutures.add(task);
207                 }
208             }
209 
210             for (Future<ProcessTask> future : processFutures) {
211                 try {
212                     final ProcessTask task = future.get();
213                     if (task.getException() != null) {
214                         throw task.getException();
215                     }
216                 } catch (InterruptedException ex) {
217                     processExecutor.shutdownNow();
218                     LOGGER.debug("Thread was interrupted during processing", ex);
219                     throw new UpdateException(ex);
220                 } catch (ExecutionException ex) {
221                     processExecutor.shutdownNow();
222                     LOGGER.debug("Execution Exception during process", ex);
223                     throw new UpdateException(ex);
224                 } finally {
225                     processExecutor.shutdown();
226                 }
227             }
228 
229             if (maxUpdates >= 1) { //ensure the modified file date gets written (we may not have actually updated it)
230                 getProperties().save(updateable.get(MODIFIED));
231                 LOGGER.info("Begin database maintenance.");
232                 getCveDB().cleanupDatabase();
233                 LOGGER.info("End database maintenance.");
234             }
235         } finally {
236             closeDataStores();
237         }
238     }
239 
240     /**
241      * Determines if the index needs to be updated. This is done by fetching the NVD CVE meta data and checking the last update
242      * date. If the data needs to be refreshed this method will return the NvdCveUrl for the files that need to be updated.
243      *
244      * @return the collection of files that need to be updated
245      * @throws MalformedURLException is thrown if the URL for the NVD CVE Meta data is incorrect
246      * @throws DownloadFailedException is thrown if there is an error. downloading the NVD CVE download data file
247      * @throws UpdateException Is thrown if there is an issue with the last updated properties file
248      */
249     protected final UpdateableNvdCve getUpdatesNeeded() throws MalformedURLException, DownloadFailedException, UpdateException {
250         UpdateableNvdCve updates = null;
251         try {
252             updates = retrieveCurrentTimestampsFromWeb();
253         } catch (InvalidDataException ex) {
254             final String msg = "Unable to retrieve valid timestamp from nvd cve downloads page";
255             LOGGER.debug(msg, ex);
256             throw new DownloadFailedException(msg, ex);
257         } catch (InvalidSettingException ex) {
258             LOGGER.debug("Invalid setting found when retrieving timestamps", ex);
259             throw new DownloadFailedException("Invalid settings", ex);
260         }
261 
262         if (updates == null) {
263             throw new DownloadFailedException("Unable to retrieve the timestamps of the currently published NVD CVE data");
264         }
265         if (!getProperties().isEmpty()) {
266             try {
267                 final long lastUpdated = Long.parseLong(getProperties().getProperty(DatabaseProperties.LAST_UPDATED, "0"));
268                 final long now = System.currentTimeMillis();
269                 final int days = Settings.getInt(Settings.KEYS.CVE_MODIFIED_VALID_FOR_DAYS, 7);
270                 if (lastUpdated == updates.getTimeStamp(MODIFIED)) {
271                     updates.clear(); //we don't need to update anything.
272                 } else if (DateUtil.withinDateRange(lastUpdated, now, days)) {
273                     for (NvdCveInfo entry : updates) {
274                         if (MODIFIED.equals(entry.getId())) {
275                             entry.setNeedsUpdate(true);
276                         } else {
277                             entry.setNeedsUpdate(false);
278                         }
279                     }
280                 } else { //we figure out which of the several XML files need to be downloaded.
281                     for (NvdCveInfo entry : updates) {
282                         if (MODIFIED.equals(entry.getId())) {
283                             entry.setNeedsUpdate(true);
284                         } else {
285                             long currentTimestamp = 0;
286                             try {
287                                 currentTimestamp = Long.parseLong(getProperties().getProperty(DatabaseProperties.LAST_UPDATED_BASE
288                                         + entry.getId(), "0"));
289                             } catch (NumberFormatException ex) {
290                                 LOGGER.debug("Error parsing '{}' '{}' from nvdcve.lastupdated",
291                                         DatabaseProperties.LAST_UPDATED_BASE, entry.getId(), ex);
292                             }
293                             if (currentTimestamp == entry.getTimestamp()) {
294                                 entry.setNeedsUpdate(false);
295                             }
296                         }
297                     }
298                 }
299             } catch (NumberFormatException ex) {
300                 LOGGER.warn("An invalid schema version or timestamp exists in the data.properties file.");
301                 LOGGER.debug("", ex);
302             }
303         }
304         return updates;
305     }
306 
307     /**
308      * Retrieves the timestamps from the NVD CVE meta data file.
309      *
310      * @return the timestamp from the currently published nvdcve downloads page
311      * @throws MalformedURLException thrown if the URL for the NVD CCE Meta data is incorrect.
312      * @throws DownloadFailedException thrown if there is an error downloading the nvd cve meta data file
313      * @throws InvalidDataException thrown if there is an exception parsing the timestamps
314      * @throws InvalidSettingException thrown if the settings are invalid
315      */
316     private UpdateableNvdCve retrieveCurrentTimestampsFromWeb()
317             throws MalformedURLException, DownloadFailedException, InvalidDataException, InvalidSettingException {
318 
319         final UpdateableNvdCve updates = new UpdateableNvdCve();
320         updates.add(MODIFIED, Settings.getString(Settings.KEYS.CVE_MODIFIED_20_URL),
321                 Settings.getString(Settings.KEYS.CVE_MODIFIED_12_URL),
322                 false);
323 
324         final int start = Settings.getInt(Settings.KEYS.CVE_START_YEAR);
325         final int end = Calendar.getInstance().get(Calendar.YEAR);
326         final String baseUrl20 = Settings.getString(Settings.KEYS.CVE_SCHEMA_2_0);
327         final String baseUrl12 = Settings.getString(Settings.KEYS.CVE_SCHEMA_1_2);
328         for (int i = start; i <= end; i++) {
329             updates.add(Integer.toString(i), String.format(baseUrl20, i),
330                     String.format(baseUrl12, i),
331                     true);
332         }
333         return updates;
334     }
335 
336 }