JarAnalyzer.java

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

  19. import com.github.packageurl.MalformedPackageURLException;
  20. import com.github.packageurl.PackageURL;
  21. import com.github.packageurl.PackageURLBuilder;
  22. import com.google.common.base.Strings;
  23. import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
  24. import java.io.File;
  25. import java.io.FileFilter;
  26. import java.io.FileInputStream;
  27. import java.io.FileOutputStream;
  28. import java.io.IOException;
  29. import java.io.InputStream;
  30. import java.io.InputStreamReader;
  31. import java.io.Reader;
  32. import java.io.UnsupportedEncodingException;
  33. import java.nio.charset.StandardCharsets;
  34. import java.nio.file.Paths;
  35. import java.util.ArrayList;
  36. import java.util.Arrays;
  37. import java.util.Enumeration;
  38. import java.util.HashMap;
  39. import java.util.HashSet;
  40. import java.util.List;
  41. import java.util.Map;
  42. import java.util.Map.Entry;
  43. import java.util.Properties;
  44. import java.util.Set;
  45. import java.util.StringTokenizer;
  46. import java.util.concurrent.atomic.AtomicInteger;
  47. import java.util.jar.Attributes;
  48. import java.util.jar.JarEntry;
  49. import java.util.jar.JarFile;
  50. import java.util.jar.Manifest;
  51. import java.util.regex.Pattern;
  52. import java.util.zip.ZipEntry;

  53. import org.apache.commons.io.FilenameUtils;
  54. import org.apache.commons.lang3.StringUtils;
  55. import org.apache.commons.io.IOUtils;
  56. import org.jsoup.Jsoup;
  57. import org.owasp.dependencycheck.Engine;
  58. import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
  59. import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
  60. import org.owasp.dependencycheck.dependency.Confidence;
  61. import org.owasp.dependencycheck.dependency.Dependency;
  62. import org.owasp.dependencycheck.dependency.EvidenceType;
  63. import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
  64. import org.owasp.dependencycheck.dependency.naming.Identifier;
  65. import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
  66. import org.owasp.dependencycheck.exception.InitializationException;
  67. import org.owasp.dependencycheck.utils.FileFilterBuilder;
  68. import org.owasp.dependencycheck.utils.FileUtils;
  69. import org.owasp.dependencycheck.utils.Settings;
  70. import org.owasp.dependencycheck.xml.pom.Developer;
  71. import org.owasp.dependencycheck.xml.pom.License;
  72. import org.owasp.dependencycheck.xml.pom.Model;
  73. import org.owasp.dependencycheck.xml.pom.PomUtils;
  74. import org.slf4j.Logger;
  75. import org.slf4j.LoggerFactory;

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

  83.     //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
  84.     /**
  85.      * A descriptor for the type of dependencies processed or added by this
  86.      * analyzer.
  87.      */
  88.     public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.JAVA;
  89.     /**
  90.      * The logger.
  91.      */
  92.     private static final Logger LOGGER = LoggerFactory.getLogger(JarAnalyzer.class);
  93.     /**
  94.      * The count of directories created during analysis. This is used for
  95.      * creating temporary directories.
  96.      */
  97.     private static final AtomicInteger DIR_COUNT = new AtomicInteger(0);
  98.     /**
  99.      * The system independent newline character.
  100.      */
  101.     private static final String NEWLINE = System.getProperty("line.separator");
  102.     /**
  103.      * A list of values in the manifest to ignore as they only result in false
  104.      * positives.
  105.      */
  106.     private static final Set<String> IGNORE_VALUES = newHashSet(
  107.             "Sun Java System Application Server");
  108.     /**
  109.      * A list of elements in the manifest to ignore.
  110.      */
  111.     private static final Set<String> IGNORE_KEYS = newHashSet(
  112.             "built-by",
  113.             "created-by",
  114.             "builtby",
  115.             "built-with",
  116.             "builtwith",
  117.             "createdby",
  118.             "build-jdk",
  119.             "buildjdk",
  120.             "ant-version",
  121.             "antversion",
  122.             "dynamicimportpackage",
  123.             "dynamicimport-package",
  124.             "dynamic-importpackage",
  125.             "dynamic-import-package",
  126.             "import-package",
  127.             "ignore-package",
  128.             "export-package",
  129.             "importpackage",
  130.             "import-template",
  131.             "importtemplate",
  132.             "java-vendor",
  133.             "export-template",
  134.             "exporttemplate",
  135.             "ignorepackage",
  136.             "exportpackage",
  137.             "sealed",
  138.             "manifest-version",
  139.             "archiver-version",
  140.             "manifestversion",
  141.             "archiverversion",
  142.             "classpath",
  143.             "class-path",
  144.             "tool",
  145.             "bundle-manifestversion",
  146.             "bundlemanifestversion",
  147.             "bundle-vendor",
  148.             "include-resource",
  149.             "embed-dependency",
  150.             "embedded-artifacts",
  151.             "ipojo-components",
  152.             "ipojo-extension",
  153.             "plugin-dependencies",
  154.             "today",
  155.             "tstamp",
  156.             "dstamp",
  157.             "eclipse-sourcereferences",
  158.             "kotlin-version",
  159.             "require-capability");
  160.     /**
  161.      * Deprecated Jar manifest attribute, that is, nonetheless, useful for
  162.      * analysis.
  163.      */
  164.     @SuppressWarnings("deprecation")
  165.     private static final String IMPLEMENTATION_VENDOR_ID = Attributes.Name.IMPLEMENTATION_VENDOR_ID
  166.             .toString();
  167.     /**
  168.      * item in some manifest, should be considered medium confidence.
  169.      */
  170.     private static final String BUNDLE_VERSION = "Bundle-Version"; //: 2.1.2
  171.     /**
  172.      * item in some manifest, should be considered medium confidence.
  173.      */
  174.     private static final String BUNDLE_DESCRIPTION = "Bundle-Description"; //: Apache Struts 2
  175.     /**
  176.      * item in some manifest, should be considered medium confidence.
  177.      */
  178.     private static final String BUNDLE_NAME = "Bundle-Name"; //: Struts 2 Core
  179.     /**
  180.      * A pattern to detect HTML within text.
  181.      */
  182.     private static final Pattern HTML_DETECTION_PATTERN = Pattern.compile("\\<[a-z]+.*/?\\>", Pattern.CASE_INSENSITIVE);
  183.     /**
  184.      * The name of the analyzer.
  185.      */
  186.     private static final String ANALYZER_NAME = "Jar Analyzer";
  187.     /**
  188.      * The phase that this analyzer is intended to run in.
  189.      */
  190.     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
  191.     /**
  192.      * The set of jar files to exclude from analysis.
  193.      */
  194.     private static final List<String> EXCLUDE_JARS = Arrays.asList("-doc.jar", "-src.jar", "-javadoc.jar", "-sources.jar");
  195.     /**
  196.      * The set of file extensions supported by this analyzer.
  197.      */
  198.     private static final String[] EXTENSIONS = {"jar", "war", "aar"};
  199.     /**
  200.      * The file filter used to determine which files this analyzer supports.
  201.      */
  202.     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();

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

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

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

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

  226.     //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
  227.     /**
  228.      * Returns the FileFilter.
  229.      *
  230.      * @return the FileFilter
  231.      */
  232.     @Override
  233.     protected FileFilter getFileFilter() {
  234.         return FILTER;
  235.     }

  236.     /**
  237.      * Returns the name of the analyzer.
  238.      *
  239.      * @return the name of the analyzer.
  240.      */
  241.     @Override
  242.     public String getName() {
  243.         return ANALYZER_NAME;
  244.     }

  245.     /**
  246.      * Returns the phase that the analyzer is intended to run in.
  247.      *
  248.      * @return the phase that the analyzer is intended to run in.
  249.      */
  250.     @Override
  251.     public AnalysisPhase getAnalysisPhase() {
  252.         return ANALYSIS_PHASE;
  253.     }

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

  259.     /**
  260.      * Returns true if the JAR is a `*-sources.jar` or `*-javadoc.jar`;
  261.      * otherwise false.
  262.      *
  263.      * @param path the path to the dependency
  264.      * @return true if the JAR is a `*-sources.jar` or `*-javadoc.jar`;
  265.      * otherwise false.
  266.      */
  267.     private boolean isExcludedJar(File path) {
  268.         final String fileName = path.getName().toLowerCase();
  269.         return EXCLUDE_JARS.stream().anyMatch(fileName::endsWith);
  270.     }
  271.     //</editor-fold>

  272.     /**
  273.      * Returns the key used in the properties file to reference the analyzer's
  274.      * enabled property.
  275.      *
  276.      * @return the analyzer's enabled property setting key
  277.      */
  278.     @Override
  279.     protected String getAnalyzerEnabledSettingKey() {
  280.         return Settings.KEYS.ANALYZER_JAR_ENABLED;
  281.     }

  282.     /**
  283.      * Loads a specified JAR file and collects information from the manifest and
  284.      * checksums to identify the correct CPE information.
  285.      *
  286.      * @param dependency the dependency to analyze.
  287.      * @param engine the engine that is scanning the dependencies
  288.      * @throws AnalysisException is thrown if there is an error reading the JAR
  289.      * file.
  290.      */
  291.     @Override
  292.     public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
  293.         final List<ClassNameInformation> classNames = collectClassNames(dependency);
  294.         final String fileName = dependency.getFileName().toLowerCase();
  295.         if ((classNames.isEmpty()
  296.                 && (fileName.endsWith("-sources.jar")
  297.                 || fileName.endsWith("-javadoc.jar")
  298.                 || fileName.endsWith("-src.jar")
  299.                 || fileName.endsWith("-doc.jar")
  300.                 || isMacOSMetaDataFile(dependency, engine)))
  301.                 || !isZipFile(dependency)) {
  302.             engine.removeDependency(dependency);
  303.             return;
  304.         }
  305.         Exception exception = null;
  306.         boolean hasManifest = false;
  307.         try {
  308.             hasManifest = parseManifest(dependency, classNames);
  309.         } catch (IOException ex) {
  310.             LOGGER.debug("Invalid Manifest", ex);
  311.             exception = ex;
  312.         }
  313.         boolean hasPOM = false;
  314.         try {
  315.             hasPOM = analyzePOM(dependency, classNames, engine);
  316.         } catch (AnalysisException ex) {
  317.             LOGGER.debug("Error parsing pom.xml", ex);
  318.             exception = ex;
  319.         }
  320.         final boolean addPackagesAsEvidence = !(hasManifest && hasPOM);
  321.         analyzePackageNames(classNames, dependency, addPackagesAsEvidence);
  322.         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);

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

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

  349.     /**
  350.      * Iterates through the given list of dependencies and returns true when it
  351.      * finds a dependency with a filename matching the given filename, otherwise
  352.      * returns false.
  353.      *
  354.      * @param dependencies the dependencies to search within
  355.      * @param fileName the filename to search for
  356.      * @return whether or not the given dependencies contain a dependency with
  357.      * the given filename
  358.      */
  359.     @SuppressFBWarnings(justification = "If actual file path is not null the path will have elements and getFileName will not be called on a null",
  360.             value = {"NP_NULL_ON_SOME_PATH_FROM_RETURN_VALUE"})
  361.     private boolean hasDependencyWithFilename(final Dependency[] dependencies, final String fileName) {
  362.         for (final Dependency dependency : dependencies) {
  363.             if (dependency.getActualFilePath() != null
  364.                     && Paths.get(dependency.getActualFilePath()).getFileName().toString().equalsIgnoreCase(fileName)) {
  365.                 return true;
  366.             }
  367.         }
  368.         return false;
  369.     }

  370.     /**
  371.      * Attempts to read the first bytes of the given dependency (using its
  372.      * actual file path) and returns true if they match the expected first bytes
  373.      * of a zip file, which may be empty or spanned. If they don't match, or if
  374.      * the file could not be read, then it returns false.
  375.      *
  376.      * @param dependency the dependency to check if it's a zip file
  377.      * @return whether or not the given dependency appears to be a zip file from
  378.      * its first bytes
  379.      */
  380.     @SuppressFBWarnings(justification = "try with resources will clean up the output stream", value = {"OBL_UNSATISFIED_OBLIGATION"})
  381.     private boolean isZipFile(final Dependency dependency) {
  382.         final byte[] buffer = new byte[4];
  383.         try (FileInputStream fileInputStream = new FileInputStream(dependency.getActualFilePath())) {
  384.             if (fileInputStream.read(buffer) > 0
  385.                     && (Arrays.equals(buffer, ZIP_FIRST_BYTES)
  386.                     || Arrays.equals(buffer, ZIP_EMPTY_FIRST_BYTES)
  387.                     || Arrays.equals(buffer, ZIP_SPANNED_FIRST_BYTES))) {
  388.                 return true;
  389.             }
  390.         } catch (Exception e) {
  391.             LOGGER.warn("Unable to check if '{}' is a zip file", dependency.getActualFilePath());
  392.             LOGGER.trace("", e);
  393.         }
  394.         return false;
  395.     }

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

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

  419.             final List<String> pomEntries = retrievePomListing(jar);

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

  429.                     final String artifactId = new File(path).getParentFile().getName();
  430.                     if (dependency.getActualFile().getName().startsWith(artifactId)) {
  431.                         evidenceAdded |= setPomEvidence(dependency, pom, classes, true);
  432.                     } else {
  433.                         final String displayPath = String.format("%s%s%s",
  434.                                 dependency.getFilePath(),
  435.                                 File.separator,
  436.                                 path);
  437.                         final String displayName = String.format("%s%s%s",
  438.                                 dependency.getFileName(),
  439.                                 File.separator,
  440.                                 path);
  441.                         final Dependency newDependency = new Dependency();
  442.                         newDependency.setActualFilePath(pomFile.getAbsolutePath());
  443.                         newDependency.setFileName(displayName);
  444.                         newDependency.setFilePath(displayPath);
  445.                         newDependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
  446.                         String groupId = pom.getGroupId();
  447.                         String version = pom.getVersion();
  448.                         if (groupId == null) {
  449.                             groupId = pom.getParentGroupId();
  450.                         }
  451.                         if (version == null) {
  452.                             version = pom.getParentVersion();
  453.                         }
  454.                         if (groupId == null) {
  455.                             newDependency.setName(pom.getArtifactId());
  456.                             newDependency.setPackagePath(String.format("%s:%s", pom.getArtifactId(), version));
  457.                         } else {
  458.                             newDependency.setName(String.format("%s:%s", groupId, pom.getArtifactId()));
  459.                             newDependency.setPackagePath(String.format("%s:%s:%s", groupId, pom.getArtifactId(), version));
  460.                         }
  461.                         newDependency.setDisplayFileName(String.format("%s (shaded: %s)",
  462.                                 dependency.getDisplayFileName(), newDependency.getPackagePath()));
  463.                         newDependency.setVersion(version);
  464.                         setPomEvidence(newDependency, pom, null, true);
  465.                         if (dependency.getProjectReferences().size() > 0) {
  466.                             newDependency.addAllProjectReferences(dependency.getProjectReferences());
  467.                         }
  468.                         engine.addDependency(newDependency);
  469.                     }
  470.                 } catch (AnalysisException ex) {
  471.                     LOGGER.warn("An error occurred while analyzing '{}'.", dependency.getActualFilePath());
  472.                     LOGGER.trace("", ex);
  473.                 }
  474.             }
  475.         } catch (IOException ex) {
  476.             LOGGER.warn("Unable to read JarFile '{}'.", dependency.getActualFilePath());
  477.             LOGGER.trace("", ex);
  478.         }
  479.         return evidenceAdded;
  480.     }

  481.     /**
  482.      * Given a path to a pom.xml within a JarFile, this method attempts to load
  483.      * a sibling pom.properties if one exists.
  484.      *
  485.      * @param path the path to the pom.xml within the JarFile
  486.      * @param jar the JarFile to load the pom.properties from
  487.      * @return a Properties object or null if no pom.properties was found
  488.      */
  489.     private Properties retrievePomProperties(String path, final JarFile jar) {
  490.         final Properties pomProperties = new Properties();
  491.         final String propPath = path.substring(0, path.length() - 7) + "pom.properties";
  492.         final ZipEntry propEntry = jar.getEntry(propPath);
  493.         if (propEntry != null) {
  494.             try (Reader reader = new InputStreamReader(jar.getInputStream(propEntry), StandardCharsets.UTF_8)) {
  495.                 pomProperties.load(reader);
  496.                 LOGGER.debug("Read pom.properties: {}", propPath);
  497.             } catch (UnsupportedEncodingException ex) {
  498.                 LOGGER.trace("UTF-8 is not supported", ex);
  499.             } catch (IOException ex) {
  500.                 LOGGER.trace("Unable to read the POM properties", ex);
  501.             }
  502.         }
  503.         return pomProperties;
  504.     }

  505.     /**
  506.      * Searches a JarFile for pom.xml entries and returns a listing of these
  507.      * entries.
  508.      *
  509.      * @param jar the JarFile to search
  510.      * @return a list of pom.xml entries
  511.      * @throws IOException thrown if there is an exception reading a JarEntry
  512.      */
  513.     private List<String> retrievePomListing(final JarFile jar) throws IOException {
  514.         final List<String> pomEntries = new ArrayList<>();
  515.         final Enumeration<JarEntry> entries = jar.entries();
  516.         while (entries.hasMoreElements()) {
  517.             final JarEntry entry = entries.nextElement();
  518.             final String entryName = new File(entry.getName()).getName().toLowerCase();
  519.             if (!entry.isDirectory() && "pom.xml".equals(entryName)
  520.                     && entry.getName().toUpperCase().startsWith("META-INF")) {
  521.                 pomEntries.add(entry.getName());
  522.             }
  523.         }
  524.         return pomEntries;
  525.     }

  526.     /**
  527.      * Retrieves the specified POM from a jar.
  528.      *
  529.      * @param path the path to the pom.xml file within the jar file
  530.      * @param jar the jar file to extract the pom from
  531.      * @return returns the POM file
  532.      * @throws AnalysisException is thrown if there is an exception extracting
  533.      * the file
  534.      */
  535.     private File extractPom(String path, JarFile jar) throws AnalysisException {
  536.         final File tmpDir = getNextTempDirectory();
  537.         final File file = new File(tmpDir, "pom.xml");
  538.         final ZipEntry entry = jar.getEntry(path);
  539.         if (entry == null) {
  540.             throw new AnalysisException(String.format("Pom (%s) does not exist in %s", path, jar.getName()));
  541.         }
  542.         try (InputStream input = jar.getInputStream(entry);
  543.                 FileOutputStream fos = new FileOutputStream(file)) {
  544.             IOUtils.copy(input, fos);
  545.         } catch (IOException ex) {
  546.             LOGGER.warn("An error occurred reading '{}' from '{}'.", path, jar.getName());
  547.             LOGGER.error("", ex);
  548.         }
  549.         return file;
  550.     }

  551.     /**
  552.      * Sets evidence from the pom on the supplied dependency.
  553.      *
  554.      * @param dependency the dependency to set data on
  555.      * @param pom the information from the pom
  556.      * @param classes a collection of ClassNameInformation - containing data
  557.      * about the fully qualified class names within the JAR file being analyzed
  558.      * @param isMainPom a flag indicating if this is the primary pom.
  559.      * @return true if there was evidence within the pom that we could use;
  560.      * otherwise false
  561.      */
  562.     public static boolean setPomEvidence(Dependency dependency, Model pom,
  563.             List<ClassNameInformation> classes, boolean isMainPom) {
  564.         if (pom == null) {
  565.             return false;
  566.         }
  567.         boolean foundSomething = false;
  568.         boolean addAsIdentifier = true;
  569.         String groupid = intepolationFailCheck(pom.getGroupId());
  570.         String parentGroupId = intepolationFailCheck(pom.getParentGroupId());
  571.         String artifactid = intepolationFailCheck(pom.getArtifactId());
  572.         String parentArtifactId = intepolationFailCheck(pom.getParentArtifactId());
  573.         String version = intepolationFailCheck(pom.getVersion());
  574.         String parentVersion = intepolationFailCheck(pom.getParentVersion());

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

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

  584.         final String originalGroupID = groupid;

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

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

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

  595.         if (isMainPom && dependency.getName() == null && originalArtifactID != null && !originalArtifactID.isEmpty()) {
  596.             if (originalGroupID != null && !originalGroupID.isEmpty()) {
  597.                 dependency.setName(String.format("%s:%s", originalGroupID, originalArtifactID));
  598.             } else {
  599.                 dependency.setName(originalArtifactID);
  600.             }
  601.         }
  602.         if (isMainPom && dependency.getVersion() == null && version != null && !version.isEmpty()) {
  603.             dependency.setVersion(version);
  604.         }

  605.         if (groupid != null && !groupid.isEmpty()) {
  606.             foundSomething = true;
  607.             dependency.addEvidence(EvidenceType.VENDOR, "pom", "groupid", groupid, Confidence.HIGHEST);
  608.             //In several cases we are seeing the product name at the end of the group identifier.
  609.             // This may cause several FP on products that have a collection of dependencies (e.g. jetty).
  610.             //dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.LOW);
  611.             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "groupid", groupid, Confidence.HIGHEST);
  612.             addMatchingValues(classes, groupid, dependency, EvidenceType.VENDOR);
  613.             addMatchingValues(classes, groupid, dependency, EvidenceType.PRODUCT);
  614.             if (parentGroupId != null && !parentGroupId.isEmpty() && !parentGroupId.equals(groupid)) {
  615.                 dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
  616.                 //see note above for groupid
  617.                 //dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.LOW);
  618.                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-groupid", parentGroupId, Confidence.MEDIUM);
  619.                 addMatchingValues(classes, parentGroupId, dependency, EvidenceType.VENDOR);
  620.                 addMatchingValues(classes, parentGroupId, dependency, EvidenceType.PRODUCT);
  621.             }
  622.         } else {
  623.             addAsIdentifier = false;
  624.         }

  625.         if (artifactid != null && !artifactid.isEmpty()) {
  626.             foundSomething = true;
  627.             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "artifactid", artifactid, Confidence.HIGHEST);
  628.             dependency.addEvidence(EvidenceType.VENDOR, "pom", "artifactid", artifactid, Confidence.LOW);
  629.             addMatchingValues(classes, artifactid, dependency, EvidenceType.VENDOR);
  630.             addMatchingValues(classes, artifactid, dependency, EvidenceType.PRODUCT);
  631.             if (parentArtifactId != null && !parentArtifactId.isEmpty() && !parentArtifactId.equals(artifactid)) {
  632.                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "parent-artifactid", parentArtifactId, Confidence.MEDIUM);
  633.                 dependency.addEvidence(EvidenceType.VENDOR, "pom", "parent-artifactid", parentArtifactId, Confidence.LOW);
  634.                 addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.VENDOR);
  635.                 addMatchingValues(classes, parentArtifactId, dependency, EvidenceType.PRODUCT);
  636.             }
  637.         } else {
  638.             addAsIdentifier = false;
  639.         }

  640.         if (version != null && !version.isEmpty()) {
  641.             foundSomething = true;
  642.             dependency.addEvidence(EvidenceType.VERSION, "pom", "version", version, Confidence.HIGHEST);
  643.             if (parentVersion != null && !parentVersion.isEmpty() && !parentVersion.equals(version)) {
  644.                 dependency.addEvidence(EvidenceType.VERSION, "pom", "parent-version", version, Confidence.LOW);
  645.             }
  646.         } else {
  647.             addAsIdentifier = false;
  648.         }

  649.         if (addAsIdentifier && isMainPom) {
  650.             Identifier id = null;
  651.             try {
  652.                 if (originalArtifactID != null && originalArtifactID.matches(VALID_NAME)
  653.                         && originalGroupID != null && originalGroupID.matches(VALID_NAME)) {
  654.                     final PackageURL purl = PackageURLBuilder.aPackageURL().withType("maven").withNamespace(originalGroupID)
  655.                             .withName(originalArtifactID).withVersion(version).build();
  656.                     id = new PurlIdentifier(purl, Confidence.HIGH);
  657.                 } else {
  658.                     LOGGER.debug("Invalid maven identifier identified: " + originalGroupID + ":" + originalArtifactID);
  659.                 }
  660.             } catch (MalformedPackageURLException ex) {
  661.                 final String gav = String.format("%s:%s:%s", originalGroupID, originalArtifactID, version);
  662.                 LOGGER.debug("Error building package url for " + gav + "; using generic identifier instead.", ex);
  663.                 id = new GenericIdentifier("maven:" + gav, Confidence.HIGH);
  664.             }
  665.             if (id != null) {
  666.                 dependency.addSoftwareIdentifier(id);
  667.             }
  668.         }

  669.         // org name
  670.         final String org = pom.getOrganization();
  671.         if (org != null && !org.isEmpty()) {
  672.             dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization name", org, Confidence.HIGH);
  673.             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization name", org, Confidence.LOW);
  674.             addMatchingValues(classes, org, dependency, EvidenceType.VENDOR);
  675.             addMatchingValues(classes, org, dependency, EvidenceType.PRODUCT);
  676.         }
  677.         // org name
  678.         String orgUrl = pom.getOrganizationUrl();
  679.         if (orgUrl != null && !orgUrl.isEmpty()) {
  680.             if (orgUrl.startsWith("https://github.com/") || orgUrl.startsWith("https://gitlab.com/")) {
  681.                 orgUrl = orgUrl.substring(19);
  682.                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "url", orgUrl, Confidence.HIGH);
  683.             } else {
  684.                 dependency.addEvidence(EvidenceType.PRODUCT, "pom", "organization url", orgUrl, Confidence.LOW);
  685.             }
  686.             dependency.addEvidence(EvidenceType.VENDOR, "pom", "organization url", orgUrl, Confidence.MEDIUM);
  687.         }
  688.         //pom name
  689.         final String pomName = pom.getName();
  690.         if (pomName != null && !pomName.isEmpty() && !"${project.groupId}:${project.artifactId}".equals(pomName)) {
  691.             foundSomething = true;
  692.             dependency.addEvidence(EvidenceType.PRODUCT, "pom", "name", pomName, Confidence.HIGH);
  693.             dependency.addEvidence(EvidenceType.VENDOR, "pom", "name", pomName, Confidence.HIGH);
  694.             addMatchingValues(classes, pomName, dependency, EvidenceType.VENDOR);
  695.             addMatchingValues(classes, pomName, dependency, EvidenceType.PRODUCT);
  696.         }

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

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

  717.         }

  718.         if (pom.getDevelopers() != null && !pom.getDevelopers().isEmpty()) {
  719.             for (Developer dev : pom.getDevelopers()) {
  720.                 if (!Strings.isNullOrEmpty(dev.getId())) {
  721.                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer id", dev.getId(), Confidence.MEDIUM);
  722.                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer id", dev.getId(), Confidence.LOW);
  723.                 }
  724.                 if (!Strings.isNullOrEmpty(dev.getName())) {
  725.                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer name", dev.getName(), Confidence.MEDIUM);
  726.                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer name", dev.getName(), Confidence.LOW);
  727.                 }
  728.                 if (!Strings.isNullOrEmpty(dev.getEmail())) {
  729.                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer email", dev.getEmail(), Confidence.LOW);
  730.                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer email", dev.getEmail(), Confidence.LOW);
  731.                 }
  732.                 if (!Strings.isNullOrEmpty(dev.getOrganizationUrl())) {
  733.                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.MEDIUM);
  734.                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org URL", dev.getOrganizationUrl(), Confidence.LOW);
  735.                 }
  736.                 final String devOrg = dev.getOrganization();
  737.                 if (!Strings.isNullOrEmpty(devOrg)) {
  738.                     dependency.addEvidence(EvidenceType.VENDOR, "pom", "developer org", devOrg, Confidence.MEDIUM);
  739.                     dependency.addEvidence(EvidenceType.PRODUCT, "pom", "developer org", devOrg, Confidence.LOW);
  740.                     addMatchingValues(classes, devOrg, dependency, EvidenceType.VENDOR);
  741.                     addMatchingValues(classes, devOrg, dependency, EvidenceType.PRODUCT);
  742.                 }
  743.             }
  744.         }

  745.         extractLicense(pom, dependency);
  746.         return foundSomething;
  747.     }

  748.     /**
  749.      * Analyzes the path information of the classes contained within the
  750.      * JarAnalyzer to try and determine possible vendor or product names. If any
  751.      * are found they are stored in the packageVendor and packageProduct
  752.      * hashSets.
  753.      *
  754.      * @param classNames a list of class names
  755.      * @param dependency a dependency to analyze
  756.      * @param addPackagesAsEvidence a flag indicating whether or not package
  757.      * names should be added as evidence.
  758.      */
  759.     protected void analyzePackageNames(List<ClassNameInformation> classNames,
  760.             Dependency dependency, boolean addPackagesAsEvidence) {
  761.         final Map<String, Integer> vendorIdentifiers = new HashMap<>();
  762.         final Map<String, Integer> productIdentifiers = new HashMap<>();
  763.         analyzeFullyQualifiedClassNames(classNames, vendorIdentifiers, productIdentifiers);

  764.         final int classCount = classNames.size();

  765.         vendorIdentifiers.forEach((key, value) -> {
  766.             final float ratio = value / (float) classCount;
  767.             if (ratio > 0.5) {
  768.                 //TODO remove weighting?
  769.                 dependency.addVendorWeighting(key);
  770.                 if (addPackagesAsEvidence && key.length() > 1) {
  771.                     dependency.addEvidence(EvidenceType.VENDOR, "jar", "package name", key, Confidence.LOW);
  772.                 }
  773.             }
  774.         });
  775.         productIdentifiers.forEach((key, value) -> {
  776.             final float ratio = value / (float) classCount;
  777.             if (ratio > 0.5) {
  778.                 //todo remove weighting
  779.                 dependency.addProductWeighting(key);
  780.                 if (addPackagesAsEvidence && key.length() > 1) {
  781.                     dependency.addEvidence(EvidenceType.PRODUCT, "jar", "package name", key, Confidence.LOW);
  782.                 }
  783.             }
  784.         });
  785.     }

  786.     /**
  787.      * <p>
  788.      * Reads the manifest from the JAR file and collects the entries. Some
  789.      * vendorKey entries are:</p>
  790.      * <ul><li>Implementation Title</li>
  791.      * <li>Implementation Version</li> <li>Implementation Vendor</li>
  792.      * <li>Implementation VendorId</li> <li>Bundle Name</li> <li>Bundle
  793.      * Version</li> <li>Bundle Vendor</li> <li>Bundle Description</li> <li>Main
  794.      * Class</li> </ul>
  795.      * However, all but a handful of specific entries are read in.
  796.      *
  797.      * @param dependency A reference to the dependency
  798.      * @param classInformation a collection of class information
  799.      * @return whether evidence was identified parsing the manifest
  800.      * @throws IOException if there is an issue reading the JAR file
  801.      */
  802.     //CSOFF: MethodLength
  803.     protected boolean parseManifest(Dependency dependency, List<ClassNameInformation> classInformation)
  804.             throws IOException {
  805.         boolean foundSomething = false;
  806.         try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
  807.             final Manifest manifest = jar.getManifest();
  808.             if (manifest == null) {
  809.                 if (!dependency.getFileName().toLowerCase().endsWith("-sources.jar")
  810.                         && !dependency.getFileName().toLowerCase().endsWith("-javadoc.jar")
  811.                         && !dependency.getFileName().toLowerCase().endsWith("-src.jar")
  812.                         && !dependency.getFileName().toLowerCase().endsWith("-doc.jar")) {
  813.                     LOGGER.debug("Jar file '{}' does not contain a manifest.", dependency.getFileName());
  814.                 }
  815.                 return false;
  816.             }
  817.             String source = "Manifest";
  818.             String specificationVersion = null;
  819.             boolean hasImplementationVersion = false;
  820.             Attributes atts = manifest.getMainAttributes();
  821.             for (Entry<Object, Object> entry : atts.entrySet()) {
  822.                 String key = entry.getKey().toString();
  823.                 String value = atts.getValue(key);
  824.                 if (HTML_DETECTION_PATTERN.matcher(value).find()) {
  825.                     value = Jsoup.parse(value).text();
  826.                 }
  827.                 if (value.startsWith("git@github.com:") || value.startsWith("git@gitlab.com:")) {
  828.                     value = value.substring(15);
  829.                 }
  830.                 if (IGNORE_VALUES.contains(value)) {
  831.                     continue;
  832.                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
  833.                     foundSomething = true;
  834.                     dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.HIGH);
  835.                     addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  836.                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
  837.                     hasImplementationVersion = true;
  838.                     foundSomething = true;
  839.                     dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH);
  840.                 } else if ("specification-version".equalsIgnoreCase(key)) {
  841.                     specificationVersion = value;
  842.                 } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
  843.                     foundSomething = true;
  844.                     dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.HIGH);
  845.                     addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
  846.                 } else if (key.equalsIgnoreCase(IMPLEMENTATION_VENDOR_ID)) {
  847.                     foundSomething = true;
  848.                     dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
  849.                     addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
  850.                 } else if (key.equalsIgnoreCase(BUNDLE_DESCRIPTION)) {
  851.                     if (!value.startsWith("Sonatype helps open source projects")) {
  852.                         foundSomething = true;
  853.                         addDescription(dependency, value, "manifest", key);
  854.                         addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  855.                     }
  856.                 } else if (key.equalsIgnoreCase(BUNDLE_NAME)) {
  857.                     foundSomething = true;
  858.                     dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
  859.                     addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  860. //                //the following caused false positives.
  861. //                } else if (key.equalsIgnoreCase(BUNDLE_VENDOR)) {
  862.                 } else if (key.equalsIgnoreCase(BUNDLE_VERSION)) {
  863.                     foundSomething = true;
  864.                     dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.HIGH);
  865.                 } else if (key.equalsIgnoreCase(Attributes.Name.MAIN_CLASS.toString())) {
  866.                     //noinspection UnnecessaryContinue
  867.                     continue;
  868.                     //skipping main class as if this has important information to add it will be added during class name analysis...
  869.                 } else if ("implementation-url".equalsIgnoreCase(key)
  870.                         && value != null
  871.                         && value.startsWith("https://projects.spring.io/spring-boot/#/spring-boot-starter-parent/parent/")) {
  872.                     continue;
  873.                 } else {
  874.                     key = key.toLowerCase();
  875.                     if (!IGNORE_KEYS.contains(key)
  876.                             && !key.endsWith("jdk")
  877.                             && !key.contains("lastmodified")
  878.                             && !key.endsWith("package")
  879.                             && !key.endsWith("classpath")
  880.                             && !key.endsWith("class-path")
  881.                             && !key.endsWith("-scm") //todo change this to a regex?
  882.                             && !key.startsWith("scm-")
  883.                             && !value.trim().startsWith("scm:")
  884.                             && !isImportPackage(key, value)
  885.                             && !isPackage(key, value)) {
  886.                         foundSomething = true;
  887.                         if (key.contains("version")) {
  888.                             if (!key.contains("specification")) {
  889.                                 dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
  890.                             }
  891.                         } else if ("build-id".equals(key)) {
  892.                             int pos = value.indexOf('(');
  893.                             if (pos > 0) {
  894.                                 value = value.substring(0, pos - 1);
  895.                             }
  896.                             pos = value.indexOf('[');
  897.                             if (pos > 0) {
  898.                                 value = value.substring(0, pos - 1);
  899.                             }
  900.                             dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
  901.                         } else if (key.contains("title")) {
  902.                             dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
  903.                             addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  904.                         } else if (key.contains("vendor")) {
  905.                             if (key.contains("specification")) {
  906.                                 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW);
  907.                             } else {
  908.                                 dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
  909.                                 addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
  910.                             }
  911.                         } else if (key.contains("name")) {
  912.                             dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
  913.                             dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
  914.                             addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
  915.                             addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  916.                         } else if (key.contains("license")) {
  917.                             addLicense(dependency, value);
  918.                         } else if (key.contains("description")) {
  919.                             if (!value.startsWith("Sonatype helps open source projects")) {
  920.                                 final String trimmedDescription = addDescription(dependency, value, "manifest", key);
  921.                                 addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.VENDOR);
  922.                                 addMatchingValues(classInformation, trimmedDescription, dependency, EvidenceType.PRODUCT);
  923.                             }
  924.                         } else {
  925.                             dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.LOW);
  926.                             dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.LOW);
  927.                             addMatchingValues(classInformation, value, dependency, EvidenceType.VERSION);
  928.                             addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  929.                             if (value.matches(".*\\d.*")) {
  930.                                 final StringTokenizer tokenizer = new StringTokenizer(value, " ");
  931.                                 while (tokenizer.hasMoreElements()) {
  932.                                     final String s = tokenizer.nextToken();
  933.                                     if (s.matches("^[0-9.]+$")) {
  934.                                         dependency.addEvidence(EvidenceType.VERSION, source, key, s, Confidence.LOW);
  935.                                     }
  936.                                 }
  937.                             }
  938.                         }
  939.                     }
  940.                 }
  941.             }
  942.             for (Map.Entry<String, Attributes> item : manifest.getEntries().entrySet()) {
  943.                 final String name = item.getKey();
  944.                 source = "manifest: " + name;
  945.                 atts = item.getValue();
  946.                 for (Entry<Object, Object> entry : atts.entrySet()) {
  947.                     final String key = entry.getKey().toString();
  948.                     final String value = atts.getValue(key);
  949.                     if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_TITLE.toString())) {
  950.                         foundSomething = true;
  951.                         dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
  952.                         addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  953.                     } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VERSION.toString())) {
  954.                         foundSomething = true;
  955.                         dependency.addEvidence(EvidenceType.VERSION, source, key, value, Confidence.MEDIUM);
  956.                     } else if (key.equalsIgnoreCase(Attributes.Name.IMPLEMENTATION_VENDOR.toString())) {
  957.                         foundSomething = true;
  958.                         dependency.addEvidence(EvidenceType.VENDOR, source, key, value, Confidence.MEDIUM);
  959.                         addMatchingValues(classInformation, value, dependency, EvidenceType.VENDOR);
  960.                     } else if (key.equalsIgnoreCase(Attributes.Name.SPECIFICATION_TITLE.toString())) {
  961.                         foundSomething = true;
  962.                         dependency.addEvidence(EvidenceType.PRODUCT, source, key, value, Confidence.MEDIUM);
  963.                         addMatchingValues(classInformation, value, dependency, EvidenceType.PRODUCT);
  964.                     }
  965.                 }
  966.             }
  967.             if (specificationVersion != null && !hasImplementationVersion) {
  968.                 foundSomething = true;
  969.                 dependency.addEvidence(EvidenceType.VERSION, source, "specification-version", specificationVersion, Confidence.HIGH);
  970.             }
  971.         }
  972.         return foundSomething;
  973.     }
  974.     //CSON: MethodLength

  975.     /**
  976.      * Adds a description to the given dependency. If the description contains
  977.      * one of the following strings beyond 100 characters, then the description
  978.      * used will be trimmed to that position:
  979.      * <ul><li>"such as"</li><li>"like "</li><li>"will use "</li><li>"* uses
  980.      * "</li></ul>
  981.      *
  982.      * @param dependency a dependency
  983.      * @param description the description
  984.      * @param source the source of the evidence
  985.      * @param key the "name" of the evidence
  986.      * @return if the description is trimmed, the trimmed version is returned;
  987.      * otherwise the original description is returned
  988.      */
  989.     public static String addDescription(Dependency dependency, String description, String source, String key) {
  990.         if (dependency.getDescription() == null) {
  991.             dependency.setDescription(description);
  992.         }
  993.         String desc;
  994.         if (HTML_DETECTION_PATTERN.matcher(description).find()) {
  995.             desc = Jsoup.parse(description).text();
  996.         } else {
  997.             desc = description;
  998.         }
  999.         dependency.setDescription(desc);
  1000.         if (desc.length() > 100) {
  1001.             desc = desc.replaceAll("\\s\\s+", " ");
  1002.             final int posSuchAs = desc.toLowerCase().indexOf("such as ", 100);
  1003.             final int posLike = desc.toLowerCase().indexOf("like ", 100);
  1004.             final int posWillUse = desc.toLowerCase().indexOf("will use ", 100);
  1005.             final int posUses = desc.toLowerCase().indexOf(" uses ", 100);

  1006.             int pos = -1;
  1007.             pos = Math.max(pos, posSuchAs);
  1008.             if (pos >= 0 && posLike >= 0) {
  1009.                 pos = Math.min(pos, posLike);
  1010.             } else {
  1011.                 pos = Math.max(pos, posLike);
  1012.             }
  1013.             if (pos >= 0 && posWillUse >= 0) {
  1014.                 pos = Math.min(pos, posWillUse);
  1015.             } else {
  1016.                 pos = Math.max(pos, posWillUse);
  1017.             }
  1018.             if (pos >= 0 && posUses >= 0) {
  1019.                 pos = Math.min(pos, posUses);
  1020.             } else {
  1021.                 pos = Math.max(pos, posUses);
  1022.             }
  1023.             if (pos > 0) {
  1024.                 desc = desc.substring(0, pos) + "...";
  1025.             }
  1026. //            //no longer add description directly. Use matching terms in other parts of the evidence collection
  1027. //            //but description adds too many FP
  1028. //            dependency.addEvidence(EvidenceType.PRODUCT, source, key, desc, Confidence.LOW);
  1029. //            dependency.addEvidence(EvidenceType.VENDOR, source, key, desc, Confidence.LOW);
  1030. //        } else {
  1031. //            dependency.addEvidence(EvidenceType.PRODUCT, source, key, desc, Confidence.MEDIUM);
  1032. //            dependency.addEvidence(EvidenceType.VENDOR, source, key, desc, Confidence.MEDIUM);
  1033.         }
  1034.         return desc;
  1035.     }

  1036.     /**
  1037.      * Adds a license to the given dependency.
  1038.      *
  1039.      * @param d a dependency
  1040.      * @param license the license
  1041.      */
  1042.     private void addLicense(Dependency d, String license) {
  1043.         if (d.getLicense() == null) {
  1044.             d.setLicense(license);
  1045.         } else if (!d.getLicense().contains(license)) {
  1046.             d.setLicense(d.getLicense() + NEWLINE + license);
  1047.         }
  1048.     }

  1049.     /**
  1050.      * Initializes the JarAnalyzer.
  1051.      *
  1052.      * @param engine a reference to the dependency-check engine
  1053.      * @throws InitializationException is thrown if there is an exception
  1054.      * creating a temporary directory
  1055.      */
  1056.     @Override
  1057.     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
  1058.         try {
  1059.             final File baseDir = getSettings().getTempDirectory();
  1060.             tempFileLocation = File.createTempFile("check", "tmp", baseDir);
  1061.             if (!tempFileLocation.delete()) {
  1062.                 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
  1063.                 setEnabled(false);
  1064.                 throw new InitializationException(msg);
  1065.             }
  1066.             if (!tempFileLocation.mkdirs()) {
  1067.                 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
  1068.                 setEnabled(false);
  1069.                 throw new InitializationException(msg);
  1070.             }
  1071.         } catch (IOException ex) {
  1072.             setEnabled(false);
  1073.             throw new InitializationException("Unable to create a temporary file", ex);
  1074.         }
  1075.     }

  1076.     /**
  1077.      * Deletes any files extracted from the JAR during analysis.
  1078.      */
  1079.     @Override
  1080.     public void closeAnalyzer() {
  1081.         if (tempFileLocation != null && tempFileLocation.exists()) {
  1082.             LOGGER.debug("Attempting to delete temporary files from `{}`", tempFileLocation.toString());
  1083.             final boolean success = FileUtils.delete(tempFileLocation);
  1084.             if (!success && tempFileLocation.exists()) {
  1085.                 final String[] l = tempFileLocation.list();
  1086.                 if (l != null && l.length > 0) {
  1087.                     LOGGER.warn("Failed to delete the JAR Analyzder's temporary files from `{}`, "
  1088.                             + "see the log for more details", tempFileLocation.getAbsolutePath());
  1089.                 }
  1090.             }
  1091.         }
  1092.     }

  1093.     /**
  1094.      * Determines if the key value pair from the manifest is for an "import"
  1095.      * type entry for package names.
  1096.      *
  1097.      * @param key the key from the manifest
  1098.      * @param value the value from the manifest
  1099.      * @return true or false depending on if it is believed the entry is an
  1100.      * "import" entry
  1101.      */
  1102.     private boolean isImportPackage(String key, String value) {
  1103.         final Pattern packageRx = Pattern.compile("^(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*[,;])+(\\s*[a-zA-Z0-9_#\\$\\*\\.]+\\s*)?$");
  1104.         final boolean matches = packageRx.matcher(value).matches();
  1105.         return matches && (key.contains("import") || key.contains("include") || value.length() > 10);
  1106.     }

  1107.     /**
  1108.      * Cycles through an enumeration of JarEntries, contained within the
  1109.      * dependency, and returns a list of the class names. This does not include
  1110.      * core Java package names (i.e. java.* or javax.*).
  1111.      *
  1112.      * @param dependency the dependency being analyzed
  1113.      * @return an list of fully qualified class names
  1114.      */
  1115.     protected List<ClassNameInformation> collectClassNames(Dependency dependency) {
  1116.         final List<ClassNameInformation> classNames = new ArrayList<>();
  1117.         try (JarFile jar = new JarFile(dependency.getActualFilePath(), false)) {
  1118.             final Enumeration<JarEntry> entries = jar.entries();
  1119.             while (entries.hasMoreElements()) {
  1120.                 final JarEntry entry = entries.nextElement();
  1121.                 final String name = entry.getName().toLowerCase();
  1122.                 //no longer stripping "|com\\.sun" - there are some com.sun jar files with CVEs.
  1123.                 if (name.endsWith(".class") && !name.matches("^javax?\\..*$")) {
  1124.                     final ClassNameInformation className = new ClassNameInformation(name.substring(0, name.length() - 6));
  1125.                     classNames.add(className);
  1126.                 }
  1127.             }
  1128.         } catch (IOException ex) {
  1129.             LOGGER.warn("Unable to open jar file '{}'.", dependency.getFileName());
  1130.             LOGGER.debug("", ex);
  1131.         }
  1132.         return classNames;
  1133.     }

  1134.     /**
  1135.      * Cycles through the list of class names and places the package levels 0-3
  1136.      * into the provided maps for vendor and product. This is helpful when
  1137.      * analyzing vendor/product as many times this is included in the package
  1138.      * name.
  1139.      *
  1140.      * @param classNames a list of class names
  1141.      * @param vendor HashMap of possible vendor names from package names (e.g.
  1142.      * owasp)
  1143.      * @param product HashMap of possible product names from package names (e.g.
  1144.      * dependencycheck)
  1145.      */
  1146.     private void analyzeFullyQualifiedClassNames(List<ClassNameInformation> classNames,
  1147.             Map<String, Integer> vendor, Map<String, Integer> product) {
  1148.         for (ClassNameInformation entry : classNames) {
  1149.             final List<String> list = entry.getPackageStructure();
  1150.             addEntry(vendor, list.get(0));

  1151.             if (list.size() == 2) {
  1152.                 addEntry(product, list.get(1));
  1153.             } else if (list.size() == 3) {
  1154.                 addEntry(vendor, list.get(1));
  1155.                 addEntry(product, list.get(1));
  1156.                 addEntry(product, list.get(2));
  1157.             } else if (list.size() >= 4) {
  1158.                 addEntry(vendor, list.get(1));
  1159.                 addEntry(vendor, list.get(2));
  1160.                 addEntry(product, list.get(1));
  1161.                 addEntry(product, list.get(2));
  1162.                 addEntry(product, list.get(3));
  1163.             }
  1164.         }
  1165.     }

  1166.     /**
  1167.      * Adds an entry to the specified collection and sets the Integer (e.g. the
  1168.      * count) to 1. If the entry already exists in the collection then the
  1169.      * Integer is incremented by 1.
  1170.      *
  1171.      * @param collection a collection of strings and their occurrence count
  1172.      * @param key the key to add to the collection
  1173.      */
  1174.     private void addEntry(Map<String, Integer> collection, String key) {
  1175.         if (collection.containsKey(key)) {
  1176.             collection.put(key, collection.get(key) + 1);
  1177.         } else {
  1178.             collection.put(key, 1);
  1179.         }
  1180.     }

  1181.     /**
  1182.      * Cycles through the collection of class name information to see if parts
  1183.      * of the package names are contained in the provided value. If found, it
  1184.      * will be added as the HIGHEST confidence evidence because we have more
  1185.      * then one source corroborating the value.
  1186.      *
  1187.      * @param classes a collection of class name information
  1188.      * @param value the value to check to see if it contains a package name
  1189.      * @param dep the dependency to add new entries too
  1190.      * @param type the type of evidence (vendor, product, or version)
  1191.      */
  1192.     protected static void addMatchingValues(List<ClassNameInformation> classes, String value, Dependency dep, EvidenceType type) {
  1193.         if (value == null || value.isEmpty() || classes == null || classes.isEmpty()) {
  1194.             return;
  1195.         }
  1196.         final HashSet<String> tested = new HashSet<>();
  1197.         //TODO add a hashSet and only analyze any given key once.
  1198.         for (ClassNameInformation cni : classes) {
  1199.             //classes.forEach((cni) -> {
  1200.             for (String key : cni.getPackageStructure()) {
  1201.                 //cni.getPackageStructure().forEach((key) -> {
  1202.                 if (!tested.contains(key)) {
  1203.                     tested.add(key);
  1204.                     final int pos = StringUtils.indexOfIgnoreCase(value, key);
  1205.                     if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length()
  1206.                             && !Character.isLetterOrDigit(value.charAt(key.length())))))
  1207.                             || (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1))
  1208.                             && (pos + key.length() == value.length() || (key.length() < value.length()
  1209.                             && !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) {
  1210.                         dep.addEvidence(type, "jar", "package name", key, Confidence.HIGHEST);
  1211.                     }
  1212.                 }
  1213.             }
  1214.         }
  1215.     }

  1216.     /**
  1217.      * Simple check to see if the attribute from a manifest is just a package
  1218.      * name.
  1219.      *
  1220.      * @param key the key of the value to check
  1221.      * @param value the value to check
  1222.      * @return true if the value looks like a java package name, otherwise false
  1223.      */
  1224.     private boolean isPackage(String key, String value) {

  1225.         return !key.matches(".*(version|title|vendor|name|license|description).*")
  1226.                 && value.matches("^[a-zA-Z_][a-zA-Z0-9_\\$]*\\.([a-zA-Z_][a-zA-Z0-9_\\$]*\\.)*([a-zA-Z_][a-zA-Z0-9_\\$]*)$");

  1227.     }

  1228.     /**
  1229.      * Returns null if the value starts with `${` and ends with `}`.
  1230.      *
  1231.      * @param value the value to check
  1232.      * @return the correct value which may be null
  1233.      */
  1234.     private static String intepolationFailCheck(String value) {
  1235.         if (value != null && value.contains("${")) {
  1236.             return null;
  1237.         }
  1238.         return value;
  1239.     }

  1240.     /**
  1241.      * Extracts the license information from the pom and adds it to the
  1242.      * dependency.
  1243.      *
  1244.      * @param pom the pom object
  1245.      * @param dependency the dependency to add license information too
  1246.      */
  1247.     public static void extractLicense(Model pom, Dependency dependency) {
  1248.         //license
  1249.         if (pom.getLicenses() != null) {
  1250.             StringBuilder license = null;
  1251.             for (License lic : pom.getLicenses()) {
  1252.                 String tmp = null;
  1253.                 if (lic.getName() != null) {
  1254.                     tmp = lic.getName();
  1255.                 }
  1256.                 if (lic.getUrl() != null) {
  1257.                     if (tmp == null) {
  1258.                         tmp = lic.getUrl();
  1259.                     } else {
  1260.                         tmp += ": " + lic.getUrl();
  1261.                     }
  1262.                 }
  1263.                 if (tmp == null) {
  1264.                     continue;
  1265.                 }
  1266.                 if (HTML_DETECTION_PATTERN.matcher(tmp).find()) {
  1267.                     tmp = Jsoup.parse(tmp).text();
  1268.                 }
  1269.                 if (license == null) {
  1270.                     license = new StringBuilder(tmp);
  1271.                 } else {
  1272.                     license.append("\n").append(tmp);
  1273.                 }
  1274.             }
  1275.             if (license != null) {
  1276.                 dependency.setLicense(license.toString());

  1277.             }
  1278.         }
  1279.     }

  1280.     /**
  1281.      * Stores information about a class name.
  1282.      */
  1283.     protected static class ClassNameInformation {

  1284.         /**
  1285.          * The fully qualified class name.
  1286.          */
  1287.         private String name;
  1288.         /**
  1289.          * Up to the first four levels of the package structure, excluding a
  1290.          * leading "org" or "com".
  1291.          */
  1292.         private final ArrayList<String> packageStructure = new ArrayList<>();

  1293.         /**
  1294.          * <p>
  1295.          * Stores information about a given class name. This class will keep the
  1296.          * fully qualified class name and a list of the important parts of the
  1297.          * package structure. Up to the first four levels of the package
  1298.          * structure are stored, excluding a leading "org" or "com".
  1299.          * Example:</p>
  1300.          * <code>ClassNameInformation obj = new ClassNameInformation("org/owasp/dependencycheck/analyzer/JarAnalyzer");
  1301.          * System.out.println(obj.getName());
  1302.          * for (String p : obj.getPackageStructure())
  1303.          *     System.out.println(p);
  1304.          * </code>
  1305.          * <p>
  1306.          * Would result in:</p>
  1307.          * <code>org.owasp.dependencycheck.analyzer.JarAnalyzer
  1308.          * owasp
  1309.          * dependencycheck
  1310.          * analyzer
  1311.          * jaranalyzer</code>
  1312.          *
  1313.          * @param className a fully qualified class name
  1314.          */
  1315.         ClassNameInformation(String className) {
  1316.             name = className;
  1317.             if (name.contains("/")) {
  1318.                 final String[] tmp = StringUtils.split(className.toLowerCase(), '/');
  1319.                 int start = 0;
  1320.                 int end = 3;
  1321.                 if ("com".equals(tmp[0]) || "org".equals(tmp[0])) {
  1322.                     start = 1;
  1323.                     end = 4;
  1324.                 }
  1325.                 if (tmp.length <= end) {
  1326.                     end = tmp.length - 1;
  1327.                 }
  1328.                 packageStructure.addAll(Arrays.asList(tmp).subList(start, end + 1));
  1329.             } else {
  1330.                 packageStructure.add(name);
  1331.             }
  1332.         }

  1333.         /**
  1334.          * Get the value of name
  1335.          *
  1336.          * @return the value of name
  1337.          */
  1338.         public String getName() {
  1339.             return name;
  1340.         }

  1341.         /**
  1342.          * Set the value of name
  1343.          *
  1344.          * @param name new value of name
  1345.          */
  1346.         public void setName(String name) {
  1347.             this.name = name;
  1348.         }

  1349.         /**
  1350.          * Get the value of packageStructure
  1351.          *
  1352.          * @return the value of packageStructure
  1353.          */
  1354.         public ArrayList<String> getPackageStructure() {
  1355.             return packageStructure;
  1356.         }
  1357.     }

  1358.     /**
  1359.      * Retrieves the next temporary directory to extract an archive too.
  1360.      *
  1361.      * @return a directory
  1362.      * @throws AnalysisException thrown if unable to create temporary directory
  1363.      */
  1364.     private File getNextTempDirectory() throws AnalysisException {
  1365.         final int dirCount = DIR_COUNT.incrementAndGet();
  1366.         final File directory = new File(tempFileLocation, String.valueOf(dirCount));
  1367.         //getting an exception for some directories not being able to be created; might be because the directory already exists?
  1368.         if (directory.exists()) {
  1369.             return getNextTempDirectory();
  1370.         }
  1371.         if (!directory.mkdirs()) {
  1372.             final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
  1373.             throw new AnalysisException(msg);
  1374.         }
  1375.         return directory;
  1376.     }
  1377. }