org.owasp.dependencycheck.data.nvdcve.CveDB.java Source code

Java tutorial

Introduction

Here is the source code for org.owasp.dependencycheck.data.nvdcve.CveDB.java

Source

/*
 * This file is part of dependency-check-core.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
 */
package org.owasp.dependencycheck.data.nvdcve;

import org.apache.commons.collections.map.ReferenceMap;
import org.owasp.dependencycheck.data.cwe.CweDB;
import org.owasp.dependencycheck.dependency.Reference;
import org.owasp.dependencycheck.dependency.Vulnerability;
import org.owasp.dependencycheck.dependency.VulnerableSoftware;
import org.owasp.dependencycheck.utils.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.concurrent.ThreadSafe;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.Map.Entry;

import static org.apache.commons.collections.map.AbstractReferenceMap.HARD;
import static org.apache.commons.collections.map.AbstractReferenceMap.SOFT;
import static org.owasp.dependencycheck.data.nvdcve.CveDB.PreparedStatementCveDb.*;

//CSOFF: AvoidStarImport
//CSON: AvoidStarImport
/**
 * The database holding information about the NVD CVE data. This class is safe
 * to be accessed from multiple threads in parallel, however internally only one
 * connection will be used.
 *
 * @author Jeremy Long
 */
@ThreadSafe
public final class CveDB implements AutoCloseable {

    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(CveDB.class);

    /**
     * The database connection factory.
     */
    private final ConnectionFactory connectionFactory;
    /**
     * Database connection
     */
    private Connection connection;
    /**
     * The bundle of statements used when accessing the database.
     */
    private ResourceBundle statementBundle;
    /**
     * Database properties object containing the 'properties' from the database
     * table.
     */
    private DatabaseProperties databaseProperties;
    /**
     * The prepared statements.
     */
    private final EnumMap<PreparedStatementCveDb, PreparedStatement> preparedStatements = new EnumMap<>(
            PreparedStatementCveDb.class);

    /**
     * Cache for CVE lookups; used to speed up the vulnerability search process.
     */
    @SuppressWarnings("unchecked")
    private final Map<String, List<Vulnerability>> vulnerabilitiesForCpeCache = Collections
            .synchronizedMap(new ReferenceMap(HARD, SOFT));
    /**
     * The configured settings
     */
    private final Settings settings;

    /**
     * The enum value names must match the keys of the statements in the
     * statement bundles "dbStatements*.properties".
     */
    enum PreparedStatementCveDb {
        /**
         * Key for SQL Statement.
         */
        CLEANUP_ORPHANS,
        /**
         * Key for SQL Statement.
         */
        COUNT_CPE,
        /**
         * Key for SQL Statement.
         */
        DELETE_REFERENCE,
        /**
         * Key for SQL Statement.
         */
        DELETE_SOFTWARE,
        /**
         * Key for SQL Statement.
         */
        DELETE_VULNERABILITY,
        /**
         * Key for SQL Statement.
         */
        INSERT_CPE,
        /**
         * Key for SQL Statement.
         */
        INSERT_PROPERTY,
        /**
         * Key for SQL Statement.
         */
        INSERT_REFERENCE,
        /**
         * Key for SQL Statement.
         */
        INSERT_SOFTWARE,
        /**
         * Key for SQL Statement.
         */
        INSERT_VULNERABILITY,
        /**
         * Key for SQL Statement.
         */
        MERGE_PROPERTY,
        /**
         * Key for SQL Statement.
         */
        SELECT_CPE_ENTRIES,
        /**
         * Key for SQL Statement.
         */
        SELECT_CPE_ID,
        /**
         * Key for SQL Statement.
         */
        SELECT_CVE_FROM_SOFTWARE,
        /**
         * Key for SQL Statement.
         */
        SELECT_PROPERTIES,
        /**
         * Key for SQL Statement.
         */
        SELECT_REFERENCES,
        /**
         * Key for SQL Statement.
         */
        SELECT_SOFTWARE,
        /**
         * Key for SQL Statement.
         */
        SELECT_VENDOR_PRODUCT_LIST,
        /**
         * Key for SQL Statement.
         */
        SELECT_VULNERABILITY,
        /**
         * Key for SQL Statement.
         */
        SELECT_VULNERABILITY_ID,
        /**
         * Key for SQL Statement.
         */
        UPDATE_PROPERTY,
        /**
         * Key for SQL Statement.
         */
        UPDATE_VULNERABILITY
    }

    /**
     * Creates a new CveDB object and opens the database connection. Note, the
     * connection must be closed by the caller by calling the close method.
     *
     * @param settings the configured settings
     * @throws DatabaseException thrown if there is an exception opening the
     * database.
     */
    public CveDB(Settings settings) throws DatabaseException {
        this.settings = settings;
        connectionFactory = new ConnectionFactory(settings);
        open();
    }

