org.owasp.dependencytrack.tasks.dependencycheck.DependencyCheckAnalysis.java Source code

Java tutorial

Introduction

Here is the source code for org.owasp.dependencytrack.tasks.dependencycheck.DependencyCheckAnalysis.java

Source

/*
 * This file is part of Dependency-Track.
 *
 * Dependency-Track is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any
 * later version.
 *
 * Dependency-Track is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License along with
 * Dependency-Track. If not, see http://www.gnu.org/licenses/.
 */
package org.owasp.dependencytrack.tasks.dependencycheck;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.codehaus.staxmate.SMInputFactory;
import org.codehaus.staxmate.in.SMHierarchicCursor;
import org.codehaus.staxmate.in.SMInputCursor;
import org.hibernate.Query;
import org.hibernate.Session;
import org.owasp.dependencycheck.agent.DependencyCheckScanAgent;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.exception.ScanAgentException;
import org.owasp.dependencycheck.reporting.ReportGenerator;
import org.owasp.dependencycheck.utils.FileUtils;
import org.owasp.dependencytrack.dao.BaseDao;
import org.owasp.dependencytrack.model.Library;
import org.owasp.dependencytrack.model.LibraryVersion;
import org.owasp.dependencytrack.model.License;
import org.owasp.dependencytrack.model.ScanResult;
import org.owasp.dependencytrack.model.Vulnerability;
import org.owasp.dependencytrack.service.VulnerabilityService;
import org.owasp.dependencytrack.tasks.DependencyCheckAnalysisRequestEvent;
import org.owasp.dependencytrack.util.XmlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.xml.sax.SAXException;

import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Performs a Dependency-Check analysis.
 *
 * @author Steve Springett (steve.springett@owasp.org)
 */
