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.nvdcve;
19  
20  import java.io.IOException;
21  import java.io.UnsupportedEncodingException;
22  import java.sql.CallableStatement;
23  import java.sql.Connection;
24  import java.sql.PreparedStatement;
25  import java.sql.ResultSet;
26  import java.sql.SQLException;
27  import java.sql.Statement;
28  import java.util.ArrayList;
29  import java.util.HashMap;
30  import java.util.HashSet;
31  import java.util.List;
32  import java.util.Locale;
33  import java.util.Map;
34  import java.util.Map.Entry;
35  import java.util.MissingResourceException;
36  import java.util.Properties;
37  import java.util.ResourceBundle;
38  import java.util.Set;
39  import org.owasp.dependencycheck.data.cwe.CweDB;
40  import org.owasp.dependencycheck.dependency.Reference;
41  import org.owasp.dependencycheck.dependency.Vulnerability;
42  import org.owasp.dependencycheck.dependency.VulnerableSoftware;
43  import org.owasp.dependencycheck.utils.DBUtils;
44  import org.owasp.dependencycheck.utils.DependencyVersion;
45  import org.owasp.dependencycheck.utils.DependencyVersionUtil;
46  import org.owasp.dependencycheck.utils.Pair;
47  import org.owasp.dependencycheck.utils.Settings;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  /**
52   * The database holding information about the NVD CVE data.
53   *
54   * @author Jeremy Long
55   */
56  public class CveDB {
57  
58      /**
59       * The logger.
60       */
61      private static final Logger LOGGER = LoggerFactory.getLogger(CveDB.class);
62      /**
63       * Database connection
64       */
65      private Connection conn;
66      /**
67       * The bundle of statements used when accessing the database.
68       */
69      private ResourceBundle statementBundle = null;
70  
71      /**
72       * Creates a new CveDB object and opens the database connection. Note, the connection must be closed by the caller by calling
73       * the close method.
74       *
75       * @throws DatabaseException thrown if there is an exception opening the database.
76       */
77      public CveDB() throws DatabaseException {
78          super();
79          try {
80              open();
81              try {
82                  final String databaseProductName = conn.getMetaData().getDatabaseProductName();
83                  LOGGER.debug("Database dialect: {}", databaseProductName);
84                  final Locale dbDialect = new Locale(databaseProductName);
85                  statementBundle = ResourceBundle.getBundle("data/dbStatements", dbDialect);
86              } catch (SQLException se) {
87                  LOGGER.warn("Problem loading database specific dialect!", se);
88                  statementBundle = ResourceBundle.getBundle("data/dbStatements");
89              }
90              databaseProperties = new DatabaseProperties(this);
91          } catch (DatabaseException ex) {
92              throw ex;
93          }
94      }
95  
96      /**
97       * Returns the database connection.
98       *
99       * @return the database connection
100      */
101     protected Connection getConnection() {
102         return conn;
103     }
104 
105     /**
106      * Opens the database connection. If the database does not exist, it will create a new one.
107      *
108      * @throws DatabaseException thrown if there is an error opening the database connection
109      */
110     public final void open() throws DatabaseException {
111         if (!isOpen()) {
112             conn = ConnectionFactory.getConnection();
113         }
114     }
115 
116     /**
117      * Closes the DB4O database. Close should be called on this object when it is done being used.
118      */
119     public void close() {
120         if (conn != null) {
121             try {
122                 conn.close();
123             } catch (SQLException ex) {
124                 LOGGER.error("There was an error attempting to close the CveDB, see the log for more details.");
125                 LOGGER.debug("", ex);
126             } catch (Throwable ex) {
127                 LOGGER.error("There was an exception attempting to close the CveDB, see the log for more details.");
128                 LOGGER.debug("", ex);
129             }
130             conn = null;
131         }
132     }
133 
134     /**
135      * Returns whether the database connection is open or closed.
136      *
137      * @return whether the database connection is open or closed
138      */
139     public boolean isOpen() {
140         return conn != null;
141     }
142 
143     /**
144      * Commits all completed transactions.
145      *
146      * @throws SQLException thrown if a SQL Exception occurs
147      */
148     public void commit() throws SQLException {
149         //temporary remove this as autocommit is on.
150         //if (conn != null) {
151         //    conn.commit();
152         //}
153     }
154 
155     /**
156      * Cleans up the object and ensures that "close" has been called.
157      *
158      * @throws Throwable thrown if there is a problem
159      */
160     @Override
161     @SuppressWarnings("FinalizeDeclaration")
162     protected void finalize() throws Throwable {
163         LOGGER.debug("Entering finalize");
164         close();
165         super.finalize();
166     }
167     /**
168      * Database properties object containing the 'properties' from the database table.
169      */
170     private DatabaseProperties databaseProperties;
171 
172     /**
173      * Get the value of databaseProperties.
174      *
175      * @return the value of databaseProperties
176      */
177     public DatabaseProperties getDatabaseProperties() {
178         return databaseProperties;
179     }
180 
181     /**
182      * Searches the CPE entries in the database and retrieves all entries for a given vendor and product combination. The returned
183      * list will include all versions of the product that are registered in the NVD CVE data.
184      *
185      * @param vendor the identified vendor name of the dependency being analyzed
186      * @param product the identified name of the product of the dependency being analyzed
187      * @return a set of vulnerable software
188      */
189     public Set<VulnerableSoftware> getCPEs(String vendor, String product) {
190         final Set<VulnerableSoftware> cpe = new HashSet<VulnerableSoftware>();
191         ResultSet rs = null;
192         PreparedStatement ps = null;
193         try {
194             ps = getConnection().prepareStatement(statementBundle.getString("SELECT_CPE_ENTRIES"));
195             ps.setString(1, vendor);
196             ps.setString(2, product);
197             rs = ps.executeQuery();
198 
199             while (rs.next()) {
200                 final VulnerableSoftware vs = new VulnerableSoftware();
201                 vs.setCpe(rs.getString(1));
202                 cpe.add(vs);
203             }
204         } catch (SQLException ex) {
205             LOGGER.error("An unexpected SQL Exception occurred; please see the verbose log for more details.");
206             LOGGER.debug("", ex);
207         } finally {
208             DBUtils.closeResultSet(rs);
209             DBUtils.closeStatement(ps);
210         }
211         return cpe;
212     }
213 
214     /**
215      * Returns the entire list of vendor/product combinations.
216      *
217      * @return the entire list of vendor/product combinations
218      * @throws DatabaseException thrown when there is an error retrieving the data from the DB
219      */
220     public Set<Pair<String, String>> getVendorProductList() throws DatabaseException {
221         final Set<Pair<String, String>> data = new HashSet<Pair<String, String>>();
222         ResultSet rs = null;
223         PreparedStatement ps = null;
224         try {
225             ps = getConnection().prepareStatement(statementBundle.getString("SELECT_VENDOR_PRODUCT_LIST"));
226             rs = ps.executeQuery();
227             while (rs.next()) {
228                 data.add(new Pair<String, String>(rs.getString(1), rs.getString(2)));
229             }
230         } catch (SQLException ex) {
231             final String msg = "An unexpected SQL Exception occurred; please see the verbose log for more details.";
232             throw new DatabaseException(msg, ex);
233         } finally {
234             DBUtils.closeResultSet(rs);
235             DBUtils.closeStatement(ps);
236         }
237         return data;
238     }
239 
240     /**
241      * Returns a set of properties.
242      *
243      * @return the properties from the database
244      */
245     Properties getProperties() {
246         final Properties prop = new Properties();
247         PreparedStatement ps = null;
248         ResultSet rs = null;
249         try {
250             ps = getConnection().prepareStatement(statementBundle.getString("SELECT_PROPERTIES"));
251             rs = ps.executeQuery();
252             while (rs.next()) {
253                 prop.setProperty(rs.getString(1), rs.getString(2));
254             }
255         } catch (SQLException ex) {
256             LOGGER.error("An unexpected SQL Exception occurred; please see the verbose log for more details.");
257             LOGGER.debug("", ex);
258         } finally {
259             DBUtils.closeStatement(ps);
260             DBUtils.closeResultSet(rs);
261         }
262         return prop;
263     }
264 
265     /**
266      * Saves a property to the database.
267      *
268      * @param key the property key
269      * @param value the property value
270      */
271     void saveProperty(String key, String value) {
272         try {
273             try {
274                 final PreparedStatement mergeProperty = getConnection().prepareStatement(statementBundle.getString("MERGE_PROPERTY"));
275                 try {
276                     mergeProperty.setString(1, key);
277                     mergeProperty.setString(2, value);
278                     mergeProperty.executeUpdate();
279                 } finally {
280                     DBUtils.closeStatement(mergeProperty);
281                 }
282             } catch (MissingResourceException mre) {
283                 // No Merge statement, so doing an Update/Insert...
284                 PreparedStatement updateProperty = null;
285                 PreparedStatement insertProperty = null;
286                 try {
287                     updateProperty = getConnection().prepareStatement(statementBundle.getString("UPDATE_PROPERTY"));
288                     updateProperty.setString(1, value);
289                     updateProperty.setString(2, key);
290                     if (updateProperty.executeUpdate() == 0) {
291                         insertProperty = getConnection().prepareStatement(statementBundle.getString("INSERT_PROPERTY"));
292                         insertProperty.setString(1, key);
293                         insertProperty.setString(2, value);
294                         insertProperty.executeUpdate();
295                     }
296                 } finally {
297                     DBUtils.closeStatement(updateProperty);
298                     DBUtils.closeStatement(insertProperty);
299                 }
300             }
301         } catch (SQLException ex) {
302             LOGGER.warn("Unable to save property '{}' with a value of '{}' to the database", key, value);
303             LOGGER.debug("", ex);
304         }
305     }
306 
307     /**
308      * Retrieves the vulnerabilities associated with the specified CPE.
309      *
310      * @param cpeStr the CPE name
311      * @return a list of Vulnerabilities
312      * @throws DatabaseException thrown if there is an exception retrieving data
313      */
314     public List<Vulnerability> getVulnerabilities(String cpeStr) throws DatabaseException {
315         final VulnerableSoftware cpe = new VulnerableSoftware();
316         try {
317             cpe.parseName(cpeStr);
318         } catch (UnsupportedEncodingException ex) {
319             LOGGER.trace("", ex);
320         }
321         final DependencyVersion detectedVersion = parseDependencyVersion(cpe);
322         final List<Vulnerability> vulnerabilities = new ArrayList<Vulnerability>();
323 
324         PreparedStatement ps = null;
325         ResultSet rs = null;
326         try {
327             ps = getConnection().prepareStatement(statementBundle.getString("SELECT_CVE_FROM_SOFTWARE"));
328             ps.setString(1, cpe.getVendor());
329             ps.setString(2, cpe.getProduct());
330             rs = ps.executeQuery();
331             String currentCVE = "";
332 
333             final Map<String, Boolean> vulnSoftware = new HashMap<String, Boolean>();
334             while (rs.next()) {
335                 final String cveId = rs.getString(1);
336                 if (!currentCVE.equals(cveId)) { //check for match and add
337                     final Entry<String, Boolean> matchedCPE = getMatchingSoftware(vulnSoftware, cpe.getVendor(), cpe.getProduct(), detectedVersion);
338                     if (matchedCPE != null) {
339                         final Vulnerability v = getVulnerability(currentCVE);
340                         v.setMatchedCPE(matchedCPE.getKey(), matchedCPE.getValue() ? "Y" : null);
341                         vulnerabilities.add(v);
342                     }
343                     vulnSoftware.clear();
344                     currentCVE = cveId;
345                 }
346 
347                 final String cpeId = rs.getString(2);
348                 final String previous = rs.getString(3);
349                 final Boolean p = previous != null && !previous.isEmpty();
350                 vulnSoftware.put(cpeId, p);
351             }
352             //remember to process the last set of CVE/CPE entries
353             final Entry<String, Boolean> matchedCPE = getMatchingSoftware(vulnSoftware, cpe.getVendor(), cpe.getProduct(), detectedVersion);
354             if (matchedCPE != null) {
355                 final Vulnerability v = getVulnerability(currentCVE);
356                 v.setMatchedCPE(matchedCPE.getKey(), matchedCPE.getValue() ? "Y" : null);
357                 vulnerabilities.add(v);
358             }
359         } catch (SQLException ex) {
360             throw new DatabaseException("Exception retrieving vulnerability for " + cpeStr, ex);
361         } finally {
362             DBUtils.closeResultSet(rs);
363             DBUtils.closeStatement(ps);
364         }
365         return vulnerabilities;
366     }
367 
368     /**
369      * Gets a vulnerability for the provided CVE.
370      *
371      * @param cve the CVE to lookup
372      * @return a vulnerability object
373      * @throws DatabaseException if an exception occurs
374      */
375     private Vulnerability getVulnerability(String cve) throws DatabaseException {
376         PreparedStatement psV = null;
377         PreparedStatement psR = null;
378         PreparedStatement psS = null;
379         ResultSet rsV = null;
380         ResultSet rsR = null;
381         ResultSet rsS = null;
382         Vulnerability vuln = null;
383         try {
384             psV = getConnection().prepareStatement(statementBundle.getString("SELECT_VULNERABILITY"));
385             psV.setString(1, cve);
386             rsV = psV.executeQuery();
387             if (rsV.next()) {
388                 vuln = new Vulnerability();
389                 vuln.setName(cve);
390                 vuln.setDescription(rsV.getString(2));
391                 String cwe = rsV.getString(3);
392                 if (cwe != null) {
393                     final String name = CweDB.getCweName(cwe);
394                     if (name != null) {
395                         cwe += ' ' + name;
396                     }
397                 }
398                 final int cveId = rsV.getInt(1);
399                 vuln.setCwe(cwe);
400                 vuln.setCvssScore(rsV.getFloat(4));
401                 vuln.setCvssAccessVector(rsV.getString(5));
402                 vuln.setCvssAccessComplexity(rsV.getString(6));
403                 vuln.setCvssAuthentication(rsV.getString(7));
404                 vuln.setCvssConfidentialityImpact(rsV.getString(8));
405                 vuln.setCvssIntegrityImpact(rsV.getString(9));
406                 vuln.setCvssAvailabilityImpact(rsV.getString(10));
407 
408                 psR = getConnection().prepareStatement(statementBundle.getString("SELECT_REFERENCES"));
409                 psR.setInt(1, cveId);
410                 rsR = psR.executeQuery();
411                 while (rsR.next()) {
412                     vuln.addReference(rsR.getString(1), rsR.getString(2), rsR.getString(3));
413                 }
414                 psS = getConnection().prepareStatement(statementBundle.getString("SELECT_SOFTWARE"));
415                 psS.setInt(1, cveId);
416                 rsS = psS.executeQuery();
417                 while (rsS.next()) {
418                     final String cpe = rsS.getString(1);
419                     final String prevVersion = rsS.getString(2);
420                     if (prevVersion == null) {
421                         vuln.addVulnerableSoftware(cpe);
422                     } else {
423                         vuln.addVulnerableSoftware(cpe, prevVersion);
424                     }
425                 }
426             }
427         } catch (SQLException ex) {
428             throw new DatabaseException("Error retrieving " + cve, ex);
429         } finally {
430             DBUtils.closeResultSet(rsV);
431             DBUtils.closeResultSet(rsR);
432             DBUtils.closeResultSet(rsS);
433             DBUtils.closeStatement(psV);
434             DBUtils.closeStatement(psR);
435             DBUtils.closeStatement(psS);
436         }
437         return vuln;
438     }
439 
440     /**
441      * Updates the vulnerability within the database. If the vulnerability does not exist it will be added.
442      *
443      * @param vuln the vulnerability to add to the database
444      * @throws DatabaseException is thrown if the database
445      */
446     public void updateVulnerability(Vulnerability vuln) throws DatabaseException {
447         PreparedStatement selectVulnerabilityId = null;
448         PreparedStatement deleteVulnerability = null;
449         PreparedStatement deleteReferences = null;
450         PreparedStatement deleteSoftware = null;
451         PreparedStatement updateVulnerability = null;
452         PreparedStatement insertVulnerability = null;
453         PreparedStatement insertReference = null;
454         PreparedStatement selectCpeId = null;
455         PreparedStatement insertCpe = null;
456         PreparedStatement insertSoftware = null;
457 
458         try {
459             selectVulnerabilityId = getConnection().prepareStatement(statementBundle.getString("SELECT_VULNERABILITY_ID"));
460             deleteVulnerability = getConnection().prepareStatement(statementBundle.getString("DELETE_VULNERABILITY"));
461             deleteReferences = getConnection().prepareStatement(statementBundle.getString("DELETE_REFERENCE"));
462             deleteSoftware = getConnection().prepareStatement(statementBundle.getString("DELETE_SOFTWARE"));
463             updateVulnerability = getConnection().prepareStatement(statementBundle.getString("UPDATE_VULNERABILITY"));
464             final String[] ids = {"id"};
465             insertVulnerability = getConnection().prepareStatement(statementBundle.getString("INSERT_VULNERABILITY"),
466                     //Statement.RETURN_GENERATED_KEYS);
467                     ids);
468             insertReference = getConnection().prepareStatement(statementBundle.getString("INSERT_REFERENCE"));
469             selectCpeId = getConnection().prepareStatement(statementBundle.getString("SELECT_CPE_ID"));
470             insertCpe = getConnection().prepareStatement(statementBundle.getString("INSERT_CPE"),
471                     //Statement.RETURN_GENERATED_KEYS);
472                     ids);
473             insertSoftware = getConnection().prepareStatement(statementBundle.getString("INSERT_SOFTWARE"));
474             int vulnerabilityId = 0;
475             selectVulnerabilityId.setString(1, vuln.getName());
476             ResultSet rs = selectVulnerabilityId.executeQuery();
477             if (rs.next()) {
478                 vulnerabilityId = rs.getInt(1);
479                 // first delete any existing vulnerability info. We don't know what was updated. yes, slower but atm easier.
480                 deleteReferences.setInt(1, vulnerabilityId);
481                 deleteReferences.execute();
482                 deleteSoftware.setInt(1, vulnerabilityId);
483                 deleteSoftware.execute();
484             }
485             DBUtils.closeResultSet(rs);
486             rs = null;
487             if (vulnerabilityId != 0) {
488                 if (vuln.getDescription().contains("** REJECT **")) {
489                     deleteVulnerability.setInt(1, vulnerabilityId);
490                     deleteVulnerability.executeUpdate();
491                 } else {
492                     updateVulnerability.setString(1, vuln.getDescription());
493                     updateVulnerability.setString(2, vuln.getCwe());
494                     updateVulnerability.setFloat(3, vuln.getCvssScore());
495                     updateVulnerability.setString(4, vuln.getCvssAccessVector());
496                     updateVulnerability.setString(5, vuln.getCvssAccessComplexity());
497                     updateVulnerability.setString(6, vuln.getCvssAuthentication());
498                     updateVulnerability.setString(7, vuln.getCvssConfidentialityImpact());
499                     updateVulnerability.setString(8, vuln.getCvssIntegrityImpact());
500                     updateVulnerability.setString(9, vuln.getCvssAvailabilityImpact());
501                     updateVulnerability.setInt(10, vulnerabilityId);
502                     updateVulnerability.executeUpdate();
503                 }
504             } else {
505                 insertVulnerability.setString(1, vuln.getName());
506                 insertVulnerability.setString(2, vuln.getDescription());
507                 insertVulnerability.setString(3, vuln.getCwe());
508                 insertVulnerability.setFloat(4, vuln.getCvssScore());
509                 insertVulnerability.setString(5, vuln.getCvssAccessVector());
510                 insertVulnerability.setString(6, vuln.getCvssAccessComplexity());
511                 insertVulnerability.setString(7, vuln.getCvssAuthentication());
512                 insertVulnerability.setString(8, vuln.getCvssConfidentialityImpact());
513                 insertVulnerability.setString(9, vuln.getCvssIntegrityImpact());
514                 insertVulnerability.setString(10, vuln.getCvssAvailabilityImpact());
515                 insertVulnerability.execute();
516                 try {
517                     rs = insertVulnerability.getGeneratedKeys();
518                     rs.next();
519                     vulnerabilityId = rs.getInt(1);
520                 } catch (SQLException ex) {
521                     final String msg = String.format("Unable to retrieve id for new vulnerability for '%s'", vuln.getName());
522                     throw new DatabaseException(msg, ex);
523                 } finally {
524                     DBUtils.closeResultSet(rs);
525                     rs = null;
526                 }
527             }
528             insertReference.setInt(1, vulnerabilityId);
529             for (Reference r : vuln.getReferences()) {
530                 insertReference.setString(2, r.getName());
531                 insertReference.setString(3, r.getUrl());
532                 insertReference.setString(4, r.getSource());
533                 insertReference.execute();
534             }
535             for (VulnerableSoftware s : vuln.getVulnerableSoftware()) {
536                 int cpeProductId = 0;
537                 selectCpeId.setString(1, s.getName());
538                 try {
539                     rs = selectCpeId.executeQuery();
540                     if (rs.next()) {
541                         cpeProductId = rs.getInt(1);
542                     }
543                 } catch (SQLException ex) {
544                     throw new DatabaseException("Unable to get primary key for new cpe: " + s.getName(), ex);
545                 } finally {
546                     DBUtils.closeResultSet(rs);
547                     rs = null;
548                 }
549 
550                 if (cpeProductId == 0) {
551                     insertCpe.setString(1, s.getName());
552                     insertCpe.setString(2, s.getVendor());
553                     insertCpe.setString(3, s.getProduct());
554                     insertCpe.executeUpdate();
555                     cpeProductId = DBUtils.getGeneratedKey(insertCpe);
556                 }
557                 if (cpeProductId == 0) {
558                     throw new DatabaseException("Unable to retrieve cpeProductId - no data returned");
559                 }
560 
561                 insertSoftware.setInt(1, vulnerabilityId);
562                 insertSoftware.setInt(2, cpeProductId);
563                 if (s.getPreviousVersion() == null) {
564                     insertSoftware.setNull(3, java.sql.Types.VARCHAR);
565                 } else {
566                     insertSoftware.setString(3, s.getPreviousVersion());
567                 }
568                 insertSoftware.execute();
569             }
570 
571         } catch (SQLException ex) {
572             final String msg = String.format("Error updating '%s'", vuln.getName());
573             LOGGER.debug("", ex);
574             throw new DatabaseException(msg, ex);
575         } finally {
576             DBUtils.closeStatement(selectVulnerabilityId);
577             DBUtils.closeStatement(deleteReferences);
578             DBUtils.closeStatement(deleteSoftware);
579             DBUtils.closeStatement(updateVulnerability);
580             DBUtils.closeStatement(deleteVulnerability);
581             DBUtils.closeStatement(insertVulnerability);
582             DBUtils.closeStatement(insertReference);
583             DBUtils.closeStatement(selectCpeId);
584             DBUtils.closeStatement(insertCpe);
585             DBUtils.closeStatement(insertSoftware);
586         }
587     }
588 
589     /**
590      * Checks to see if data exists so that analysis can be performed.
591      *
592      * @return <code>true</code> if data exists; otherwise <code>false</code>
593      */
594     public boolean dataExists() {
595         Statement cs = null;
596         ResultSet rs = null;
597         try {
598             cs = conn.createStatement();
599             rs = cs.executeQuery("SELECT COUNT(*) records FROM cpeEntry");
600             if (rs.next()) {
601                 if (rs.getInt(1) > 0) {
602                     return true;
603                 }
604             }
605         } catch (SQLException ex) {
606             String dd;
607             try {
608                 dd = Settings.getDataDirectory().getAbsolutePath();
609             } catch (IOException ex1) {
610                 dd = Settings.getString(Settings.KEYS.DATA_DIRECTORY);
611             }
612             LOGGER.error("Unable to access the local database.\n\nEnsure that '{}' is a writable directory. "
613                     + "If the problem persist try deleting the files in '{}' and running {} again. If the problem continues, please "
614                     + "create a log file (see documentation at http://jeremylong.github.io/DependencyCheck/) and open a ticket at "
615                     + "https://github.com/jeremylong/DependencyCheck/issues and include the log file.\n\n",
616                     dd, dd, Settings.getString(Settings.KEYS.APPLICATION_VAME));
617             LOGGER.debug("", ex);
618         } finally {
619             DBUtils.closeResultSet(rs);
620             DBUtils.closeStatement(cs);
621         }
622         return false;
623     }
624 
625     /**
626      * It is possible that orphaned rows may be generated during database updates. This should be called after all updates have
627      * been completed to ensure orphan entries are removed.
628      */
629     public void cleanupDatabase() {
630         PreparedStatement ps = null;
631         try {
632             ps = getConnection().prepareStatement(statementBundle.getString("CLEANUP_ORPHANS"));
633             if (ps != null) {
634                 ps.executeUpdate();
635             }
636         } catch (SQLException ex) {
637             LOGGER.error("An unexpected SQL Exception occurred; please see the verbose log for more details.");
638             LOGGER.debug("", ex);
639         } finally {
640             DBUtils.closeStatement(ps);
641         }
642     }
643 
644     /**
645      * Determines if the given identifiedVersion is affected by the given cpeId and previous version flag. A non-null, non-empty
646      * string passed to the previous version argument indicates that all previous versions are affected.
647      *
648      * @param vendor the vendor of the dependency being analyzed
649      * @param product the product name of the dependency being analyzed
650      * @param vulnerableSoftware a map of the vulnerable software with a boolean indicating if all previous versions are affected
651      * @param identifiedVersion the identified version of the dependency being analyzed
652      * @return true if the identified version is affected, otherwise false
653      */
654     Entry<String, Boolean> getMatchingSoftware(Map<String, Boolean> vulnerableSoftware, String vendor, String product,
655             DependencyVersion identifiedVersion) {
656 
657         final boolean isVersionTwoADifferentProduct = "apache".equals(vendor) && "struts".equals(product);
658 
659         final Set<String> majorVersionsAffectingAllPrevious = new HashSet<String>();
660         final boolean matchesAnyPrevious = identifiedVersion == null || "-".equals(identifiedVersion.toString());
661         String majorVersionMatch = null;
662         for (Entry<String, Boolean> entry : vulnerableSoftware.entrySet()) {
663             final DependencyVersion v = parseDependencyVersion(entry.getKey());
664             if (v == null || "-".equals(v.toString())) { //all versions
665                 return entry;
666             }
667             if (entry.getValue()) {
668                 if (matchesAnyPrevious) {
669                     return entry;
670                 }
671                 if (identifiedVersion != null && identifiedVersion.getVersionParts().get(0).equals(v.getVersionParts().get(0))) {
672                     majorVersionMatch = v.getVersionParts().get(0);
673                 }
674                 majorVersionsAffectingAllPrevious.add(v.getVersionParts().get(0));
675             }
676         }
677         if (matchesAnyPrevious) {
678             return null;
679         }
680 
681         final boolean canSkipVersions = majorVersionMatch != null && majorVersionsAffectingAllPrevious.size() > 1;
682         //yes, we are iterating over this twice. The first time we are skipping versions those that affect all versions
683         //then later we process those that affect all versions. This could be done with sorting...
684         for (Entry<String, Boolean> entry : vulnerableSoftware.entrySet()) {
685             if (!entry.getValue()) {
686                 final DependencyVersion v = parseDependencyVersion(entry.getKey());
687                 //this can't dereference a null 'majorVersionMatch' as canSkipVersions accounts for this.
688                 if (canSkipVersions && !majorVersionMatch.equals(v.getVersionParts().get(0))) {
689                     continue;
690                 }
691                 //this can't dereference a null 'identifiedVersion' because if it was null we would have exited
692                 //in the above loop or just after loop (if matchesAnyPrevious return null).
693                 if (identifiedVersion.equals(v)) {
694                     return entry;
695                 }
696             }
697         }
698         for (Entry<String, Boolean> entry : vulnerableSoftware.entrySet()) {
699             if (entry.getValue()) {
700                 final DependencyVersion v = parseDependencyVersion(entry.getKey());
701                 //this can't dereference a null 'majorVersionMatch' as canSkipVersions accounts for this.
702                 if (canSkipVersions && !majorVersionMatch.equals(v.getVersionParts().get(0))) {
703                     continue;
704                 }
705                 //this can't dereference a null 'identifiedVersion' because if it was null we would have exited
706                 //in the above loop or just after loop (if matchesAnyPrevious return null).
707                 if (entry.getValue() && identifiedVersion.compareTo(v) <= 0) {
708                     if (!(isVersionTwoADifferentProduct && !identifiedVersion.getVersionParts().get(0).equals(v.getVersionParts().get(0)))) {
709                         return entry;
710                     }
711                 }
712             }
713         }
714         return null;
715     }
716 
717     /**
718      * Parses the version (including revision) from a CPE identifier. If no version is identified then a '-' is returned.
719      *
720      * @param cpeStr a cpe identifier
721      * @return a dependency version
722      */
723     private DependencyVersion parseDependencyVersion(String cpeStr) {
724         final VulnerableSoftware cpe = new VulnerableSoftware();
725         try {
726             cpe.parseName(cpeStr);
727         } catch (UnsupportedEncodingException ex) {
728             //never going to happen.
729             LOGGER.trace("", ex);
730         }
731         return parseDependencyVersion(cpe);
732     }
733 
734     /**
735      * Takes a CPE and parses out the version number. If no version is identified then a '-' is returned.
736      *
737      * @param cpe a cpe object
738      * @return a dependency version
739      */
740     private DependencyVersion parseDependencyVersion(VulnerableSoftware cpe) {
741         final DependencyVersion cpeVersion;
742         if (cpe.getVersion() != null && !cpe.getVersion().isEmpty()) {
743             final String versionText;
744             if (cpe.getUpdate() != null && !cpe.getUpdate().isEmpty()) {
745                 versionText = String.format("%s.%s", cpe.getVersion(), cpe.getUpdate());
746             } else {
747                 versionText = cpe.getVersion();
748             }
749             cpeVersion = DependencyVersionUtil.parseVersion(versionText);
750         } else {
751             cpeVersion = new DependencyVersion("-");
752         }
753         return cpeVersion;
754     }
755 
756     /**
757      * This method is only referenced in unused code.
758      *
759      * Deletes unused dictionary entries from the database.
760      */
761     public void deleteUnusedCpe() {
762         CallableStatement cs = null;
763         try {
764             cs = getConnection().prepareCall(statementBundle.getString("DELETE_UNUSED_DICT_CPE"));
765             cs.executeUpdate();
766         } catch (SQLException ex) {
767             LOGGER.error("Unable to delete CPE dictionary entries", ex);
768         } finally {
769             DBUtils.closeStatement(cs);
770         }
771     }
772 
773     /**
774      * This method is only referenced in unused code and will likely break on MySQL if ever used due to the MERGE statement.
775      *
776      * Merges CPE entries into the database.
777      *
778      * @param cpe the CPE identifier
779      * @param vendor the CPE vendor
780      * @param product the CPE product
781      */
782     public void addCpe(String cpe, String vendor, String product) {
783         PreparedStatement ps = null;
784         try {
785             ps = getConnection().prepareCall(statementBundle.getString("ADD_DICT_CPE"));
786             ps.setString(1, cpe);
787             ps.setString(2, vendor);
788             ps.setString(3, product);
789             ps.executeUpdate();
790         } catch (SQLException ex) {
791             LOGGER.error("Unable to add CPE dictionary entry", ex);
792         } finally {
793             DBUtils.closeStatement(ps);
794         }
795     }
796 }