    /**
     * Tries to determine the product name of the database.
     *
     * @param conn the database connection
     * @return the product name of the database if successful, {@code null} else
     */
    private String determineDatabaseProductName(Connection conn) {
        try {
            final String databaseProductName = conn.getMetaData().getDatabaseProductName().toLowerCase();
            LOGGER.debug("Database product: {}", databaseProductName);
            return databaseProductName;
        } catch (SQLException se) {
            LOGGER.warn("Problem determining database product!", se);
            return null;
        }
    }

    /**
     * Opens the database connection. If the database does not exist, it will
     * create a new one.
     *
     * @throws DatabaseException thrown if there is an error opening the
     * database connection
     */
    private synchronized void open() throws DatabaseException {
        try {
            if (!isOpen()) {
                connection = connectionFactory.getConnection();
                final String databaseProductName = determineDatabaseProductName(this.connection);
                statementBundle = databaseProductName != null
                        ? ResourceBundle.getBundle("data/dbStatements", new Locale(databaseProductName))
                        : ResourceBundle.getBundle("data/dbStatements");
                prepareStatements();
                databaseProperties = new DatabaseProperties(this);
            }
        } catch (DatabaseException e) {
            releaseResources();
            throw e;
        }
    }

    /**
     * Closes the database connection. Close should be called on this object
     * when it is done being used.
     */
    @Override
    public synchronized void close() {
        if (isOpen()) {
            clearCache();
            closeStatements();
            try {
                connection.close();
            } catch (SQLException ex) {
                LOGGER.error("There was an error attempting to close the CveDB, see the log for more details.");
                LOGGER.debug("", ex);
            } catch (Throwable ex) {
                LOGGER.error("There was an exception attempting to close the CveDB, see the log for more details.");
                LOGGER.debug("", ex);
            }
            releaseResources();
            connectionFactory.cleanup();
        }
    }

    /**
     * Releases the resources used by CveDB.
     */
    private synchronized void releaseResources() {
        statementBundle = null;
        preparedStatements.clear();
        databaseProperties = null;
        connection = null;
    }

    /**
     * Returns whether the database connection is open or closed.
     *
     * @return whether the database connection is open or closed
     */
    protected synchronized boolean isOpen() {
        return connection != null;
    }

    /**
     * Prepares all statements to be used.
     *
     * @throws DatabaseException thrown if there is an error preparing the
     * statements
     */
    private void prepareStatements() throws DatabaseException {
        for (PreparedStatementCveDb key : values()) {
            PreparedStatement preparedStatement = null;
            try {
                final String statementString = statementBundle.getString(key.name());
                if (key == INSERT_VULNERABILITY || key == INSERT_CPE) {
                    preparedStatement = connection.prepareStatement(statementString, new String[] { "id" });
                } else {
                    preparedStatement = connection.prepareStatement(statementString);
                }
            } catch (SQLException ex) {
                throw new DatabaseException(ex);
            } catch (MissingResourceException ex) {
                if (!ex.getMessage().contains("key MERGE_PROPERTY")) {
                    throw new DatabaseException(ex);
                }
            }
            if (preparedStatement != null) {
                preparedStatements.put(key, preparedStatement);
            }
        }
    }

    /**
     * Closes all prepared statements.
     */
    private synchronized void closeStatements() {
        for (PreparedStatement preparedStatement : preparedStatements.values()) {
            DBUtils.closeStatement(preparedStatement);
        }
    }

    /**
     * Returns the specified prepared statement.
     *
     * @param key the prepared statement from {@link PreparedStatementCveDb} to
     * return
     * @return the prepared statement
     * @throws SQLException thrown if a SQL Exception occurs
     */
    private synchronized PreparedStatement getPreparedStatement(PreparedStatementCveDb key) throws SQLException {
        if (!preparedStatements.containsKey(key)) {
            return null;
        }
        final PreparedStatement preparedStatement = preparedStatements.get(key);
        preparedStatement.clearParameters();
        return preparedStatement;
    }

    /**
     * Commits all completed transactions.
     *
     * @throws SQLException thrown if a SQL Exception occurs
     */
    @SuppressWarnings("EmptyMethod")
    public synchronized void commit() throws SQLException {
        //temporary remove this as autocommit is on.
        //if (isOpen()) {
        //    connection.commit();
        //}
    }

    /**
     * Cleans up the object and ensures that "close" has been called.
     *
     * @throws Throwable thrown if there is a problem
     */
    @Override
    @SuppressWarnings("FinalizeDeclaration")
    protected void finalize() throws Throwable {
        LOGGER.debug("Entering finalize");
        close();
        super.finalize();
    }

    /**
     * Get the value of databaseProperties.
     *
     * @return the value of databaseProperties
     */
    public synchronized DatabaseProperties getDatabaseProperties() {
        return databaseProperties;
    }

