FalsePositiveAnalyzer.java

/*
 * 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.analyzer;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.FileFilter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.concurrent.ThreadSafe;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.Evidence;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.CpeIdentifier;
import org.owasp.dependencycheck.dependency.naming.Identifier;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import us.springett.parsers.cpe.Cpe;
import us.springett.parsers.cpe.CpeBuilder;
import us.springett.parsers.cpe.exceptions.CpeValidationException;
import us.springett.parsers.cpe.values.Part;

/**
 * This analyzer attempts to remove some well known false positives -
 * specifically regarding the java runtime.
 *
 * @author Jeremy Long
 */
@ThreadSafe
public class FalsePositiveAnalyzer extends AbstractAnalyzer {

    /**
     * The Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(FalsePositiveAnalyzer.class);
    /**
     * The file filter used to find DLL and EXE.
     */
    private static final FileFilter DLL_EXE_FILTER = FileFilterBuilder.newInstance().addExtensions("dll", "exe").build();
    /**
     * Regex to identify core java libraries and a few other commonly
     * misidentified ones.
     */
    public static final Pattern CORE_JAVA = Pattern.compile("^cpe:/a:(sun|oracle|ibm):(j2[ems]e|"
            + "java(_platform_micro_edition|_runtime_environment|_se|virtual_machine|se_development_kit|fx)?|"
            + "jdk|jre|jsse)($|:.*)");
    /**
     * Regex to identify core jsf libraries.
     */
    public static final Pattern CORE_JAVA_JSF = Pattern.compile("^cpe:/a:(sun|oracle|ibm):jsf($|:.*)");
    /**
     * Regex to identify core java library files. This is currently incomplete.
     */
    public static final Pattern CORE_FILES = Pattern.compile("(^|/)((alt[-])?rt|jsse|jfxrt|jfr|jce|javaws|deploy|charsets)\\.jar$");
    /**
     * Regex to identify core jsf java library files. This is currently
     * incomplete.
     */
    public static final Pattern CORE_JSF_FILES = Pattern.compile("(^|/)jsf[-][^/]*\\.jar$");

    //<editor-fold defaultstate="collapsed" desc="All standard implementation details of Analyzer">
    /**
     * The name of the analyzer.
     */
    private static final String ANALYZER_NAME = "False Positive Analyzer";
    /**
     * The phase that this analyzer is intended to run in.
     */
    private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_IDENTIFIER_ANALYSIS;

    /**
     * Returns the name of the analyzer.
     *
     * @return the name of the analyzer.
     */
    @Override
    public String getName() {
        return ANALYZER_NAME;
    }

    /**
     * Returns the phase that the analyzer is intended to run in.
     *
     * @return the phase that the analyzer is intended to run in.
     */
    @Override
    public AnalysisPhase getAnalysisPhase() {
        return ANALYSIS_PHASE;
    }

    /**
     * <p>
     * Returns the setting key to determine if the analyzer is enabled.</p>
     *
     * @return the key for the analyzer's enabled property
     */
    @Override
    protected String getAnalyzerEnabledSettingKey() {
        return Settings.KEYS.ANALYZER_FALSE_POSITIVE_ENABLED;
    }
    //</editor-fold>

