All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.owasp.dependencycheck.analyzer.JarAnalyzer Maven / Gradle / Ivy

/*
 * 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 com.github.packageurl.MalformedPackageURLException;
import com.github.packageurl.PackageURL;
import com.github.packageurl.PackageURLBuilder;
import com.google.common.base.Strings;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.io.IOUtils;
import org.jsoup.Jsoup;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.Identifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.FileUtils;
import org.owasp.dependencycheck.utils.Settings;
import org.owasp.dependencycheck.xml.pom.Developer;
import org.owasp.dependencycheck.xml.pom.License;
import org.owasp.dependencycheck.xml.pom.Model;
import org.owasp.dependencycheck.xml.pom.PomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Used to load a JAR file and collect information that can be used to determine
 * the associated CPE.
 *
 * @author Jeremy Long
 */
public class JarAnalyzer extends AbstractFileTypeAnalyzer {

    //
    /**
     * A descriptor for the type of dependencies processed or added by this
     * analyzer.
     */
    public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVA;
    /**
     * The logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class);
    /**
     * The count of directories created during analysis. This is used for
     * creating temporary directories.
     */
    private static final AtomicInteger DIR_COUNT = new AtomicInteger(0);
    /**
     * The system independent newline character.
     */
    private static final String NEWLINE = System.getProperty("line.separator");
    /**
     * A list of values in the manifest to ignore as they only result in false
     * positives.
     */
    private static final Set IGNORE_VALUES = newHashSet(
            "Sun Java System Application Server");
    /**
     * A list of elements in the manifest to ignore.
     */
    private static final Set IGNORE_KEYS = newHashSet(
            "built-by",
            "created-by",
            "builtby",
            "built-with",
            "builtwith",
            "createdby",
            "build-jdk",
            "buildjdk",
            "ant-version",
            "antversion",
            "dynamicimportpackage",
            "dynamicimport-package",
            "dynamic-importpackage",
            "dynamic-import-package",
            "import-package",
            "ignore-package",
            "export-package",
            "importpackage",
            "import-template",
            "importtemplate",
            "java-vendor",
            "export-template",
            "exporttemplate",
            "ignorepackage",
            "exportpackage",
            "sealed",
            "manifest-version",
            "archiver-version",
            "manifestversion",
            "archiverversion",
            "classpath",
            "class-path",
            "tool",
            "bundle-manifestversion",
            "bundlemanifestversion",
            "bundle-vendor",
            "include-resource",
            "embed-dependency",
            "embedded-artifacts",
            "ipojo-components",
            "ipojo-extension",
            "plugin-dependencies",
            "today",
            "tstamp",
            "dstamp",
            "eclipse-sourcereferences",
            "kotlin-version",
            "require-capability");
    /**
     * Deprecated Jar manifest attribute, that is, nonetheless, useful for
     * analysis.
     */
    @SuppressWarnings("deprecation")
    private static final String IMPLEMENTATION_VENDOR_ID = Attributes.Name.IMPLEMENTATION_VENDOR_ID
            .toString();
    /**
     * item in some manifest, should be considered medium confidence.
     */
    private static final String BUNDLE_VERSION = "Bundle-Version"; //: 2.1.2
    /**
     * item in some manifest, should be considered medium confidence.
     */
    private static final String BUNDLE_DESCRIPTION = "Bundle-Description"; //: Apache Struts 2
    /**
     * item in some manifest, should be considered medium confidence.
     */
    private static final String BUNDLE_NAME = "Bundle-Name"; //: Struts 2 Core
    /**
     * A pattern to detect HTML within text.
     */
    private static final Pattern HTML_DETECTION_PATTERN = Pattern.compile("\\<[a-z]+.*/?\\>", Pattern.CASE_INSENSITIVE);
    /**
     * The name of the analyzer.
     */
    private static final String ANALYZER_NAME = "Jar Analyzer";
    /**
     * The phase that this analyzer is intended to run in.
     */
    private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
    /**
     * The set of jar files to exclude from analysis.
     */
    private static final List EXCLUDE_JARS = Arrays.asList("-doc.jar", "-src.jar", "-javadoc.jar", "-sources.jar");
    /**
     * The set of file extensions supported by this analyzer.
     */
    private static final String[] EXTENSIONS = {"jar", "war", "aar"};
    /**
     * The file filter used to determine which files this analyzer supports.
     */
    private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();