    /**
     * Used within the unit tests to reload the database properties.
     *
     * @return the database properties
     */
    protected synchronized DatabaseProperties reloadProperties() {
        databaseProperties = new DatabaseProperties(this);
        return databaseProperties;
    }

    /**
     * Searches the CPE entries in the database and retrieves all entries for a
     * given vendor and product combination. The returned list will include all
     * versions of the product that are registered in the NVD CVE data.
     *
     * @param vendor the identified vendor name of the dependency being analyzed
     * @param product the identified name of the product of the dependency being
     * analyzed
     * @return a set of vulnerable software
     */
    public synchronized Set<VulnerableSoftware> getCPEs(String vendor, String product) {
        final Set<VulnerableSoftware> cpe = new HashSet<>();
        ResultSet rs = null;
        try {
            final PreparedStatement ps = getPreparedStatement(SELECT_CPE_ENTRIES);
            ps.setString(1, vendor);
            ps.setString(2, product);
            rs = ps.executeQuery();

            while (rs.next()) {
                final VulnerableSoftware vs = new VulnerableSoftware();
                vs.setCpe(rs.getString(1));
                cpe.add(vs);
            }
        } catch (SQLException ex) {
            LOGGER.error("An unexpected SQL Exception occurred; please see the verbose log for more details.");
            LOGGER.debug("", ex);
        } finally {
            DBUtils.closeResultSet(rs);
        }
        return cpe;
    }

    /**
     * Returns the entire list of vendor/product combinations.
     *
     * @return the entire list of vendor/product combinations
     * @throws DatabaseException thrown when there is an error retrieving the
     * data from the DB
     */
    public synchronized Set<Pair<String, String>> getVendorProductList() throws DatabaseException {
        final Set<Pair<String, String>> data = new HashSet<>();
        ResultSet rs = null;
        try {
            final PreparedStatement ps = getPreparedStatement(SELECT_VENDOR_PRODUCT_LIST);
            rs = ps.executeQuery();
            while (rs.next()) {
                data.add(new Pair<>(rs.getString(1), rs.getString(2)));
            }
        } catch (SQLException ex) {
            final String msg = "An unexpected SQL Exception occurred; please see the verbose log for more details.";
            throw new DatabaseException(msg, ex);
        } finally {
            DBUtils.closeResultSet(rs);
        }
        return data;
    }

    /**
     * Returns a set of properties.
     *
     * @return the properties from the database
     */
    public synchronized Properties getProperties() {
        final Properties prop = new Properties();
        ResultSet rs = null;
        try {
            final PreparedStatement ps = getPreparedStatement(SELECT_PROPERTIES);
            rs = ps.executeQuery();
            while (rs.next()) {
                prop.setProperty(rs.getString(1), rs.getString(2));
            }
        } catch (SQLException ex) {
            LOGGER.error("An unexpected SQL Exception occurred; please see the verbose log for more details.");
            LOGGER.debug("", ex);
        } finally {
            DBUtils.closeResultSet(rs);
        }
        return prop;
    }

    /**
     * Saves a property to the database.
     *
     * @param key the property key
     * @param value the property value
     */
    public synchronized void saveProperty(String key, String value) {
        clearCache();
        try {
            final PreparedStatement mergeProperty = getPreparedStatement(MERGE_PROPERTY);
            if (mergeProperty != null) {
                mergeProperty.setString(1, key);
                mergeProperty.setString(2, value);
                mergeProperty.execute();
            } else {
                // No Merge statement, so doing an Update/Insert...
                final PreparedStatement updateProperty = getPreparedStatement(UPDATE_PROPERTY);
                updateProperty.setString(1, value);
                updateProperty.setString(2, key);
                if (updateProperty.executeUpdate() == 0) {
                    final PreparedStatement insertProperty = getPreparedStatement(INSERT_PROPERTY);
                    insertProperty.setString(1, key);
                    insertProperty.setString(2, value);
                    insertProperty.executeUpdate();
                }
            }
        } catch (SQLException ex) {
            LOGGER.warn("Unable to save property '{}' with a value of '{}' to the database", key, value);
            LOGGER.debug("", ex);
        }
    }

    /**
     * Clears cache. Should be called whenever something is modified. While this
     * is not the optimal cache eviction strategy, this is good enough for
     * typical usage (update DB and then only read) and it is easier to maintain
     * the code.
     * <p>
     * It should be also called when DB is closed.
     * </p>
     */
    private synchronized void clearCache() {
        vulnerabilitiesForCpeCache.clear();
    }

