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