    /**
     * The expected first bytes when reading a zip file.
     */
    private static final byte[] ZIP_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x03, 0x04};

    /**
     * The expected first bytes when reading an empty zip file.
     */
    private static final byte[] ZIP_EMPTY_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x05, 0x06};

    /**
     * The expected first bytes when reading a spanned zip file.
     */
    private static final byte[] ZIP_SPANNED_FIRST_BYTES = new byte[]{0x50, 0x4B, 0x07, 0x08};

    //
    /**
     * The parent directory for the individual directories per archive.
     */
    private File tempFileLocation = null;
    /**
     * Maven group id and artifact ids must match the regex to be considered
     * valid. In some cases ODC cannot interpolate a variable and it produced
     * invalid names.
     */
    private static final String VALID_NAME = "^[A-Za-z0-9_\\-.]+$";

    //
    /**
     * Returns the FileFilter.
     *
     * @return the FileFilter
     */
    @Override
    protected FileFilter getFileFilter() {
        return FILTER;
    }

    /**
     * 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;
    }

    @Override
    public boolean accept(File pathname) {
        final boolean accepted = super.accept(pathname);
        return accepted && !isExcludedJar(pathname);
    }

    /**
     * Returns true if the JAR is a `*-sources.jar` or `*-javadoc.jar`;
     * otherwise false.
     *
     * @param path the path to the dependency
     * @return true if the JAR is a `*-sources.jar` or `*-javadoc.jar`;
     * otherwise false.
     */
    private boolean isExcludedJar(File path) {
        final String fileName = path.getName().toLowerCase();
        return EXCLUDE_JARS.stream().anyMatch(fileName::endsWith);
    }
    //

    /**
     * Returns the key used in the properties file to reference the analyzer's
     * enabled property.
     *
     * @return the analyzer's enabled property setting key
     */
    @Override
    protected String getAnalyzerEnabledSettingKey() {
        return Settings.KEYS.ANALYZER_JAR_ENABLED;
    }

    /**
     * Loads a specified JAR file and collects information from the manifest and
     * checksums to identify the correct CPE information.
     *
     * @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
    public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
        final List classNames = collectClassNames(dependency);
        final String fileName = dependency.getFileName().toLowerCase();
        if ((classNames.isEmpty()
                && (fileName.endsWith("-sources.jar")
                || fileName.endsWith("-javadoc.jar")
                || fileName.endsWith("-src.jar")
                || fileName.endsWith("-doc.jar")
                || isMacOSMetaDataFile(dependency, engine)))
                || !isZipFile(dependency)) {
            engine.removeDependency(dependency);
            return;
        }
        Exception exception = null;
        boolean hasManifest = false;
        try {
            hasManifest = parseManifest(dependency, classNames);
        } catch (IOException ex) {
            LOGGER.debug("Invalid Manifest", ex);
            exception = ex;
        }
        boolean hasPOM = false;
        try {
            hasPOM = analyzePOM(dependency, classNames, engine);
        } catch (AnalysisException ex) {
            LOGGER.debug("Error parsing pom.xml", ex);
            exception = ex;
        }
        final boolean addPackagesAsEvidence = !(hasManifest && hasPOM);
        analyzePackageNames(classNames, dependency, addPackagesAsEvidence);
        dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);

        if (exception != null) {
            throw new AnalysisException(String.format("An error occurred extracting evidence from "
                    + "%s, analysis may be incomplete; please see the log for more details.",
                    dependency.getDisplayFileName()), exception);
        }
    }

    /**
     * Checks if the given dependency appears to be a macOS meta-data file,
     * returning true if its filename starts with a ._ prefix and if there is
     * another dependency with the same filename minus the ._ prefix, otherwise
     * it returns false.
     *
     * @param dependency the dependency to check if it's a macOS meta-data file
     * @param engine the engine that is scanning the dependencies
     * @return whether or not the given dependency appears to be a macOS
     * meta-data file
     */
    @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
            value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
    private boolean isMacOSMetaDataFile(final Dependency dependency, final Engine engine) {
        if (dependency.getActualFilePath() != null) {
            final String fileName = Paths.get(dependency.getActualFilePath()).getFileName().toString();
            return fileName.startsWith("._") && hasDependencyWithFilename(engine.getDependencies(), fileName.substring(2));
        }
        return false;
    }

    /**
     * Iterates through the given list of dependencies and returns true when it
     * finds a dependency with a filename matching the given filename, otherwise
     * returns false.
     *
     * @param dependencies the dependencies to search within
     * @param fileName the filename to search for
     * @return whether or not the given dependencies contain a dependency with
     * the given filename
     */
    @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
            value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
    private boolean hasDependencyWithFilename(final Dependency[] dependencies, final String fileName) {
        for (final Dependency dependency : dependencies) {
            if (dependency.getActualFilePath() != null
                    && Paths.get(dependency.getActualFilePath()).getFileName().toString().equalsIgnoreCase(fileName)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Attempts to read the first bytes of the given dependency (using its
     * actual file path) and returns true if they match the expected first bytes
     * of a zip file, which may be empty or spanned. If they don't match, or if
     * the file could not be read, then it returns false.
     *
     * @param dependency the dependency to check if it's a zip file
     * @return whether or not the given dependency appears to be a zip file from
     * its first bytes
     */
    @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
    private boolean isZipFile(final Dependency dependency) {
        final byte[] buffer = new byte[4];
        try (FileInputStream fileInputStream = new FileInputStream(dependency.getActualFilePath())) {
            if (fileInputStream.read(buffer) > 0
                    && (Arrays.equals(buffer, ZIP_FIRST_BYTES)
                    || Arrays.equals(buffer, ZIP_EMPTY_FIRST_BYTES)
                    || Arrays.equals(buffer, ZIP_SPANNED_FIRST_BYTES))) {
                return true;
            }
        } catch (Exception e) {
            LOGGER.warn("Unable to check if '{}' is a zip file", dependency.getActualFilePath());
            LOGGER.trace("", e);
        }
        return false;
    }

    /**
     * Attempts to find a pom.xml within the JAR file. If found it extracts
     * information and adds it to the evidence. This will attempt to interpolate
     * the strings contained within the pom.properties if one exists.
     *
     * @param dependency the dependency being analyzed
     * @param classes a collection of class name information
     * @param engine the analysis engine, used to add additional dependencies
     * @throws AnalysisException is thrown if there is an exception parsing the
     * pom
     * @return whether or not evidence was added to the dependency
     */
    protected boolean analyzePOM(Dependency dependency, List classes, Engine engine) throws AnalysisException {

        //TODO add breakpoint on groov-all to find out why commons-cli is not added as a new dependency?
        boolean evidenceAdded = false;
        try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
            //check if we are scanning in a repo directory - so the pom is adjacent to the jar
            final String repoPomName = FilenameUtils.removeExtension(dependency.getActualFilePath()) + ".pom";
            final File repoPom = new File(repoPomName);
            if (repoPom.isFile()) {
                final Model pom = PomUtils.readPom(repoPom);
                evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
            }

            final List pomEntries = retrievePomListing(jar);

            for (String path : pomEntries) {
                LOGGER.debug("Reading pom entry: {}", path);
                try {
                    //extract POM to its own directory and add it as its own dependency
                    final Properties pomProperties = retrievePomProperties(path, jar);
                    final File pomFile = extractPom(path, jar);
                    final Model pom = PomUtils.readPom(pomFile);
                    pom.setGAVFromPomDotProperties(pomProperties);
                    pom.processProperties(pomProperties);

                    final String artifactId = new File(path).getParentFile().getName();
                    if (dependency.getActualFile().getName().startsWith(artifactId)) {
                        evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
                    } else {
                        final String displayPath = String.format("%s%s%s",
                                dependency.getFilePath(),
                                File.separator,
                                path);
                        final String displayName = String.format("%s%s%s",
                                dependency.getFileName(),
                                File.separator,
                                path);
                        final Dependency newDependency = new Dependency();
                        newDependency.setActualFilePath(pomFile.getAbsolutePath());
                        newDependency.setFileName(displayName);
                        newDependency.setFilePath(displayPath);
                        newDependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
                        String groupId = pom.getGroupId();
                        String version = pom.getVersion();
                        if (groupId == null) {
                            groupId = pom.getParentGroupId();
                        }
                        if (version == null) {
                            version = pom.getParentVersion();
                        }
                        if (groupId == null) {
                            newDependency.setName(pom.getArtifactId());
                            newDependency.setPackagePath(String.format("%s:%s", pom.getArtifactId(), version));
                        } else {
                            newDependency.setName(String.format("%s:%s", groupId, pom.getArtifactId()));
                            newDependency.setPackagePath(String.format("%s:%s:%s", groupId, pom.getArtifactId(), version));
                        }
                        newDependency.setDisplayFileName(String.format("%s (shaded: %s)",
                                dependency.getDisplayFileName(), newDependency.getPackagePath()));
                        newDependency.setVersion(version);
                        setPomEvidence(newDependency, pom, null, true);
                        if (dependency.getProjectReferences().size() > 0) {
                            newDependency.addAllProjectReferences(dependency.getProjectReferences());
                        }
                        engine.addDependency(newDependency);
                    }
                } catch (AnalysisException ex) {
                    LOGGER.warn("An error occurred while analyzing '{}'.", dependency.getActualFilePath());
                    LOGGER.trace("", ex);
                }
            }
        } catch (IOException ex) {
            LOGGER.warn("Unable to read JarFile '{}'.", dependency.getActualFilePath());
            LOGGER.trace("", ex);
        }
        return evidenceAdded;
    }

    /**
     * Given a path to a pom.xml within a JarFile, this method attempts to load
     * a sibling pom.properties if one exists.
     *
     * @param path the path to the pom.xml within the JarFile
     * @param jar the JarFile to load the pom.properties from
     * @return a Properties object or null if no pom.properties was found
     */
    private Properties retrievePomProperties(String path, final JarFile jar) {
        final Properties pomProperties = new Properties();
        final String propPath = path.substring(0, path.length() - 7) + "pom.properties";
        final ZipEntry propEntry = jar.getEntry(propPath);
        if (propEntry != null) {
            try (Reader reader = new InputStreamReader(jar.getInputStream(propEntry), StandardCharsets.UTF_8)) {
                pomProperties.load(reader);
                LOGGER.debug("Read pom.properties: {}", propPath);
            } catch (UnsupportedEncodingException ex) {
                LOGGER.trace("UTF-8 is not supported", ex);
            } catch (IOException ex) {
                LOGGER.trace("Unable to read the POM properties", ex);
            }
        }
        return pomProperties;
    }

    /**
     * Searches a JarFile for pom.xml entries and returns a listing of these
     * entries.
     *
     * @param jar the JarFile to search
     * @return a list of pom.xml entries
     * @throws IOException thrown if there is an exception reading a JarEntry
     */
    private List retrievePomListing(final JarFile jar) throws IOException {
        final List pomEntries = new ArrayList<>();
        final Enumeration entries = jar.entries();
        while (entries.hasMoreElements()) {
            final JarEntry entry = entries.nextElement();
            final String entryName = new File(entry.getName()).getName().toLowerCase();
            if (!entry.isDirectory() && "pom.xml".equals(entryName)
                    && entry.getName().toUpperCase().startsWith("META-INF")) {
                pomEntries.add(entry.getName());
            }
        }
        return pomEntries;
    }

    /**
     * Retrieves the specified POM from a jar.
     *
     * @param path the path to the pom.xml file within the jar file
     * @param jar the jar file to extract the pom from
     * @return returns the POM file
     * @throws AnalysisException is thrown if there is an exception extracting
     * the file
     */
    private File extractPom(String path, JarFile jar) throws AnalysisException {
        final File tmpDir = getNextTempDirectory();
        final File file = new File(tmpDir, "pom.xml");
        final ZipEntry entry = jar.getEntry(path);
        if (entry == null) {
            throw new AnalysisException(String.format("Pom (%s) does not exist in %s", path, jar.getName()));
        }
        try (InputStream input = jar.getInputStream(entry);
                FileOutputStream fos = new FileOutputStream(file)) {
            IOUtils.copy(input, fos);
        } catch (IOException ex) {
            LOGGER.warn("An error occurred reading '{}' from '{}'.", path, jar.getName());
            LOGGER.error("", ex);
        }
        return file;
    }

    /**
     * Sets evidence from the pom on the supplied dependency.
     *
     * @param dependency the dependency to set data on
     * @param pom the information from the pom
     * @param classes a collection of ClassNameInformation - containing data
     * about the fully qualified class names within the JAR file being analyzed
     * @param isMainPom a flag indicating if this is the primary pom.
     * @return true if there was evidence within the pom that we could use;
     * otherwise false
     */
    public static boolean setPomEvidence(Dependency dependency, Model pom,
            List classes, boolean isMainPom) {
        if (pom == null) {
            return false;
        }
        boolean foundSomething = false;
        boolean addAsIdentifier = true;
        String groupid = intepolationFailCheck(pom.getGroupId());
        String parentGroupId = intepolationFailCheck(pom.getParentGroupId());
        String artifactid = intepolationFailCheck(pom.getArtifactId());
        String parentArtifactId = intepolationFailCheck(pom.getParentArtifactId());
        String version = intepolationFailCheck(pom.getVersion());
        String parentVersion = intepolationFailCheck(pom.getParentVersion());

        if (("org.sonatype.oss".equals(parentGroupId) && "oss-parent".equals(parentArtifactId))
                || ("org.springframework.boot".equals(parentGroupId) && "spring-boot-starter-parent".equals(parentArtifactId))) {
            parentGroupId = null;
            parentArtifactId = null;
            parentVersion = null;
        }

        if ((groupid == null || groupid.isEmpty()) && parentGroupId != null && !parentGroupId.isEmpty()) {
            groupid = parentGroupId;
        }

        final String originalGroupID = groupid;

        if ((artifactid == null || artifactid.isEmpty()) && parentArtifactId != null && !parentArtifactId.isEmpty()) {
            artifactid = parentArtifactId;
        }

        final String originalArtifactID = artifactid;
        if (artifactid != null && (artifactid.startsWith("org.") || artifactid.startsWith("com."))) {
            artifactid = artifactid.substring(4);
        }

        if ((version == null || version.isEmpty()) && parentVersion != null && !parentVersion.isEmpty()) {
            version = parentVersion;
        }

        if (isMainPom && dependency.getName() == null && originalArtifactID != null && !originalArtifactID.isEmpty()) {
            if (originalGroupID != null && !originalGroupID.isEmpty()) {
                dependency.setName(String.format("%s:%s", originalGroupID, originalArtifactID));
            } else {
                dependency.setName(originalArtifactID);
            }
        }
        if (isMainPom && dependency.getVersion() == null && version != null && !version.isEmpty()) {
            dependency.setVersion(version);
        }

        if (groupid != null && !groupid.isEmpty()) {
            foundSomething = true;
            dependency.addEvidence(EvidenceType.VENDOR, "pom", "groupid", groupid, Confidence.HIGHEST);
            //In several cases we are seeing the product name at the end of the group identifier.
            // This may cause several FP on products that have a collection of dependencies (e.g. jetty).
            //dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.LOW);
            dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.HIGHEST);
            addMatchingValues(classes, groupid, dependency, EvidenceType.VENDOR);
            addMatchingValues(classes, groupid, dependency, EvidenceType.PRODUCT);
            if (parentGroupId != null && !parentGroupId.isEmpty() && !parentGroupId.equals(groupid)) {
                dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
                //see note above for groupid
                //dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.LOW);
                dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
                addMatchingValues(classes, parentGroupId, dependency, EvidenceType.VENDOR);
                addMatchingValues(classes, parentGroupId, dependency, EvidenceType.PRODUCT);
            }
        } else {
            addAsIdentifier = false;
        }

        if (artifactid != null && !artifactid.isEmpty()) {
            foundSomething = true;
            dependency.addEvidence(EvidenceType.PRODUCT, "pom", "artifactid", artifactid, Confidence.HIGHEST);
            dependency.addEvidence(EvidenceType.VENDOR, "pom", "artifactid", artifactid, Confidence.LOW);
            addMatchingValues(classes, artifactid, dependency, EvidenceType.VENDOR);
            addMatchingValues(classes, artifactid, dependency, EvidenceType.PRODUCT);
            if (parentArtifactId != null && !parentArtifactId.isEmpty() && !parentArtifactId.equals(artifactid)) {
                dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-artifactid", parentArtifactId, Confidence.MEDIUM);
                dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-artifactid", parentArtifactId, Confidence.LOW);
                addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.VENDOR);
                addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.PRODUCT);
            }
        } else {
            addAsIdentifier = false;
        }

        if (version != null && !version.isEmpty()) {
            foundSomething = true;
            dependency.addEvidence(EvidenceType.VERSION, "pom", "version", version, Confidence.HIGHEST);
            if (parentVersion != null && !parentVersion.isEmpty() && !parentVersion.equals(version)) {
                dependency.addEvidence(EvidenceType.VERSION, "pom", "parent-version", version, Confidence.LOW);
            }
        } else {
            addAsIdentifier = false;
        }

        if (addAsIdentifier && isMainPom) {
            Identifier id = null;
            try {
                if (originalArtifactID != null && originalArtifactID.matches(VALID_NAME)
                        && originalGroupID != null && originalGroupID.matches(VALID_NAME)) {
                    final PackageURL purl = PackageURLBuilder.aPackageURL().withType("maven").withNamespace(originalGroupID)
                            .withName(originalArtifactID).withVersion(version).build();
                    id = new PurlIdentifier(purl, Confidence.HIGH);
                } else {
                    LOGGER.debug("Invalid maven identifier identified: " + originalGroupID + ":" + originalArtifactID);
                }
            } catch (MalformedPackageURLException ex) {
                final String gav = String.format("%s:%s:%s", originalGroupID, originalArtifactID, version);
                LOGGER.debug("Error building package url for " + gav + "; using generic identifier instead.", ex);
                id = new GenericIdentifier("maven:" + gav, Confidence.HIGH);
            }
            if (id != null) {
                dependency.addSoftwareIdentifier(id);
            }
        }

        // org name
        final String org = pom.getOrganization();
        if (org != null && !org.isEmpty()) {
            dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization name", org, Confidence.HIGH);
            dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization name", org, Confidence.LOW);
            addMatchingValues(classes, org, dependency, EvidenceType.VENDOR);
            addMatchingValues(classes, org, dependency, EvidenceType.PRODUCT);
        }
        // org name
        String orgUrl = pom.getOrganizationUrl();
        if (orgUrl != null && !orgUrl.isEmpty()) {
            if (orgUrl.startsWith("https://github.com/") || orgUrl.startsWith("https://gitlab.com/")) {
                orgUrl = orgUrl.substring(19);
                dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", orgUrl, Confidence.HIGH);
            } else {
                dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization url", orgUrl, Confidence.LOW);
            }
            dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization url", orgUrl, Confidence.MEDIUM);
        }
        //pom name
        final String pomName = pom.getName();
        if (pomName != null && !pomName.isEmpty() && !"${project.groupId}:${project.artifactId}".equals(pomName)) {
            foundSomething = true;
            dependency.addEvidence(EvidenceType.PRODUCT, "pom", "name", pomName, Confidence.HIGH);
            dependency.addEvidence(EvidenceType.VENDOR, "pom", "name", pomName, Confidence.HIGH);
            addMatchingValues(classes, pomName, dependency, EvidenceType.VENDOR);
            addMatchingValues(classes, pomName, dependency, EvidenceType.PRODUCT);
        }

        //Description
        final String description = pom.getDescription();
        if (description != null && !description.isEmpty()
                && !description.startsWith("POM was created by")
                && !description.startsWith("Sonatype helps open source projects")
                && !description.endsWith("project for Spring Boot")) {
            foundSomething = true;
            final String trimmedDescription = addDescription(dependency, description, "pom", "description");
            addMatchingValues(classes, trimmedDescription, dependency, EvidenceType.VENDOR);
            addMatchingValues(classes, trimmedDescription, dependency, EvidenceType.PRODUCT);
        }

        String projectURL = pom.getProjectURL();
        if (projectURL != null && !projectURL.trim().isEmpty()) {
            if (projectURL.startsWith("https://github.com/") || projectURL.startsWith("https://gitlab.com/")) {
                projectURL = projectURL.substring(19);
                dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", projectURL, Confidence.HIGH);
            } else {
                dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", projectURL, Confidence.MEDIUM);
            }
            dependency.addEvidence(EvidenceType.VENDOR, "pom", "url", projectURL, Confidence.HIGHEST);

        }

        if (pom.getDevelopers() != null && !pom.getDevelopers().isEmpty()) {
            for (Developer dev : pom.getDevelopers()) {
                if (!Strings.isNullOrEmpty(dev.getId())) {
                    dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer id", dev.getId(), Confidence.MEDIUM);
                    dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer id", dev.getId(), Confidence.LOW);
                }
                if (!Strings.isNullOrEmpty(dev.getName())) {
                    dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer name", dev.getName(), Confidence.MEDIUM);
                    dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer name", dev.getName(), Confidence.LOW);
                }
                if (!Strings.isNullOrEmpty(dev.getEmail())) {
                    dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer email", dev.getEmail(), Confidence.LOW);
                    dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer email", dev.getEmail(), Confidence.LOW);
                }
                if (!Strings.isNullOrEmpty(dev.getOrganizationUrl())) {
                    dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.MEDIUM);
                    dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.LOW);
                }
                final String devOrg = dev.getOrganization();
                if (!Strings.isNullOrEmpty(devOrg)) {
                    dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org", devOrg, Confidence.MEDIUM);
                    dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org", devOrg, Confidence.LOW);
                    addMatchingValues(classes, devOrg, dependency, EvidenceType.VENDOR);
                    addMatchingValues(classes, devOrg, dependency, EvidenceType.PRODUCT);
                }
            }
        }

        extractLicense(pom, dependency);
        return foundSomething;
    }

    /**
     * Analyzes the path information of the classes contained within the
     * JarAnalyzer to try and determine possible vendor or product names. If any
     * are found they are stored in the packageVendor and packageProduct
     * hashSets.
     *
     * @param classNames a list of class names
     * @param dependency a dependency to analyze
     * @param addPackagesAsEvidence a flag indicating whether or not package
     * names should be added as evidence.
     */
    protected void analyzePackageNames(List classNames,
            Dependency dependency, boolean addPackagesAsEvidence) {
        final Map vendorIdentifiers = new HashMap<>();
        final Map productIdentifiers = new HashMap<>();
        analyzeFullyQualifiedClassNames(classNames, vendorIdentifiers, productIdentifiers);

        final int classCount = classNames.size();

        vendorIdentifiers.forEach((key, value) -> {
            final float ratio = value / (float) classCount;
            if (ratio > 0.5) {
                //TODO remove weighting?
                dependency.addVendorWeighting(key);
                if (addPackagesAsEvidence && key.length() > 1) {
                    dependency.addEvidence(EvidenceType.VENDOR, "jar", "package name", key, Confidence.LOW);
                }
            }
        });
        productIdentifiers.forEach((key, value) -> {
            final float ratio = value / (float) classCount;
            if (ratio > 0.5) {
                //todo remove weighting
                dependency.addProductWeighting(key);
                if (addPackagesAsEvidence && key.length() > 1) {
                    dependency.addEvidence(EvidenceType.PRODUCT, "jar", "package name", key, Confidence.LOW);
                }
            }
        });
    }

    /**
     * 

* Reads the manifest from the JAR file and collects the entries. Some * vendorKey entries are:

*
  • Implementation Title
  • *
  • Implementation Version
  • Implementation Vendor
  • *
  • Implementation VendorId
  • Bundle Name
  • Bundle * Version
  • Bundle Vendor
  • Bundle Description
  • Main * Class
* However, all but a handful of specific entries are read in. * * @param dependency A reference to the dependency * @param classInformation a collection of class information * @return whether evidence was identified parsing the manifest * @throws IOException if there is an issue reading the JAR file */ //CSOFF: MethodLength protected boolean parseManifest(Dependency dependency, List classInformation) throws IOException { boolean foundSomething = false; try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) { final Manifest manifest = jar.getManifest(); if (manifest == null) { if (!dependency.getFileName().toLowerCase().endsWith("-sources.jar") && !dependency.getFileName().toLowerCase().endsWith("-javadoc.jar") && !dependency.getFileName().toLowerCase().endsWith("-src.jar") && !dependency.getFileName().toLowerCase().endsWith("-doc.jar")) { LOGGER.debug("Jar file '{}' does not contain a manifest.", dependency.getFileName()); } return false; } String source = "Manifest"; String specificationVersion = null; boolean hasImplementationVersion = false; Attributes atts = manifest.getMainAttributes(); for (Entry entry : atts.entrySet()) { String key = entry.getKey().toString(); String value = atts.getValue(key); if (HTML_DETECTION_PATTERN.matcher(value).find()) { value = Jsoup.parse(value).text(); } if (value.startsWith("[email protected]:") || value.startsWith("[email protected]:")) { value = value.substring(15); } if (IGNORE_VALUES.contains(value)) { continue; } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) { foundSomething = true; dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.HIGH); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) { hasImplementationVersion = true; foundSomething = true; dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH); } else if ("specification-version".equalsIgnoreCase(key)) { specificationVersion = value; } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) { foundSomething = true; dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.HIGH); addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR); } else if (key.equalsIgnoreCase(IMPLEMENTATION_VENDOR_ID)) { foundSomething = true; dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR); } else if (key.equalsIgnoreCase(BUNDLE_DESCRIPTION)) { if (!value.startsWith("Sonatype helps open source projects")) { foundSomething = true; addDescription(dependency, value, "manifest", key); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); } } else if (key.equalsIgnoreCase(BUNDLE_NAME)) { foundSomething = true; dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); // //the following caused false positives. // } else if (key.equalsIgnoreCase(BUNDLE_VENDOR)) { } else if (key.equalsIgnoreCase(BUNDLE_VERSION)) { foundSomething = true; dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH); } else if (key.equalsIgnoreCase(Attributes.Name.MAIN_CLASS.toString())) { //noinspection UnnecessaryContinue continue; //skipping main class as if this has important information to add it will be added during class name analysis... } else if ("implementation-url".equalsIgnoreCase(key) && value != null && value.startsWith("https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/parent/")) { continue; } else { key = key.toLowerCase(); if (!IGNORE_KEYS.contains(key) && !key.endsWith("jdk") && !key.contains("lastmodified") && !key.endsWith("package") && !key.endsWith("classpath") && !key.endsWith("class-path") && !key.endsWith("-scm") //todo change this to a regex? && !key.startsWith("scm-") && !value.trim().startsWith("scm:") && !isImportPackage(key, value) && !isPackage(key, value)) { foundSomething = true; if (key.contains("version")) { if (!key.contains("specification")) { dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM); } } else if ("build-id".equals(key)) { int pos = value.indexOf('('); if (pos > 0) { value = value.substring(0, pos - 1); } pos = value.indexOf('['); if (pos > 0) { value = value.substring(0, pos - 1); } dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM); } else if (key.contains("title")) { dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); } else if (key.contains("vendor")) { if (key.contains("specification")) { dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW); } else { dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR); } } else if (key.contains("name")) { dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM); dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); } else if (key.contains("license")) { addLicense(dependency, value); } else if (key.contains("description")) { if (!value.startsWith("Sonatype helps open source projects")) { final String trimmedDescription = addDescription(dependency, value, "manifest", key); addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.VENDOR); addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.PRODUCT); } } else { dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.LOW); dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW); addMatchingValues(classInformation, value, dependency, EvidenceType.VERSION); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); if (value.matches(".*\\d.*")) { final StringTokenizer tokenizer = new StringTokenizer(value, " "); while (tokenizer.hasMoreElements()) { final String s = tokenizer.nextToken(); if (s.matches("^[0-9.]+$")) { dependency.addEvidence(EvidenceType.VERSION, source, key, s, Confidence.LOW); } } } } } } } for (Map.Entry item : manifest.getEntries().entrySet()) { final String name = item.getKey(); source = "manifest: " + name; atts = item.getValue(); for (Entry entry : atts.entrySet()) { final String key = entry.getKey().toString(); final String value = atts.getValue(key); if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) { foundSomething = true; dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) { foundSomething = true; dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM); } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) { foundSomething = true; dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR); } else if (key.equalsIgnoreCase(Attributes.Name.SPECIFICATION_TITLE.toString())) { foundSomething = true; dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM); addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT); } } } if (specificationVersion != null && !hasImplementationVersion) { foundSomething = true; dependency.addEvidence(EvidenceType.VERSION, source, "specification-version", specificationVersion, Confidence.HIGH); } } return foundSomething; } //CSON: MethodLength /** * Adds a description to the given dependency. If the description contains * one of the following strings beyond 100 characters, then the description * used will be trimmed to that position: *
  • "such as"
  • "like "
  • "will use "
  • "* uses * "
