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       * <p>
63       * Downloads the latest NVD CVE XML file from the web and imports it into
64       * the current CVE Database.</p>
65       *
66       * @throws UpdateException is thrown if there is an error updating the
67       * database
68       */
69      @Override
70      public void update() throws UpdateException {
71          try {
72              openDataStores();
73              boolean autoUpdate = true;
74              try {
75                  autoUpdate = Settings.getBoolean(Settings.KEYS.AUTO_UPDATE);
76              } catch (InvalidSettingException ex) {
77                  LOGGER.debug("Invalid setting for auto-update; using true.");
78              }
79              if (autoUpdate && checkUpdate()) {
80                  final UpdateableNvdCve updateable = getUpdatesNeeded();
81                  getProperties().save(DatabaseProperties.LAST_CHECKED, Long.toString(System.currentTimeMillis()));
82                  if (updateable.isUpdateNeeded()) {
83                      performUpdate(updateable);
84                  }
85              }
86          } catch (MalformedURLException ex) {
87              LOGGER.warn(
88                      "NVD CVE properties files contain an invalid URL, unable to update the data to use the most current data.");
89              LOGGER.debug("", ex);
90          } catch (DownloadFailedException ex) {
91              LOGGER.warn(
92                      "Unable to download the NVD CVE data; the results may not include the most recent CPE/CVEs from the NVD.");
93              if (Settings.getString(Settings.KEYS.PROXY_SERVER) == null) {
94                  LOGGER.info(
95                          "If you are behind a proxy you may need to configure dependency-check to use the proxy.");
96              }
97              LOGGER.debug("", ex);
98          } finally {
99              closeDataStores();
100         }
101     }
102 
103     /**
104      * Checks if the NVD CVE XML files were last checked recently. As an
105      * optimization, we can avoid repetitive checks against the NVD. Setting
106      * CVE_CHECK_VALID_FOR_HOURS determines the duration since last check before
107      * checking again. A database property stores the timestamp of the last
108      * check.
109      *
110      * @return true to proceed with the check, or false to skip.
111      * @throws UpdateException thrown when there is an issue checking for
112      * updates.
113      */
114     private boolean checkUpdate() throws UpdateException {
115         boolean proceed = true;
116         // If the valid setting has not been specified, then we proceed to check...
117         final int validForHours = Settings.getInt(Settings.KEYS.CVE_CHECK_VALID_FOR_HOURS, 0);
118         if (dataExists() && 0 < validForHours) {
119             // ms Valid = valid (hours) x 60 min/hour x 60 sec/min x 1000 ms/sec
120             final long msValid = validForHours * 60L * 60L * 1000L;
121             final long lastChecked = Long.parseLong(getProperties().getProperty(DatabaseProperties.LAST_CHECKED, "0"));
122             final long now = System.currentTimeMillis();
123             proceed = (now - lastChecked) > msValid;
124             if (!proceed) {
125                 LOGGER.info("Skipping NVD check since last check was within {} hours.", validForHours);
126                 LOGGER.debug("Last NVD was at {}, and now {} is within {} ms.",
127                         lastChecked, now, msValid);
128             }
129         }
130         return proceed;
131     }
132 
133     /**
134      * Checks the CVE Index to ensure data exists and analysis can continue.
135      *
136      * @return true if the database contains data
137      */
138     private boolean dataExists() {
139         CveDB cve = null;
140         try {
141             cve = new CveDB();
142             cve.open();
143             return cve.dataExists();
144         } catch (DatabaseException ex) {
145             return false;
146         } finally {
147             if (cve != null) {
148                 cve.close();
149             }
150         }
151     }
152 
153     /**
154      * Downloads the latest NVD CVE XML file from the web and imports it into
155      * the current CVE Database.
156      *
157      * @param updateable a collection of NVD CVE data file references that need
158      * to be downloaded and processed to update the database
159      * @throws UpdateException is thrown if there is an error updating the
160      * database
161      */
162     public void performUpdate(UpdateableNvdCve updateable) throws UpdateException {
163         int maxUpdates = 0;
164         try {
165             for (NvdCveInfo cve : updateable) {
166                 if (cve.getNeedsUpdate()) {
167                     maxUpdates += 1;
168                 }
169             }
170             if (maxUpdates <= 0) {
171                 return;
172             }
173             if (maxUpdates > 3) {
174                 LOGGER.info(
175                         "NVD CVE requires several updates; this could take a couple of minutes.");
176             }
177             if (maxUpdates > 0) {
178                 openDataStores();
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         } finally {
249             closeDataStores();
250         }
251     }
252 
253     /**
254      * Determines if the index needs to be updated. This is done by fetching the
255      * NVD CVE meta data and checking the last update date. If the data needs to
256      * be refreshed this method will return the NvdCveUrl for the files that
257      * need to be updated.
258      *
259      * @return the collection of files that need to be updated
260      * @throws MalformedURLException is thrown if the URL for the NVD CVE Meta
261      * data is incorrect
262      * @throws DownloadFailedException is thrown if there is an error.
263      * downloading the NVD CVE download data file
264      * @throws UpdateException Is thrown if there is an issue with the last
265      * updated properties file
266      */
267     protected final UpdateableNvdCve getUpdatesNeeded() throws MalformedURLException, DownloadFailedException, UpdateException {
268         UpdateableNvdCve updates = null;
269         try {
270             updates = retrieveCurrentTimestampsFromWeb();
271         } catch (InvalidDataException ex) {
272             final String msg = "Unable to retrieve valid timestamp from nvd cve downloads page";
273             LOGGER.debug(msg, ex);
274             throw new DownloadFailedException(msg, ex);
275         } catch (InvalidSettingException ex) {
276             LOGGER.debug("Invalid setting found when retrieving timestamps", ex);
277             throw new DownloadFailedException("Invalid settings", ex);
278         }
279 
280         if (updates == null) {
281             throw new DownloadFailedException("Unable to retrieve the timestamps of the currently published NVD CVE data");
282         }
283         if (!getProperties().isEmpty()) {
284             try {
285                 final long lastUpdated = Long.parseLong(getProperties().getProperty(DatabaseProperties.LAST_UPDATED, "0"));
286                 final long now = System.currentTimeMillis();
287                 final int days = Settings.getInt(Settings.KEYS.CVE_MODIFIED_VALID_FOR_DAYS, 7);
288                 if (lastUpdated == updates.getTimeStamp(MODIFIED)) {
289                     updates.clear(); //we don't need to update anything.
290                 } else if (DateUtil.withinDateRange(lastUpdated, now, days)) {
291                     for (NvdCveInfo entry : updates) {
292                         if (MODIFIED.equals(entry.getId())) {
293                             entry.setNeedsUpdate(true);
294                         } else {
295                             entry.setNeedsUpdate(false);
296                         }
297                     }
298                 } else { //we figure out which of the several XML files need to be downloaded.
299                     for (NvdCveInfo entry : updates) {
300                         if (MODIFIED.equals(entry.getId())) {
301                             entry.setNeedsUpdate(true);
302                         } else {
303                             long currentTimestamp = 0;
304                             try {
305                                 currentTimestamp = Long.parseLong(getProperties().getProperty(DatabaseProperties.LAST_UPDATED_BASE
306                                         + entry.getId(), "0"));
307                             } catch (NumberFormatException ex) {
308                                 LOGGER.debug("Error parsing '{}' '{}' from nvdcve.lastupdated",
309                                         DatabaseProperties.LAST_UPDATED_BASE, entry.getId(), ex);
310                             }
311                             if (currentTimestamp == entry.getTimestamp()) {
312                                 entry.setNeedsUpdate(false);
313                             }
314                         }
315                     }
316                 }
317             } catch (NumberFormatException ex) {
318                 LOGGER.warn("An invalid schema version or timestamp exists in the data.properties file.");
319                 LOGGER.debug("", ex);
320             }
321         }
322         return updates;
323     }
324 
325     /**
326      * Retrieves the timestamps from the NVD CVE meta data file.
327      *
328      * @return the timestamp from the currently published nvdcve downloads page
329      * @throws MalformedURLException thrown if the URL for the NVD CCE Meta data
330      * is incorrect.
331      * @throws DownloadFailedException thrown if there is an error downloading
332      * the nvd cve meta data file
333      * @throws InvalidDataException thrown if there is an exception parsing the
334      * timestamps
335      * @throws InvalidSettingException thrown if the settings are invalid
336      */
337     private UpdateableNvdCve retrieveCurrentTimestampsFromWeb()
338             throws MalformedURLException, DownloadFailedException, InvalidDataException, InvalidSettingException {
339 
340         final UpdateableNvdCve updates = new UpdateableNvdCve();
341         updates.add(MODIFIED, Settings.getString(Settings.KEYS.CVE_MODIFIED_20_URL),
342                 Settings.getString(Settings.KEYS.CVE_MODIFIED_12_URL),
343                 false);
344 
345         final int start = Settings.getInt(Settings.KEYS.CVE_START_YEAR);
346         final int end = Calendar.getInstance().get(Calendar.YEAR);
347         final String baseUrl20 = Settings.getString(Settings.KEYS.CVE_SCHEMA_2_0);
348         final String baseUrl12 = Settings.getString(Settings.KEYS.CVE_SCHEMA_1_2);
349         for (int i = start; i <= end; i++) {
350             updates.add(Integer.toString(i), String.format(baseUrl20, i),
351                     String.format(baseUrl12, i),
352                     true);
353         }
354         return updates;
355     }
356 }