    /**
     * Retrieves the vulnerabilities associated with the specified CPE.
     *
     * @param cpeStr the CPE name
     * @return a list of Vulnerabilities
     * @throws DatabaseException thrown if there is an exception retrieving data
     */
    public synchronized List<Vulnerability> getVulnerabilities(String cpeStr) throws DatabaseException {
        final List<Vulnerability> cachedVulnerabilities = vulnerabilitiesForCpeCache.get(cpeStr);
        if (cachedVulnerabilities != null) {
            LOGGER.debug("Cache hit for {}", cpeStr);
            return cachedVulnerabilities;
        } else {
            LOGGER.debug("Cache miss for {}", cpeStr);
        }
        final VulnerableSoftware cpe = new VulnerableSoftware();
        try {
            cpe.parseName(cpeStr);
        } catch (UnsupportedEncodingException ex) {
            LOGGER.trace("", ex);
        }
        final DependencyVersion detectedVersion = parseDependencyVersion(cpe);
        final List<Vulnerability> vulnerabilities = new ArrayList<>();

        ResultSet rs = null;
        try {
            final PreparedStatement ps = getPreparedStatement(SELECT_CVE_FROM_SOFTWARE);
            ps.setString(1, cpe.getVendor());
            ps.setString(2, cpe.getProduct());
            rs = ps.executeQuery();
            String currentCVE = "";

            final Map<String, Boolean> vulnSoftware = new HashMap<>();
            while (rs.next()) {
                final String cveId = rs.getString(1);
                if (!currentCVE.equals(cveId)) { //check for match and add
                    final Entry<String, Boolean> matchedCPE = getMatchingSoftware(vulnSoftware, cpe.getVendor(),
                            cpe.getProduct(), detectedVersion);
                    if (matchedCPE != null) {
                        final Vulnerability v = getVulnerability(currentCVE);
                        if (v != null) {
                            v.setMatchedCPE(matchedCPE.getKey(), matchedCPE.getValue() ? "Y" : null);
                            vulnerabilities.add(v);
                        }
                    }
                    vulnSoftware.clear();
                    currentCVE = cveId;
                }

                final String cpeId = rs.getString(2);
                final String previous = rs.getString(3);
                final Boolean p = previous != null && !previous.isEmpty();
                vulnSoftware.put(cpeId, p);
            }
            //remember to process the last set of CVE/CPE entries
            final Entry<String, Boolean> matchedCPE = getMatchingSoftware(vulnSoftware, cpe.getVendor(),
                    cpe.getProduct(), detectedVersion);
            if (matchedCPE != null) {
                final Vulnerability v = getVulnerability(currentCVE);
                if (v != null) {
                    v.setMatchedCPE(matchedCPE.getKey(), matchedCPE.getValue() ? "Y" : null);
                    vulnerabilities.add(v);
                }
            }
        } catch (SQLException ex) {
            throw new DatabaseException("Exception retrieving vulnerability for " + cpeStr, ex);
        } finally {
            DBUtils.closeResultSet(rs);
        }
        vulnerabilitiesForCpeCache.put(cpeStr, vulnerabilities);
        return vulnerabilities;
    }

    /**
     * Gets a vulnerability for the provided CVE.
     *
     * @param cve the CVE to lookup
     * @return a vulnerability object
     * @throws DatabaseException if an exception occurs
     */
    public synchronized Vulnerability getVulnerability(String cve) throws DatabaseException {
        ResultSet rsV = null;
        ResultSet rsR = null;
        ResultSet rsS = null;
        Vulnerability vuln = null;

        try {
            final PreparedStatement psV = getPreparedStatement(SELECT_VULNERABILITY);
            psV.setString(1, cve);
            rsV = psV.executeQuery();
            if (rsV.next()) {
                vuln = new Vulnerability();
                vuln.setName(cve);
                vuln.setDescription(rsV.getString(2));
                String cwe = rsV.getString(3);
                if (cwe != null) {
                    final String name = CweDB.getCweName(cwe);
                    if (name != null) {
                        cwe += ' ' + name;
                    }
                }
                final int cveId = rsV.getInt(1);
                vuln.setCwe(cwe);
                vuln.setCvssScore(rsV.getFloat(4));
                vuln.setCvssAccessVector(rsV.getString(5));
                vuln.setCvssAccessComplexity(rsV.getString(6));
                vuln.setCvssAuthentication(rsV.getString(7));
                vuln.setCvssConfidentialityImpact(rsV.getString(8));
                vuln.setCvssIntegrityImpact(rsV.getString(9));
                vuln.setCvssAvailabilityImpact(rsV.getString(10));

                final PreparedStatement psR = getPreparedStatement(SELECT_REFERENCES);
                psR.setInt(1, cveId);
                rsR = psR.executeQuery();
                while (rsR.next()) {
                    vuln.addReference(rsR.getString(1), rsR.getString(2), rsR.getString(3));
                }

                final PreparedStatement psS = getPreparedStatement(SELECT_SOFTWARE);
                psS.setInt(1, cveId);
                rsS = psS.executeQuery();
                while (rsS.next()) {
                    final String cpe = rsS.getString(1);
                    final String prevVersion = rsS.getString(2);
                    if (prevVersion == null) {
                        vuln.addVulnerableSoftware(cpe);
                    } else {
                        vuln.addVulnerableSoftware(cpe, prevVersion);
                    }
                }
            }
        } catch (SQLException ex) {
            throw new DatabaseException("Error retrieving " + cve, ex);
        } finally {
            DBUtils.closeResultSet(rsV);
            DBUtils.closeResultSet(rsR);
            DBUtils.closeResultSet(rsS);
        }
        return vuln;
    }