    /**
     * Analyzes the dependencies and removes bad/incorrect CPE associations
     * based on various heuristics.
     *
     * @param dependency the dependency to analyze.
     * @param engine the engine that is scanning the dependencies
     * @throws AnalysisException is thrown if there is an error reading the JAR
     * file.
     */
    @Override
    protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
        removeJreEntries(dependency);
        removeBadMatches(dependency);
        removeWrongVersionMatches(dependency);
        removeSpuriousCPE(dependency);
        removeDuplicativeEntriesFromJar(dependency, engine);
        addFalseNegativeCPEs(dependency);
    }

    /**
     * <p>
     * Intended to remove spurious CPE entries. By spurious we mean duplicate,
     * less specific CPE entries.</p>
     * <p>
     * Example:</p>
     * <code>
     * cpe:/a:some-vendor:some-product
     * cpe:/a:some-vendor:some-product:1.5
     * cpe:/a:some-vendor:some-product:1.5.2
     * </code>
     * <p>
     * Should be trimmed to:</p>
     * <code>
     * cpe:/a:some-vendor:some-product:1.5.2
     * </code>
     *
     * @param dependency the dependency being analyzed
     */
    //CSOFF: NestedIfDepth
    @SuppressWarnings("null")
    @SuppressFBWarnings(justification = "null checks are working correctly to prevent NPE", value = {"NP_NULL_ON_SOME_PATH_MIGHT_BE_INFEASIBLE"})
    private void removeSpuriousCPE(Dependency dependency) {
        final List<Identifier> ids = new ArrayList<>(dependency.getVulnerableSoftwareIdentifiers());
        Collections.sort(ids);
        final ListIterator<Identifier> mainItr = ids.listIterator();
        while (mainItr.hasNext()) {
            final Identifier temp = mainItr.next();
            if (temp instanceof CpeIdentifier) {
                final CpeIdentifier currentId = (CpeIdentifier) temp;
                final Cpe currentCpe = currentId.getCpe();
                final ListIterator<Identifier> subItr = ids.listIterator(mainItr.nextIndex());
                while (subItr.hasNext()) {
                    final Identifier nextId = subItr.next();
                    if (nextId instanceof CpeIdentifier) {
                        final CpeIdentifier nextCpeId = (CpeIdentifier) nextId;
                        final Cpe nextCpe = nextCpeId.getCpe();
                        //TODO fix the version problem below
                        if (currentCpe.getVendor().equals(nextCpe.getVendor())) {
                            if (currentCpe.getProduct().equals(nextCpe.getProduct())) {
                                // see if one is contained in the other.. remove the contained one from dependency.getIdentifier
                                final String currentVersion = currentCpe.getVersion();
                                final String nextVersion = nextCpe.getVersion();
                                if (currentVersion == null && nextVersion == null) {
                                    //how did we get here?
                                    LOGGER.debug("currentVersion and nextVersion are both null?");
                                } else if (currentVersion == null && nextVersion != null) {
                                    dependency.removeVulnerableSoftwareIdentifier(currentId);
                                } else if (nextVersion == null && currentVersion != null) {
                                    dependency.removeVulnerableSoftwareIdentifier(nextId);
                                } else if (currentVersion.length() < nextVersion.length()) {
                                    if (nextVersion.startsWith(currentVersion) || "-".equals(currentVersion)) {
                                        dependency.removeVulnerableSoftwareIdentifier(currentId);
                                    }
                                } else if (currentVersion.startsWith(nextVersion) || "-".equals(nextVersion)) {
                                    dependency.removeVulnerableSoftwareIdentifier(nextId);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    //CSON: NestedIfDepth

    /**
     * Removes any CPE entries for the JDK/JRE unless the filename ends with
     * rt.jar
     *
     * @param dependency the dependency to remove JRE CPEs from
     */
    private void removeJreEntries(Dependency dependency) {
        final Set<Identifier> removalSet = new HashSet<>();
        dependency.getVulnerableSoftwareIdentifiers().forEach(i -> {
            final Matcher coreCPE = CORE_JAVA.matcher(i.getValue());
            final Matcher coreFiles = CORE_FILES.matcher(dependency.getFileName());
            final Matcher coreJsfCPE = CORE_JAVA_JSF.matcher(i.getValue());
            final Matcher coreJsfFiles = CORE_JSF_FILES.matcher(dependency.getFileName());
            if ((coreCPE.matches() && !coreFiles.matches())
                    || (coreJsfCPE.matches() && !coreJsfFiles.matches())) {
                removalSet.add(i);
            }

        });
        removalSet.forEach(dependency::removeVulnerableSoftwareIdentifier);
    }

    /**
     * Removes bad CPE matches for a dependency. Unfortunately, right now these
     * are hard-coded patches for specific problems identified when testing this
     * on a LARGE volume of jar files.
     *
     * @param dependency the dependency to analyze
     */
    protected void removeBadMatches(Dependency dependency) {

        final Set<Identifier> toRemove = new HashSet<>();
        /* TODO - can we utilize the pom's groupid and artifactId to filter??? most of
         * these are due to low quality data.  Other idea would be to say any CPE
         * found based on LOW confidence evidence should have a different CPE type? (this
         * might be a better solution then just removing the URL for "best-guess" matches).
         */
        //Set<Evidence> groupId = dependency.getVendorEvidence().getEvidence("pom", "groupid");
        //Set<Evidence> artifactId = dependency.getVendorEvidence().getEvidence("pom", "artifactid");
        for (Identifier i : dependency.getVulnerableSoftwareIdentifiers()) {
            //TODO move this startsWith expression to the base suppression file
            if (i instanceof CpeIdentifier) {
                final CpeIdentifier cpeId = (CpeIdentifier) i;
                final Cpe cpe = cpeId.getCpe();
                if ((cpe.getProduct().matches(".*c\\+\\+.*")
                        || ("file".equals(cpe.getVendor()) && "file".equals(cpe.getProduct()))
                        || ("mozilla".equals(cpe.getVendor()) && "mozilla".equals(cpe.getProduct()))
                        || ("cvs".equals(cpe.getVendor()) && "cvs".equals(cpe.getProduct()))
                        || ("ftp".equals(cpe.getVendor()) && "ftp".equals(cpe.getProduct()))
                        || ("tcp".equals(cpe.getVendor()) && "tcp".equals(cpe.getProduct()))
                        || ("ssh".equals(cpe.getVendor()) && "ssh".equals(cpe.getProduct()))
                        || ("lookup".equals(cpe.getVendor()) && "lookup".equals(cpe.getProduct())))
                        && (dependency.getFileName().toLowerCase().endsWith(".jar")
                        || dependency.getFileName().toLowerCase().endsWith("pom.xml")
                        || dependency.getFileName().toLowerCase().endsWith(".dll")
                        || dependency.getFileName().toLowerCase().endsWith(".exe")
                        || dependency.getFileName().toLowerCase().endsWith(".nuspec")
                        || dependency.getFileName().toLowerCase().endsWith(".zip")
                        || dependency.getFileName().toLowerCase().endsWith(".sar")
                        || dependency.getFileName().toLowerCase().endsWith(".apk")
                        || dependency.getFileName().toLowerCase().endsWith(".tar")
                        || dependency.getFileName().toLowerCase().endsWith(".gz")
                        || dependency.getFileName().toLowerCase().endsWith(".tgz")
                        || dependency.getFileName().toLowerCase().endsWith(".rpm")
                        || dependency.getFileName().toLowerCase().endsWith(".ear")
                        || dependency.getFileName().toLowerCase().endsWith(".war"))) {
                    toRemove.add(i);
                } else if ((("jquery".equals(cpe.getVendor()) && "jquery".equals(cpe.getProduct()))
                        || ("prototypejs".equals(cpe.getVendor()) && "prototype".equals(cpe.getProduct()))
                        || ("yahoo".equals(cpe.getVendor()) && "yui".equals(cpe.getProduct())))
                        && (dependency.getFileName().toLowerCase().endsWith(".jar")
                        || dependency.getFileName().toLowerCase().endsWith("pom.xml")
                        || dependency.getFileName().toLowerCase().endsWith(".dll")
                        || dependency.getFileName().toLowerCase().endsWith(".exe"))) {
                    toRemove.add(i);
                } else if ((("microsoft".equals(cpe.getVendor()) && "excel".equals(cpe.getProduct()))
                        || ("microsoft".equals(cpe.getVendor()) && "word".equals(cpe.getProduct()))
                        || ("microsoft".equals(cpe.getVendor()) && "visio".equals(cpe.getProduct()))
                        || ("microsoft".equals(cpe.getVendor()) && "powerpoint".equals(cpe.getProduct()))
                        || ("microsoft".equals(cpe.getVendor()) && "office".equals(cpe.getProduct()))
                        || ("core_ftp".equals(cpe.getVendor()) && "core_ftp".equals(cpe.getProduct())))
                        && (dependency.getFileName().toLowerCase().endsWith(".jar")
                        || dependency.getFileName().toLowerCase().endsWith(".ear")
                        || dependency.getFileName().toLowerCase().endsWith(".war")
                        || dependency.getFileName().toLowerCase().endsWith("pom.xml"))) {
                    toRemove.add(i);
                } else if (("apache".equals(cpe.getVendor()) && "maven".equals(cpe.getProduct()))
                        && !dependency.getFileName().toLowerCase().matches("maven-core-[\\d.]+\\.jar")) {
                    toRemove.add(i);
                } else if (("m-core".equals(cpe.getVendor()) && "m-core".equals(cpe.getProduct()))) {
                    boolean found = false;
                    for (Evidence e : dependency.getEvidence(EvidenceType.PRODUCT)) {
                        if ("m-core".equalsIgnoreCase(e.getValue())) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        for (Evidence e : dependency.getEvidence(EvidenceType.VENDOR)) {
                            if ("m-core".equalsIgnoreCase(e.getValue())) {
                                found = true;
                                break;
                            }
                        }
                    }
                    if (!found) {
                        toRemove.add(i);
                    }
                } else if (("jboss".equals(cpe.getVendor()) && "jboss".equals(cpe.getProduct()))
                        && !dependency.getFileName().toLowerCase().matches("jboss-?[\\d.-]+(GA)?\\.jar")) {
                    toRemove.add(i);
                } else if ("java-websocket_project".equals(cpe.getVendor())
                        && "java-websocket".equals(cpe.getProduct())) {
                    boolean found = false;
                    for (Identifier si : dependency.getSoftwareIdentifiers()) {
                        if (si.getValue().toLowerCase().contains("org.java-websocket/java-websocket")) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        toRemove.add(i);
                    }
                }
            }
        }
        toRemove.forEach(dependency::removeVulnerableSoftwareIdentifier);
    }

    /**
     * Removes CPE matches for the wrong version of a dependency. Currently,
     * this only covers Axis 1 & 2.
     *
     * @param dependency the dependency to analyze
     */
    private void removeWrongVersionMatches(Dependency dependency) {
        final Set<Identifier> identifiersToRemove = new HashSet<>();
        final String fileName = dependency.getFileName();
        if (fileName != null && fileName.contains("axis2")) {
            dependency.getVulnerableSoftwareIdentifiers().stream()
                    .filter((i) -> (i instanceof CpeIdentifier))
                    .map(i -> (CpeIdentifier) i)
                    .forEach((i) -> {
                        final Cpe cpe = i.getCpe();
                        if ("apache".equals(cpe.getVendor()) && "axis".equals(cpe.getProduct())) {
                            identifiersToRemove.add(i);
                        }
                    });
        } else if (fileName != null && fileName.contains("axis")) {
            dependency.getVulnerableSoftwareIdentifiers().stream()
                    .filter((i) -> (i instanceof CpeIdentifier))
                    .map(i -> (CpeIdentifier) i)
                    .forEach((i) -> {
                        final Cpe cpe = i.getCpe();
                        if ("apache".equals(cpe.getVendor()) && "axis2".equals(cpe.getProduct())) {
                            identifiersToRemove.add(i);
                        }
                    });
        }
        identifiersToRemove.forEach(dependency::removeVulnerableSoftwareIdentifier);
    }

    /**
     * There are some known CPE entries, specifically regarding sun and oracle
     * products due to the acquisition and changes in product names, that based
     * on given evidence we can add the related CPE entries to ensure a complete
     * list of CVE entries.
     *
     * @param dependency the dependency being analyzed
     */
    @SuppressWarnings("UnnecessaryParentheses")
    private void addFalseNegativeCPEs(Dependency dependency) {
        final CpeBuilder builder = new CpeBuilder();
        //TODO move this to the hint analyzer
        final List<Identifier> identifiersToAdd = new ArrayList<>();
        dependency.getVulnerableSoftwareIdentifiers().stream()
                .filter((i) -> (i instanceof CpeIdentifier))
                .map(i -> (CpeIdentifier) i)
                .forEach((i) -> {
                    final Cpe cpe = i.getCpe();
                    if ((("oracle".equals(cpe.getVendor())
                            && ("opensso".equals(cpe.getProduct()) || "opensso_enterprise".equals(cpe.getProduct()))))
                            || ("sun".equals(cpe.getVendor())
                            && ("opensso".equals(cpe.getProduct()) || "opensso_enterprise".equals(cpe.getProduct())))) {

                        try {
                            final Cpe newCpe1 = builder.part(Part.APPLICATION).vendor("sun")
                                    .product("opensso_enterprise").version(cpe.getVersion()).build();
                            final Cpe newCpe2 = builder.part(Part.APPLICATION).vendor("oracle")
                                    .product("opensso_enterprise").version(cpe.getVersion()).build();
                            final Cpe newCpe3 = builder.part(Part.APPLICATION).vendor("sun")
                                    .product("opensso").version(cpe.getVersion()).build();
                            final Cpe newCpe4 = builder.part(Part.APPLICATION).vendor("oracle")
                                    .product("opensso").version(cpe.getVersion()).build();
                            final CpeIdentifier newCpeId1 = new CpeIdentifier(newCpe1, i.getConfidence());
                            final CpeIdentifier newCpeId2 = new CpeIdentifier(newCpe2, i.getConfidence());
                            final CpeIdentifier newCpeId3 = new CpeIdentifier(newCpe3, i.getConfidence());
                            final CpeIdentifier newCpeId4 = new CpeIdentifier(newCpe4, i.getConfidence());
                            identifiersToAdd.add(newCpeId1);
                            identifiersToAdd.add(newCpeId2);
                            identifiersToAdd.add(newCpeId3);
                            identifiersToAdd.add(newCpeId4);

                        } catch (CpeValidationException ex) {
                            LOGGER.warn("Unable to add oracle and sun CPEs", ex);
                        }
                    }
                    if ("apache".equals(cpe.getVendor()) && "santuario_xml_security_for_java".equals(cpe.getProduct())) {
                        try {
                            final Cpe newCpe1 = builder.part(Part.APPLICATION).vendor("apache")
                                    .product("xml_security_for_java").version(cpe.getVersion()).build();
                            final CpeIdentifier newCpeId1 = new CpeIdentifier(newCpe1, i.getConfidence());
                            identifiersToAdd.add(newCpeId1);
                        } catch (CpeValidationException ex) {
                            LOGGER.warn("Unable to add apache xml_security_for_java CPE", ex);
                        }
                    }
                });
        identifiersToAdd.forEach(dependency::addVulnerableSoftwareIdentifier);
    }

    /**
     * Removes duplicate entries identified that are contained within JAR files.
     * These occasionally crop up due to POM entries or other types of files
     * (such as DLLs and EXEs) being contained within the JAR.
     *
     * @param dependency the dependency that might be a duplicate
     * @param engine the engine used to scan all dependencies
     */
    private synchronized void removeDuplicativeEntriesFromJar(Dependency dependency, Engine engine) {
        //Believed to be code that should have been removed several versions ago. This logic
        // incorreclty removes dependencies such as more than half the pom entries in pax-web-jetty-bundle-6.0.7.jar
//        if (dependency.getFileName().toLowerCase().endsWith("pom.xml")
//                || DLL_EXE_FILTER.accept(dependency.getActualFile())) {
//            String parentPath = dependency.getFilePath().toLowerCase();
//            if (parentPath.contains(".jar")) {
//                parentPath = parentPath.substring(0, parentPath.indexOf(".jar") + 4);
//                final Dependency[] dependencies = engine.getDependencies();
//                final Dependency parent = findDependency(parentPath, dependencies);
//                if (parent != null) {
//                    final boolean remove = dependency.getVulnerableSoftwareIdentifiers().stream()
//                            .filter((i) -> (i instanceof CpeIdentifier))
//                            .map(i -> (CpeIdentifier) i)
//                            .anyMatch(i -> parent.getVulnerableSoftwareIdentifiers().stream()
//                                    .filter((p) -> (p instanceof CpeIdentifier))
//                                    .map(p -> (CpeIdentifier) p)
//                                    .anyMatch(p -> !p.equals(i)
//                                    && p.getCpe().getPart().equals(i.getCpe().getPart())
//                                    && p.getCpe().getVendor().equals(i.getCpe().getVendor())
//                                    && p.getCpe().getProduct().equals(i.getCpe().getProduct())));
//                    if (remove) {
//                        engine.removeDependency(dependency);
//                    }
//                }
//            }
//        }
    }

    /**
     * Retrieves a given dependency, based on a given path, from a list of
     * dependencies.
     *
     * @param dependencyPath the path of the dependency to return
     * @param dependencies the array of dependencies to search
     * @return the dependency object for the given path, otherwise null
     */
    private Dependency findDependency(String dependencyPath, Dependency[] dependencies) {
        for (Dependency d : dependencies) {
            if (d.getFilePath().equalsIgnoreCase(dependencyPath)) {
                return d;
            }
        }
        return null;
    }
}