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