@Service
public class DependencyCheckAnalysis extends BaseDao
        implements ApplicationListener<DependencyCheckAnalysisRequestEvent> {

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

    @Value("${app.data.dir}")
    private String appDataDir;

    @Value("${app.dir}")
    private String appDir;

    @Value("${app.suppression.path}")
    private String appSuppressionPath;

    @Autowired
    private VulnerabilityService vulnerabilityService;

    private Session session;

    private AtomicBoolean inProgress = new AtomicBoolean(false);

    public DependencyCheckAnalysis() {
    }

    /**
     * {@inheritDoc}
     */
    public void onApplicationEvent(DependencyCheckAnalysisRequestEvent event) {
        if (!inProgress.get()) {
            try {
                inProgress.set(true);
                LOGGER.info("Starting Dependency-Check analysis");
                this.session = getSession();
                final List<LibraryVersion> libraryVersions = event.getLibraryVersions();
                if (libraryVersions == null || libraryVersions.size() == 0) {
                    execute();
                } else {
                    execute(libraryVersions);
                }
                vulnerabilityService.updateLibraryVersionVulnerabilityCount();
                vulnerabilityService.updateApplicationVersionVulnerabilityCount();
                cleanup();
            } finally {
                inProgress.set(false);
            }
        }
    }

    /**
     * Performs a scan against all libraries in the database.
     */
    @Transactional
    public synchronized void execute() {
        // Retrieve a list of all library versions defined in the system
        final Query query = session.createQuery("from LibraryVersion");
        @SuppressWarnings("unchecked")
        final List<LibraryVersion> libraryVersions = query.list();

        execute(libraryVersions);
    }

    /**
     * Performs a scan against the specified list of libraries.
     * @param libraryVersions a list of LibraryVersion object to perform a scan against
     */
    public synchronized void execute(List<LibraryVersion> libraryVersions) {
        if (performAnalysis(libraryVersions)) {
            try {
                analyzeResults();
            } catch (SAXException | IOException e) {
                LOGGER.error("An error occurred while analyzing Dependency-Check results: " + e.getMessage());
            }
        }
    }

    /**
     * Performs a Dependency-Check analysis against the specified list of libraries.
     * @param libraryVersions a list of LibraryVersion object to perform a scan against
     * @return true if scan was successful, false if scan failed for some reason
     */
    private synchronized boolean performAnalysis(List<LibraryVersion> libraryVersions) {
        LOGGER.info("Executing Dependency-Check Task");

        // iterate through the libraries, create evidence and create the resulting dependency
        final List<Dependency> dependencies = new ArrayList<>();
        for (LibraryVersion libraryVersion : libraryVersions) {
            final Library library = libraryVersion.getLibrary();
            final Dependency dependency = new Dependency(new File(FileUtils.getBitBucket()));

            if (StringUtils.isNotBlank(libraryVersion.getMd5())) {
                dependency.setMd5sum(libraryVersion.getMd5());
            } else {
                dependency.setMd5sum(libraryVersion.getUuidAsMd5Hash());
            }

            if (StringUtils.isNotBlank(libraryVersion.getSha1())) {
                dependency.setSha1sum(libraryVersion.getSha1());
            } else {
                dependency.setSha1sum(libraryVersion.getUuidAsSha1Hash());
            }

            final License license = library.getLicense();
            if (license != null) {
                dependency.setLicense(library.getLicense().getLicensename());
            }
            dependency.setDescription(String.valueOf(libraryVersion.getId()));
            dependency.getVendorEvidence().addEvidence("dependency-track", "vendor",
                    library.getLibraryVendor().getVendor(), Confidence.HIGHEST);
            dependency.getProductEvidence().addEvidence("dependency-track", "name", library.getLibraryname(),
                    Confidence.HIGHEST);
            dependency.getVersionEvidence().addEvidence("dependency-track", "version",
                    libraryVersion.getLibraryversion(), Confidence.HIGHEST);
            dependencies.add(dependency);
        }

        LOGGER.info("Performing Dependency-Check analysis against " + dependencies.size() + " component(s)");

        final DependencyCheckScanAgent scanAgent = new DependencyCheckScanAgent();
        scanAgent.setConnectionString("jdbc:h2:file:%s;FILE_LOCK=SERIALIZED;AUTOCOMMIT=ON;");
        scanAgent.setDataDirectory(appDataDir);
        scanAgent.setReportOutputDirectory(appDir);
        scanAgent.setReportFormat(ReportGenerator.Format.ALL);
        scanAgent.setAutoUpdate(true);
        scanAgent.setDependencies(dependencies);
        scanAgent.setCentralAnalyzerEnabled(false);
        scanAgent.setNexusAnalyzerEnabled(false);

        // If a global suppression file exists, use it.
        final File suppressions = new File(appSuppressionPath);
        if (suppressions.exists() && suppressions.isFile()) {
            scanAgent.setSuppressionFile(suppressions.getAbsolutePath());
        }

        boolean success = false;
        try {
            scanAgent.execute();
            success = true;
        } catch (ScanAgentException e) {
            LOGGER.error("An error occurred executing Dependency-Check scan agent: " + e.getMessage());
        }

        LOGGER.info("Dependency-Check analysis complete");
        return success;
    }

    /**
     * Analyzes the result (dependency-check-report.xml) from a Dependency-Check scan.
     * @throws SAXException if it's not able to parse the XML report
     * @throws IOException if it's not able to open the XML report
     */
    private synchronized void analyzeResults() throws SAXException, IOException {
        final SMInputFactory inputFactory = XmlUtil.newStaxParser();
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(new File(appDir + File.separator + "dependency-check-report.xml"));
            final SMHierarchicCursor rootC = inputFactory.rootElementCursor(fis);
            rootC.advance(); // <analysis>
            final SMInputCursor cursor = rootC.childCursor();
            while (cursor.getNext() != null) {
                final String nodeName = cursor.getLocalName();
                if ("dependencies".equals(nodeName)) {
                    processDependencies(cursor);
                }
            }
        } catch (XMLStreamException e) {
            throw new IllegalStateException("XML is not valid", e);
        } finally {
            IOUtils.closeQuietly(fis);
        }
    }

    /**
     * Processes a node of dependency tags.
     * @param depC a SMInputCursor cursor
     * @throws XMLStreamException if exception is thrown
     */
    private void processDependencies(SMInputCursor depC) throws XMLStreamException {
        final SMInputCursor cursor = depC.childElementCursor("dependency");
        while (cursor.getNext() != null) {
            processDependency(cursor);
        }
    }

    /**
     * Processes a single dependency node.
     * @param depC a SMInputCursor cursor
     * @throws XMLStreamException if exception is thrown
     */
    private void processDependency(SMInputCursor depC) throws XMLStreamException {
        int libraryVersionId = -1;
        List<String[]> matchedCpes = Collections.emptyList();
        final SMInputCursor childCursor = depC.childCursor();
        while (childCursor.getNext() != null) {
            final String nodeName = childCursor.getLocalName();
            if ("description".equals(nodeName)) {
                libraryVersionId = Integer.valueOf(StringUtils.trim(childCursor.collectDescendantText(false)));
            } else if ("identifiers".equals(nodeName)) {
                matchedCpes = processIdentifiers(childCursor);
            } else if ("vulnerabilities".equals(nodeName)) {
                processVulnerabilities(childCursor, libraryVersionId, matchedCpes);
            }
        }
    }

    /**
     * Processes a node of identifier tags.
     * @param identsC a SMInputCursor cursor
     * @throws XMLStreamException if exception is thrown
     */
    private List<String[]> processIdentifiers(SMInputCursor identsC) throws XMLStreamException {
        final ArrayList<String[]> identifiers = new ArrayList<>();
        final SMInputCursor cursor = identsC.childElementCursor("identifier");
        while (cursor.getNext() != null) {
            final String[] identifier = processIdentifier(cursor);
            identifiers.add(identifier);
        }
        return identifiers;
    }

    /**
     * Processes a single identifier node.
     * @param identC a SMInputCursor cursor
     * @throws XMLStreamException if exception is thrown
     */
    private String[] processIdentifier(SMInputCursor identC) throws XMLStreamException {
        final String[] values = new String[2];
        final SMInputCursor childCursor = identC.childCursor();
        while (childCursor.getNext() != null) {
            final String nodeName = childCursor.getLocalName();
            if ("name".equals(nodeName)) {
                String value = StringUtils.trim(childCursor.collectDescendantText(false));
                if (value != null && value.startsWith("(") && value.endsWith(")")) {
                    value = value.substring(1, value.length() - 1);
                }
                values[0] = value;
            } else if ("url".equals(nodeName)) {
                values[1] = StringUtils.trim(childCursor.collectDescendantText(false));
            }
        }
        return values;
    }

    /**
     * Processes a node vulnerability tags.
     * @param vulnC a SMInputCursor cursor
     * @param libraryVersionId the library version identifier associated with this collection of vulnerabilities
     * @param matchedCpes a list of CPEs that the dependency was matched against
     * @throws XMLStreamException if exception is thrown
     */
    private void processVulnerabilities(SMInputCursor vulnC, int libraryVersionId, List<String[]> matchedCpes)
            throws XMLStreamException {
        final SMInputCursor cursor = vulnC.childElementCursor("vulnerability");
        while (cursor.getNext() != null) {
            processVulnerability(cursor, libraryVersionId, matchedCpes);
        }
    }

    /**
     * Processes a single dependency node.
     * @param vulnC a SMInputCursor cursor
     * @param libraryVersionId the library version identifier associated with this vulnerability
     * @param matchedCpes a list of CPEs that the dependency was matched against
     * @throws XMLStreamException if exception is thrown
     */
    private void processVulnerability(SMInputCursor vulnC, int libraryVersionId, List<String[]> matchedCpes)
            throws XMLStreamException {
        String name = null, cvssScore = null, cwe = null, description = null;
        final SMInputCursor childCursor = vulnC.childCursor();
        while (childCursor.getNext() != null) {
            final String nodeName = childCursor.getLocalName();
            if ("name".equals(nodeName)) {
                name = StringUtils.trim(childCursor.collectDescendantText(false));
            } else if ("cvssScore".equals(nodeName)) {
                cvssScore = StringUtils.trim(childCursor.collectDescendantText(false));
            } else if ("cwe".equals(nodeName)) {
                cwe = StringUtils.trim(childCursor.collectDescendantText(false));
            } else if ("description".equals(nodeName)) {
                description = StringUtils.trim(childCursor.collectDescendantText(false));
            }
        }
        final Vulnerability vulnerability = getVulnerability(name);
        vulnerability.setName(name);
        vulnerability.setCvssScore(new BigDecimal(cvssScore));
        vulnerability.setCwe(cwe);
        vulnerability.setDescription(description);
        if (matchedCpes.size() > 0) {
            vulnerability.setMatchedCPE(matchedCpes.get(0)[0]);
        }
        commitVulnerabilityData(vulnerability, libraryVersionId);
    }

    /**
     * Given an Analysis object (a result from a Dependency-Check scan), this method
     * will commit the results of the scan to the database.
     * @param vulnerability the newly created vulnerability to commit
     * @param libraryVersionId the unique identifier of the library version associated with the vulnerability
     */
    @Transactional
    private void commitVulnerabilityData(final Vulnerability vulnerability, final int libraryVersionId) {
        final Query query = session.createQuery("FROM LibraryVersion WHERE id=:id");
        query.setParameter("id", libraryVersionId);
        final LibraryVersion libraryVersion = (LibraryVersion) query.uniqueResult();

        if (libraryVersion == null) {
            return;
        }

        // Check if it's an existing vulnerability
        if (vulnerability.getId() != null) {
            final Query scanQuery = session.createQuery(
                    "from ScanResult s where s.vulnerability=:vulnerability and s.scanDate=:scanDate and s.libraryVersion=:libraryVersion");
            scanQuery.setParameter("vulnerability", vulnerability);
            scanQuery.setParameter("scanDate", new Date());
            scanQuery.setParameter("libraryVersion", libraryVersion);
            if (scanQuery.list().size() > 0) {
                return; // Don't add the same entry more than once
            }
        }

        session.save(vulnerability);
        final ScanResult scan = new ScanResult();
        scan.setScanDate(new Date());
        scan.setLibraryVersion(libraryVersion);
        scan.setVulnerability(vulnerability);
        session.save(scan);
    }

    /**
     * Queries the database for a given CVE
     * @param name the name of the vulnerability (typically a CVE identifier)
     * @return a Vulnerability object
     */
    private Vulnerability getVulnerability(final String name) {
        final Query query = session.createQuery("from Vulnerability where name=:name order by id asc");
        query.setParameter("name", name);
        @SuppressWarnings("unchecked")
        final List<Vulnerability> vulns = query.list();
        if (vulns.size() > 0) {
            return vulns.get(0);
        }
        return new Vulnerability();
    }

}