    /**
     * Updates the vulnerability within the database. If the vulnerability does
     * not exist it will be added.
     *
     * @param vuln the vulnerability to add to the database
     * @throws DatabaseException is thrown if the database
     */
    public synchronized void updateVulnerability(Vulnerability vuln) throws DatabaseException {
        clearCache();
        ResultSet rs = null;
        try {
            int vulnerabilityId = 0;
            final PreparedStatement selectVulnerabilityId = getPreparedStatement(SELECT_VULNERABILITY_ID);
            selectVulnerabilityId.setString(1, vuln.getName());
            rs = selectVulnerabilityId.executeQuery();
            if (rs.next()) {
                vulnerabilityId = rs.getInt(1);
                // first delete any existing vulnerability info. We don't know what was updated. yes, slower but atm easier.
                final PreparedStatement deleteReference = getPreparedStatement(DELETE_REFERENCE);
                deleteReference.setInt(1, vulnerabilityId);
                deleteReference.execute();

                final PreparedStatement deleteSoftware = getPreparedStatement(DELETE_SOFTWARE);
                deleteSoftware.setInt(1, vulnerabilityId);
                deleteSoftware.execute();
            }

            DBUtils.closeResultSet(rs);

            if (vulnerabilityId != 0) {
                if (vuln.getDescription().contains("** REJECT **")) {
                    final PreparedStatement deleteVulnerability = getPreparedStatement(DELETE_VULNERABILITY);
                    deleteVulnerability.setInt(1, vulnerabilityId);
                    deleteVulnerability.executeUpdate();
                } else {
                    final PreparedStatement updateVulnerability = getPreparedStatement(UPDATE_VULNERABILITY);
                    updateVulnerability.setString(1, vuln.getDescription());
                    updateVulnerability.setString(2, vuln.getCwe());
                    updateVulnerability.setFloat(3, vuln.getCvssScore());
                    updateVulnerability.setString(4, vuln.getCvssAccessVector());
                    updateVulnerability.setString(5, vuln.getCvssAccessComplexity());
                    updateVulnerability.setString(6, vuln.getCvssAuthentication());
                    updateVulnerability.setString(7, vuln.getCvssConfidentialityImpact());
                    updateVulnerability.setString(8, vuln.getCvssIntegrityImpact());
                    updateVulnerability.setString(9, vuln.getCvssAvailabilityImpact());
                    updateVulnerability.setInt(10, vulnerabilityId);
                    updateVulnerability.executeUpdate();
                }
            } else {
                final PreparedStatement insertVulnerability = getPreparedStatement(INSERT_VULNERABILITY);
                insertVulnerability.setString(1, vuln.getName());
                insertVulnerability.setString(2, vuln.getDescription());
                insertVulnerability.setString(3, vuln.getCwe());
                insertVulnerability.setFloat(4, vuln.getCvssScore());
                insertVulnerability.setString(5, vuln.getCvssAccessVector());
                insertVulnerability.setString(6, vuln.getCvssAccessComplexity());
                insertVulnerability.setString(7, vuln.getCvssAuthentication());
                insertVulnerability.setString(8, vuln.getCvssConfidentialityImpact());
                insertVulnerability.setString(9, vuln.getCvssIntegrityImpact());
                insertVulnerability.setString(10, vuln.getCvssAvailabilityImpact());
                insertVulnerability.execute();
                try {
                    rs = insertVulnerability.getGeneratedKeys();
                    rs.next();
                    vulnerabilityId = rs.getInt(1);
                } catch (SQLException ex) {
                    final String msg = String.format("Unable to retrieve id for new vulnerability for '%s'",
                            vuln.getName());
                    throw new DatabaseException(msg, ex);
                } finally {
                    DBUtils.closeResultSet(rs);
                }
            }

            PreparedStatement insertReference = getPreparedStatement(INSERT_REFERENCE);
            int countReferences = 0;
            for (Reference r : vuln.getReferences()) {
                insertReference.setInt(1, vulnerabilityId);
                insertReference.setString(2, r.getName());
                insertReference.setString(3, r.getUrl());
                insertReference.setString(4, r.getSource());
                if (isBatchInsertEnabled()) {
                    insertReference.addBatch();
                    countReferences++;
                    if (countReferences % getBatchSize() == 0) {
                        insertReference.executeBatch();
                        insertReference = getPreparedStatement(INSERT_REFERENCE);
                        LOGGER.trace(getLogForBatchInserts(countReferences,
                                "Completed %s batch inserts to references table: %s"));
                        countReferences = 0;
                    } else if (countReferences == vuln.getReferences().size()) {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(getLogForBatchInserts(countReferences,
                                    "Completed %s batch inserts to reference table: %s"));
                        }
                        insertReference.executeBatch();
                        countReferences = 0;
                    }
                } else {
                    insertReference.execute();
                }
            }

            PreparedStatement insertSoftware = getPreparedStatement(INSERT_SOFTWARE);
            int countSoftware = 0;
            for (VulnerableSoftware vulnerableSoftware : vuln.getVulnerableSoftware()) {
                int cpeProductId = 0;
                final PreparedStatement selectCpeId = getPreparedStatement(SELECT_CPE_ID);
                selectCpeId.setString(1, vulnerableSoftware.getName());
                try {
                    rs = selectCpeId.executeQuery();
                    if (rs.next()) {
                        cpeProductId = rs.getInt(1);
                    }
                } catch (SQLException ex) {
                    throw new DatabaseException(
                            "Unable to get primary key for new cpe: " + vulnerableSoftware.getName(), ex);
                } finally {
                    DBUtils.closeResultSet(rs);
                }

                if (cpeProductId == 0) {
                    final PreparedStatement insertCpe = getPreparedStatement(INSERT_CPE);
                    insertCpe.setString(1, vulnerableSoftware.getName());
                    insertCpe.setString(2, vulnerableSoftware.getVendor());
                    insertCpe.setString(3, vulnerableSoftware.getProduct());
                    insertCpe.executeUpdate();
                    cpeProductId = DBUtils.getGeneratedKey(insertCpe);
                }
                if (cpeProductId == 0) {
                    throw new DatabaseException("Unable to retrieve cpeProductId - no data returned");
                }

                insertSoftware.setInt(1, vulnerabilityId);
                insertSoftware.setInt(2, cpeProductId);

                if (vulnerableSoftware.getPreviousVersion() == null) {
                    insertSoftware.setNull(3, java.sql.Types.VARCHAR);
                } else {
                    insertSoftware.setString(3, vulnerableSoftware.getPreviousVersion());
                }
                if (isBatchInsertEnabled()) {
                    insertSoftware.addBatch();
                    countSoftware++;
                    if (countSoftware % getBatchSize() == 0) {
                        executeBatch(vuln, vulnerableSoftware, insertSoftware);
                        insertSoftware = getPreparedStatement(INSERT_SOFTWARE);
                        LOGGER.trace(getLogForBatchInserts(countSoftware,
                                "Completed %s batch inserts software table: %s"));
                        countSoftware = 0;
                    } else if (countSoftware == vuln.getVulnerableSoftware().size()) {
                        if (LOGGER.isTraceEnabled()) {
                            LOGGER.trace(getLogForBatchInserts(countSoftware,
                                    "Completed %s batch inserts software table: %s"));
                            countReferences = 0;
                        }
                        executeBatch(vuln, vulnerableSoftware, insertSoftware);
                    }
                } else {
                    try {
                        insertSoftware.execute();
                    } catch (SQLException ex) {
                        if (ex.getMessage().contains("Duplicate entry")) {
                            final String msg = String.format("Duplicate software key identified in '%s:%s'",
                                    vuln.getName(), vuln.getName());
                            LOGGER.info(msg, ex);
                        } else {
                            throw ex;
                        }
                    }
                }
            }
        } catch (SQLException ex) {
            final String msg = String.format("Error updating '%s'", vuln.getName());
            LOGGER.debug(msg, ex);
            throw new DatabaseException(msg, ex);
        } finally {
            DBUtils.closeResultSet(rs);
        }
    }

    /**
     * Returns the size of the batch.
     *
     * @return the size of the batch
     */
    private int getBatchSize() {
        int max;
        try {
            max = settings.getInt(Settings.KEYS.MAX_BATCH_SIZE);
        } catch (InvalidSettingException pE) {
            max = 1000;
        }
        return max;
    }

    /**
     * Determines whether or not batch insert is enabled.
     *
     * @return <code>true</code> if batch insert is enabled; otherwise
     * <code>false</code>
     */
    private boolean isBatchInsertEnabled() {
        boolean batch = false;
        try {
            batch = settings.getBoolean(Settings.KEYS.ENABLE_BATCH_UPDATES);
        } catch (InvalidSettingException pE) {
            //If there's no configuration, default is to not perform batch inserts
            batch = false;
        }
        return batch;
    }

    /**
     * Generates a logging message for batch inserts.
     *
     * @param pCountReferences the number of batch statements executed
     * @param pFormat a Java String.format string
     * @return the formated string
     */
    private String getLogForBatchInserts(int pCountReferences, String pFormat) {
        return String.format(pFormat, pCountReferences, new Date());
    }

    /**
     * Executes batch inserts of vulnerabilities when property
     * database.batchinsert.maxsize is reached.
     *
     * @param pVulnerability the vulnerability
     * @param pVulnerableSoftware the vulnerable software
     * @param pInsertSoftware the prepared statement to batch execute
     * @throws SQLException thrown when the batch cannot be executed
     */
    private void executeBatch(Vulnerability pVulnerability, VulnerableSoftware pVulnerableSoftware,
            PreparedStatement pInsertSoftware) throws SQLException {
        try {
            pInsertSoftware.executeBatch();
        } catch (SQLException ex) {
            if (ex.getMessage().contains("Duplicate entry")) {
                final String msg = String.format("Duplicate software key identified in '%s:%s'",
                        pVulnerability.getName(), pVulnerableSoftware.getName());
                LOGGER.info(msg, ex);
            } else {
                throw ex;
            }
        }
    }

    /**
     * Checks to see if data exists so that analysis can be performed.
     *
     * @return <code>true</code> if data exists; otherwise <code>false</code>
     */
    public synchronized boolean dataExists() {
        ResultSet rs = null;
        try {
            final PreparedStatement cs = getPreparedStatement(COUNT_CPE);
            rs = cs.executeQuery();
            if (rs.next() && rs.getInt(1) > 0) {
                return true;
            }
        } catch (Exception ex) {
            String dd;
            try {
                dd = settings.getDataDirectory().getAbsolutePath();
            } catch (IOException ex1) {
                dd = settings.getString(Settings.KEYS.DATA_DIRECTORY);
            }
            LOGGER.error("Unable to access the local database.\n\nEnsure that '{}' is a writable directory. "
                    + "If the problem persist try deleting the files in '{}' and running {} again. If the problem continues, please "
                    + "create a log file (see documentation at http://jeremylong.github.io/DependencyCheck/) and open a ticket at "
                    + "https://github.com/jeremylong/DependencyCheck/issues and include the log file.\n\n", dd, dd,
                    settings.getString(Settings.KEYS.APPLICATION_NAME));
            LOGGER.debug("", ex);
        } finally {
            DBUtils.closeResultSet(rs);
        }
        return false;
    }

    /**
     * It is possible that orphaned rows may be generated during database
     * updates. This should be called after all updates have been completed to
     * ensure orphan entries are removed.
     */
    public synchronized void cleanupDatabase() {
        clearCache();
        try {
            final PreparedStatement ps = getPreparedStatement(CLEANUP_ORPHANS);
            if (ps != null) {
                ps.executeUpdate();
            }
        } catch (SQLException ex) {
            LOGGER.error("An unexpected SQL Exception occurred; please see the verbose log for more details.");
            LOGGER.debug("", ex);
        }
    }

    /**
     * Determines if the given identifiedVersion is affected by the given cpeId
     * and previous version flag. A non-null, non-empty string passed to the
     * previous version argument indicates that all previous versions are
     * affected.
     *
     * @param vendor the vendor of the dependency being analyzed
     * @param product the product name of the dependency being analyzed
     * @param vulnerableSoftware a map of the vulnerable software with a boolean
     * indicating if all previous versions are affected
     * @param identifiedVersion the identified version of the dependency being
     * analyzed
     * @return true if the identified version is affected, otherwise false
     */
    protected Entry<String, Boolean> getMatchingSoftware(Map<String, Boolean> vulnerableSoftware, String vendor,
            String product, DependencyVersion identifiedVersion) {

        final boolean isVersionTwoADifferentProduct = "apache".equals(vendor) && "struts".equals(product);

        final Set<String> majorVersionsAffectingAllPrevious = new HashSet<>();
        final boolean matchesAnyPrevious = identifiedVersion == null || "-".equals(identifiedVersion.toString());
        String majorVersionMatch = null;
        for (Entry<String, Boolean> entry : vulnerableSoftware.entrySet()) {
            final DependencyVersion v = parseDependencyVersion(entry.getKey());
            if (v == null || "-".equals(v.toString())) { //all versions
                return entry;
            }
            if (entry.getValue()) {
                if (matchesAnyPrevious) {
                    return entry;
                }
                if (identifiedVersion != null
                        && identifiedVersion.getVersionParts().get(0).equals(v.getVersionParts().get(0))) {
                    majorVersionMatch = v.getVersionParts().get(0);
                }
                majorVersionsAffectingAllPrevious.add(v.getVersionParts().get(0));
            }
        }
        if (matchesAnyPrevious) {
            return null;
        }

        final boolean canSkipVersions = majorVersionMatch != null && majorVersionsAffectingAllPrevious.size() > 1;
        //yes, we are iterating over this twice. The first time we are skipping versions those that affect all versions
        //then later we process those that affect all versions. This could be done with sorting...
        for (Entry<String, Boolean> entry : vulnerableSoftware.entrySet()) {
            if (!entry.getValue()) {
                final DependencyVersion v = parseDependencyVersion(entry.getKey());
                //this can't dereference a null 'majorVersionMatch' as canSkipVersions accounts for this.
                if (canSkipVersions && majorVersionMatch != null
                        && !majorVersionMatch.equals(v.getVersionParts().get(0))) {
                    continue;
                }
                //this can't dereference a null 'identifiedVersion' because if it was null we would have exited
                //in the above loop or just after loop (if matchesAnyPrevious return null).
                if (identifiedVersion != null && identifiedVersion.equals(v)) {
                    return entry;
                }
            }
        }
        for (Entry<String, Boolean> entry : vulnerableSoftware.entrySet()) {
            if (entry.getValue()) {
                final DependencyVersion v = parseDependencyVersion(entry.getKey());
                //this can't dereference a null 'majorVersionMatch' as canSkipVersions accounts for this.
                if (canSkipVersions && majorVersionMatch != null
                        && !majorVersionMatch.equals(v.getVersionParts().get(0))) {
                    continue;
                }
                //this can't dereference a null 'identifiedVersion' because if it was null we would have exited
                //in the above loop or just after loop (if matchesAnyPrevious return null).
                if (entry.getValue() && identifiedVersion != null && identifiedVersion.compareTo(v) <= 0
                        && !(isVersionTwoADifferentProduct && !identifiedVersion.getVersionParts().get(0)
                                .equals(v.getVersionParts().get(0)))) {
                    return entry;
                }
            }
        }
        return null;
    }

    /**
     * Parses the version (including revision) from a CPE identifier. If no
     * version is identified then a '-' is returned.
     *
     * @param cpeStr a cpe identifier
     * @return a dependency version
     */
    private DependencyVersion parseDependencyVersion(String cpeStr) {
        final VulnerableSoftware cpe = new VulnerableSoftware();
        try {
            cpe.parseName(cpeStr);
        } catch (UnsupportedEncodingException ex) {
            //never going to happen.
            LOGGER.trace("", ex);
        }
        return parseDependencyVersion(cpe);
    }

    /**
     * Takes a CPE and parses out the version number. If no version is
     * identified then a '-' is returned.
     *
     * @param cpe a cpe object
     * @return a dependency version
     */
    private DependencyVersion parseDependencyVersion(VulnerableSoftware cpe) {
        final DependencyVersion cpeVersion;
        if (cpe.getVersion() != null && !cpe.getVersion().isEmpty()) {
            String versionText;
            if (cpe.getUpdate() != null && !cpe.getUpdate().isEmpty()) {
                versionText = String.format("%s.%s", cpe.getVersion(), cpe.getUpdate());
            } else {
                versionText = cpe.getVersion();
            }
            if (versionText.contains(".wso2")) {
                versionText = versionText.substring(0, versionText.indexOf(".wso2"));
            }
            cpeVersion = DependencyVersionUtil.parseVersion(versionText, true);
        } else {
            cpeVersion = new DependencyVersion("-");
        }
        return cpeVersion;
    }

    /**
     * This method is only referenced in unused code.
     * <p>
     * Deletes unused dictionary entries from the database.
     * </p>
     */
    public synchronized void deleteUnusedCpe() {
        clearCache();
        PreparedStatement ps = null;
        try {
            ps = connection.prepareStatement(statementBundle.getString("DELETE_UNUSED_DICT_CPE"));
            ps.executeUpdate();
        } catch (SQLException ex) {
            LOGGER.error("Unable to delete CPE dictionary entries", ex);
        } finally {
            DBUtils.closeStatement(ps);
        }
    }

    /**
     * This method is only referenced in unused code and will likely break on
     * MySQL if ever used due to the MERGE statement.
     * <p>
     * Merges CPE entries into the database.
     * </p>
     *
     * @param cpe the CPE identifier
     * @param vendor the CPE vendor
     * @param product the CPE product
     */
    public synchronized void addCpe(String cpe, String vendor, String product) {
        clearCache();
        PreparedStatement ps = null;
        try {
            ps = connection.prepareStatement(statementBundle.getString("ADD_DICT_CPE"));
            ps.setString(1, cpe);
            ps.setString(2, vendor);
            ps.setString(3, product);
            ps.executeUpdate();
        } catch (SQLException ex) {
            LOGGER.error("Unable to add CPE dictionary entry", ex);
        } finally {
            DBUtils.closeStatement(ps);
        }
    }
}