* * @param dependency a dependency * @param description the description * @param source the source of the evidence * @param key the "name" of the evidence * @return if the description is trimmed, the trimmed version is returned; * otherwise the original description is returned */ public static String addDescription(Dependency dependency, String description, String source, String key) { if (dependency.getDescription() == null) { dependency.setDescription(description); } String desc; if (HTML_DETECTION_PATTERN.matcher(description).find()) { desc = Jsoup.parse(description).text(); } else { desc = description; } dependency.setDescription(desc); if (desc.length() > 100) { desc = desc.replaceAll("\\s\\s+", " "); final int posSuchAs = desc.toLowerCase().indexOf("such as ", 100); final int posLike = desc.toLowerCase().indexOf("like ", 100); final int posWillUse = desc.toLowerCase().indexOf("will use ", 100); final int posUses = desc.toLowerCase().indexOf(" uses ", 100); int pos = -1; pos = Math.max(pos, posSuchAs); if (pos >= 0 && posLike >= 0) { pos = Math.min(pos, posLike); } else { pos = Math.max(pos, posLike); } if (pos >= 0 && posWillUse >= 0) { pos = Math.min(pos, posWillUse); } else { pos = Math.max(pos, posWillUse); } if (pos >= 0 && posUses >= 0) { pos = Math.min(pos, posUses); } else { pos = Math.max(pos, posUses); } if (pos > 0) { desc = desc.substring(0, pos) + "..."; } // //no longer add description directly. Use matching terms in other parts of the evidence collection // //but description adds too many FP // dependency.addEvidence(EvidenceType.PRODUCT, source, key, desc, Confidence.LOW); // dependency.addEvidence(EvidenceType.VENDOR, source, key, desc, Confidence.LOW); // } else { // dependency.addEvidence(EvidenceType.PRODUCT, source, key, desc, Confidence.MEDIUM); // dependency.addEvidence(EvidenceType.VENDOR, source, key, desc, Confidence.MEDIUM); } return desc; } /** * Adds a license to the given dependency. * * @param d a dependency * @param license the license */ private void addLicense(Dependency d, String license) { if (d.getLicense() == null) { d.setLicense(license); } else if (!d.getLicense().contains(license)) { d.setLicense(d.getLicense() + NEWLINE + license); } } /** * Initializes the JarAnalyzer. * * @param engine a reference to the dependency-check engine * @throws InitializationException is thrown if there is an exception * creating a temporary directory */ @Override public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException { try { final File baseDir = getSettings().getTempDirectory(); tempFileLocation = File.createTempFile("check", "tmp", baseDir); if (!tempFileLocation.delete()) { final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath()); setEnabled(false); throw new InitializationException(msg); } if (!tempFileLocation.mkdirs()) { final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath()); setEnabled(false); throw new InitializationException(msg); } } catch (IOException ex) { setEnabled(false); throw new InitializationException("Unable to create a temporary file", ex); } } /** * Deletes any files extracted from the JAR during analysis. */ @Override public void closeAnalyzer() { if (tempFileLocation != null && tempFileLocation.exists()) { LOGGER.debug("Attempting to delete temporary files from `{}`", tempFileLocation.toString()); final boolean success = FileUtils.delete(tempFileLocation); if (!success && tempFileLocation.exists()) { final String[] l = tempFileLocation.list(); if (l != null && l.length > 0) { LOGGER.warn("Failed to delete the JAR Analyzder's temporary files from `{}`, " + "see the log for more details", tempFileLocation.getAbsolutePath()); } } } } /** * Determines if the key value pair from the manifest is for an "import" * type entry for package names. * * @param key the key from the manifest * @param value the value from the manifest * @return true or false depending on if it is believed the entry is an * "import" entry */ private boolean isImportPackage(String key, String value) { final Pattern packageRx = Pattern.compile("^(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*[,;])+(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*)?$"); final boolean matches = packageRx.matcher(value).matches(); return matches && (key.contains("import") || key.contains("include") || value.length() > 10); } /** * Cycles through an enumeration of JarEntries, contained within the * dependency, and returns a list of the class names. This does not include * core Java package names (i.e. java.* or javax.*). * * @param dependency the dependency being analyzed * @return an list of fully qualified class names */ protected List collectClassNames(Dependency dependency) { final List classNames = new ArrayList<>(); try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) { final Enumeration entries = jar.entries(); while (entries.hasMoreElements()) { final JarEntry entry = entries.nextElement(); final String name = entry.getName().toLowerCase(); //no longer stripping "|com\\.sun" - there are some com.sun jar files with CVEs. if (name.endsWith(".class") && !name.matches("^javax?\\..*$")) { final ClassNameInformation className = new ClassNameInformation(name.substring(0, name.length() - 6)); classNames.add(className); } } } catch (IOException ex) { LOGGER.warn("Unable to open jar file '{}'.", dependency.getFileName()); LOGGER.debug("", ex); } return classNames; } /** * Cycles through the list of class names and places the package levels 0-3 * into the provided maps for vendor and product. This is helpful when * analyzing vendor/product as many times this is included in the package * name. * * @param classNames a list of class names * @param vendor HashMap of possible vendor names from package names (e.g. * owasp) * @param product HashMap of possible product names from package names (e.g. * dependencycheck) */ private void analyzeFullyQualifiedClassNames(List classNames, Map vendor, Map product) { for (ClassNameInformation entry : classNames) { final List list = entry.getPackageStructure(); addEntry(vendor, list.get(0)); if (list.size() == 2) { addEntry(product, list.get(1)); } else if (list.size() == 3) { addEntry(vendor, list.get(1)); addEntry(product, list.get(1)); addEntry(product, list.get(2)); } else if (list.size() >= 4) { addEntry(vendor, list.get(1)); addEntry(vendor, list.get(2)); addEntry(product, list.get(1)); addEntry(product, list.get(2)); addEntry(product, list.get(3)); } } } /** * Adds an entry to the specified collection and sets the Integer (e.g. the * count) to 1. If the entry already exists in the collection then the * Integer is incremented by 1. * * @param collection a collection of strings and their occurrence count * @param key the key to add to the collection */ private void addEntry(Map collection, String key) { if (collection.containsKey(key)) { collection.put(key, collection.get(key) + 1); } else { collection.put(key, 1); } } /** * Cycles through the collection of class name information to see if parts * of the package names are contained in the provided value. If found, it * will be added as the HIGHEST confidence evidence because we have more * then one source corroborating the value. * * @param classes a collection of class name information * @param value the value to check to see if it contains a package name * @param dep the dependency to add new entries too * @param type the type of evidence (vendor, product, or version) */ protected static void addMatchingValues(List classes, String value, Dependency dep, EvidenceType type) { if (value == null || value.isEmpty() || classes == null || classes.isEmpty()) { return; } final HashSet tested = new HashSet<>(); //TODO add a hashSet and only analyze any given key once. for (ClassNameInformation cni : classes) { //classes.forEach((cni) -> { for (String key : cni.getPackageStructure()) { //cni.getPackageStructure().forEach((key) -> { if (!tested.contains(key)) { tested.add(key); final int pos = StringUtils.indexOfIgnoreCase(value, key); if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length() && !Character.isLetterOrDigit(value.charAt(key.length()))))) || (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1)) && (pos + key.length() == value.length() || (key.length() < value.length() && !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) { dep.addEvidence(type, "jar", "package name", key, Confidence.HIGHEST); } } } } } /** * Simple check to see if the attribute from a manifest is just a package * name. * * @param key the key of the value to check * @param value the value to check * @return true if the value looks like a java package name, otherwise false */ private boolean isPackage(String key, String value) { return !key.matches(".*(version|title|vendor|name|license|description).*") && value.matches("^[a-zA-Z_][a-zA-Z0-9_\\$]*\\.([a-zA-Z_][a-zA-Z0-9_\\$]*\\.)*([a-zA-Z_][a-zA-Z0-9_\\$]*)$"); } /** * Returns null if the value starts with `${` and ends with `}`. * * @param value the value to check * @return the correct value which may be null */ private static String intepolationFailCheck(String value) { if (value != null && value.contains("${")) { return null; } return value; } /** * Extracts the license information from the pom and adds it to the * dependency. * * @param pom the pom object * @param dependency the dependency to add license information too */ public static void extractLicense(Model pom, Dependency dependency) { //license if (pom.getLicenses() != null) { StringBuilder license = null; for (License lic : pom.getLicenses()) { String tmp = null; if (lic.getName() != null) { tmp = lic.getName(); } if (lic.getUrl() != null) { if (tmp == null) { tmp = lic.getUrl(); } else { tmp += ": " + lic.getUrl(); } } if (tmp == null) { continue; } if (HTML_DETECTION_PATTERN.matcher(tmp).find()) { tmp = Jsoup.parse(tmp).text(); } if (license == null) { license = new StringBuilder(tmp); } else { license.append("\n").append(tmp); } } if (license != null) { dependency.setLicense(license.toString()); } } } /** * Stores information about a class name. */ protected static class ClassNameInformation { /** * The fully qualified class name. */ private String name; /** * Up to the first four levels of the package structure, excluding a * leading "org" or "com". */ private final ArrayList packageStructure = new ArrayList<>(); /** *

* Stores information about a given class name. This class will keep the * fully qualified class name and a list of the important parts of the * package structure. Up to the first four levels of the package * structure are stored, excluding a leading "org" or "com". * Example:

* ClassNameInformation obj = new ClassNameInformation("org/owasp/dependencycheck/analyzer/JarAnalyzer"); * System.out.println(obj.getName()); * for (String p : obj.getPackageStructure()) * System.out.println(p); * *

* Would result in:

* org.owasp.dependencycheck.analyzer.JarAnalyzer * owasp * dependencycheck * analyzer * jaranalyzer * * @param className a fully qualified class name */ ClassNameInformation(String className) { name = className; if (name.contains("/")) { final String[] tmp = StringUtils.split(className.toLowerCase(), '/'); int start = 0; int end = 3; if ("com".equals(tmp[0]) || "org".equals(tmp[0])) { start = 1; end = 4; } if (tmp.length <= end) { end = tmp.length - 1; } packageStructure.addAll(Arrays.asList(tmp).subList(start, end + 1)); } else { packageStructure.add(name); } } /** * Get the value of name * * @return the value of name */ public String getName() { return name; } /** * Set the value of name * * @param name new value of name */ public void setName(String name) { this.name = name; } /** * Get the value of packageStructure * * @return the value of packageStructure */ public ArrayList getPackageStructure() { return packageStructure; } } /** * Retrieves the next temporary directory to extract an archive too. * * @return a directory * @throws AnalysisException thrown if unable to create temporary directory */ private File getNextTempDirectory() throws AnalysisException { final int dirCount = DIR_COUNT.incrementAndGet(); final File directory = new File(tempFileLocation, String.valueOf(dirCount)); //getting an exception for some directories not being able to be created; might be because the directory already exists? if (directory.exists()) { return getNextTempDirectory(); } if (!directory.mkdirs()) { final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath()); throw new AnalysisException(msg); } return directory; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy