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