DependencyBundlingAnalyzer.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 org.semver4j.Semver;
  21. import org.semver4j.SemverException;
  22. import java.io.File;
  23. import java.util.Set;
  24. import java.util.regex.Matcher;
  25. import java.util.regex.Pattern;
  26. import static java.util.stream.Collectors.toSet;
  27. import javax.annotation.concurrent.ThreadSafe;
  28. import org.owasp.dependencycheck.dependency.Dependency;
  29. import org.owasp.dependencycheck.dependency.Vulnerability;
  30. import org.owasp.dependencycheck.dependency.naming.Identifier;
  31. import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
  32. import org.owasp.dependencycheck.utils.DependencyVersion;
  33. import org.owasp.dependencycheck.utils.DependencyVersionUtil;
  34. import org.owasp.dependencycheck.utils.Settings;
  35. import org.slf4j.Logger;
  36. import org.slf4j.LoggerFactory;

  37. /**
  38.  * <p>
  39.  * This analyzer ensures dependencies that should be grouped together, to remove
  40.  * excess noise from the report, are grouped. An example would be Spring, Spring
  41.  * Beans, Spring MVC, etc. If they are all for the same version and have the
  42.  * same relative path then these should be grouped into a single dependency
  43.  * under the core/main library.</p>
  44.  * <p>
  45.  * Note, this grouping only works on dependencies with identified CVE
  46.  * entries</p>
  47.  *
  48.  * @author Jeremy Long
  49.  */
  50. @ThreadSafe
  51. public class DependencyBundlingAnalyzer extends AbstractDependencyComparingAnalyzer {

  52.     /**
  53.      * The Logger.
  54.      */
  55.     private static final Logger LOGGER = LoggerFactory.getLogger(DependencyBundlingAnalyzer.class);

  56.     /**
  57.      * A pattern for obtaining the first part of a filename.
  58.      */
  59.     private static final Pattern STARTING_TEXT_PATTERN = Pattern.compile("^[a-zA-Z0-9]*");

  60.     /**
  61.      * The name of the analyzer.
  62.      */
  63.     private static final String ANALYZER_NAME = "Dependency Bundling Analyzer";
  64.     /**
  65.      * The phase that this analyzer is intended to run in.
  66.      */
  67.     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.FINAL;

  68.     /**
  69.      * Returns the name of the analyzer.
  70.      *
  71.      * @return the name of the analyzer.
  72.      */
  73.     @Override
  74.     public String getName() {
  75.         return ANALYZER_NAME;
  76.     }

  77.     /**
  78.      * Returns the phase that the analyzer is intended to run in.
  79.      *
  80.      * @return the phase that the analyzer is intended to run in.
  81.      */
  82.     @Override
  83.     public AnalysisPhase getAnalysisPhase() {
  84.         return ANALYSIS_PHASE;
  85.     }

  86.     /**
  87.      * <p>
  88.      * Returns the setting key to determine if the analyzer is enabled.</p>
  89.      *
  90.      * @return the key for the analyzer's enabled property
  91.      */
  92.     @Override
  93.     protected String getAnalyzerEnabledSettingKey() {
  94.         return Settings.KEYS.ANALYZER_DEPENDENCY_BUNDLING_ENABLED;
  95.     }

  96.     /**
  97.      * Evaluates the dependencies
  98.      *
  99.      * @param dependency a dependency to compare
  100.      * @param nextDependency a dependency to compare
  101.      * @param dependenciesToRemove a set of dependencies that will be removed
  102.      * @return true if a dependency is removed; otherwise false
  103.      */
  104.     @Override
  105.     protected boolean evaluateDependencies(final Dependency dependency, final Dependency nextDependency, final Set<Dependency> dependenciesToRemove) {
  106.         if (hashesMatch(dependency, nextDependency)) {
  107.             if (!containedInWar(dependency.getFilePath())
  108.                     && !containedInWar(nextDependency.getFilePath())) {
  109.                 if (firstPathIsShortest(dependency.getFilePath(), nextDependency.getFilePath())) {
  110.                     mergeDependencies(dependency, nextDependency, dependenciesToRemove);
  111.                 } else {
  112.                     mergeDependencies(nextDependency, dependency, dependenciesToRemove);
  113.                     return true; //since we merged into the next dependency - skip forward to the next in mainIterator
  114.                 }
  115.             }
  116.         } else if (isShadedJar(dependency, nextDependency)) {
  117.             if (dependency.getFileName().toLowerCase().endsWith("pom.xml")) {
  118.                 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
  119.                 nextDependency.removeRelatedDependencies(dependency);
  120.                 return true;
  121.             } else {
  122.                 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
  123.                 dependency.removeRelatedDependencies(nextDependency);
  124.             }
  125.         } else if (isWebJar(dependency, nextDependency)) {
  126.             if (dependency.getFileName().toLowerCase().endsWith(".js")) {
  127.                 mergeDependencies(nextDependency, dependency, dependenciesToRemove, true);
  128.                 nextDependency.removeRelatedDependencies(dependency);
  129.                 return true;
  130.             } else {
  131.                 mergeDependencies(dependency, nextDependency, dependenciesToRemove, true);
  132.                 dependency.removeRelatedDependencies(nextDependency);
  133.             }
  134.         } else if (cpeIdentifiersMatch(dependency, nextDependency)
  135.                 && hasSameBasePath(dependency, nextDependency)
  136.                 && vulnerabilitiesMatch(dependency, nextDependency)
  137.                 && fileNameMatch(dependency, nextDependency)) {
  138.             if (isCore(dependency, nextDependency)) {
  139.                 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
  140.             } else {
  141.                 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
  142.                 return true; //since we merged into the next dependency - skip forward to the next in mainIterator
  143.             }
  144.         } else if (ecosystemIs(AbstractNpmAnalyzer.NPM_DEPENDENCY_ECOSYSTEM, dependency, nextDependency)
  145.                 && namesAreEqual(dependency, nextDependency)
  146.                 && npmVersionsMatch(dependency.getVersion(), nextDependency.getVersion())) {

  147.             if (!dependency.isVirtual()) {
  148.                 DependencyMergingAnalyzer.mergeDependencies(dependency, nextDependency, dependenciesToRemove);
  149.             } else {
  150.                 DependencyMergingAnalyzer.mergeDependencies(nextDependency, dependency, dependenciesToRemove);
  151.                 return true;
  152.             }
  153.         }
  154.         return false;
  155.     }

  156.     /**
  157.      * Adds the relatedDependency to the dependency's related dependencies.
  158.      *
  159.      * @param dependency the main dependency
  160.      * @param relatedDependency a collection of dependencies to be removed from
  161.      * the main analysis loop, this is the source of dependencies to remove
  162.      * @param dependenciesToRemove a collection of dependencies that will be
  163.      * removed from the main analysis loop, this function adds to this
  164.      * collection
  165.      */
  166.     public static void mergeDependencies(final Dependency dependency,
  167.             final Dependency relatedDependency, final Set<Dependency> dependenciesToRemove) {
  168.         mergeDependencies(dependency, relatedDependency, dependenciesToRemove, false);
  169.     }

  170.     /**
  171.      * Adds the relatedDependency to the dependency's related dependencies.
  172.      *
  173.      * @param dependency the main dependency
  174.      * @param relatedDependency a collection of dependencies to be removed from
  175.      * the main analysis loop, this is the source of dependencies to remove
  176.      * @param dependenciesToRemove a collection of dependencies that will be
  177.      * removed from the main analysis loop, this function adds to this
  178.      * collection
  179.      * @param copyVulnsAndIds whether or not identifiers and vulnerabilities are
  180.      * copied
  181.      */
  182.     public static void mergeDependencies(final Dependency dependency,
  183.             final Dependency relatedDependency, final Set<Dependency> dependenciesToRemove,
  184.             final boolean copyVulnsAndIds) {
  185.         dependency.addRelatedDependency(relatedDependency);
  186.         relatedDependency.getRelatedDependencies()
  187.                 .forEach(dependency::addRelatedDependency);
  188.         relatedDependency.clearRelatedDependencies();

  189.         if (copyVulnsAndIds) {
  190.             relatedDependency.getSoftwareIdentifiers()
  191.                     .forEach(dependency::addSoftwareIdentifier);
  192.             relatedDependency.getVulnerableSoftwareIdentifiers()
  193.                     .forEach(dependency::addVulnerableSoftwareIdentifier);
  194.             relatedDependency.getVulnerabilities()
  195.                     .forEach(dependency::addVulnerability);
  196.         }
  197.         //TODO this null check was added for #1296 - but I believe this to be related to virtual dependencies
  198.         //  we may want to merge project references on virtual dependencies...
  199.         if (dependency.getSha1sum() != null && dependency.getSha1sum().equals(relatedDependency.getSha1sum())) {
  200.             dependency.addAllProjectReferences(relatedDependency.getProjectReferences());
  201.             dependency.addAllIncludedBy(relatedDependency.getIncludedBy());
  202.         }
  203.         if (dependenciesToRemove != null) {
  204.             dependenciesToRemove.add(relatedDependency);
  205.         }
  206.     }

  207.     /**
  208.      * Attempts to trim a maven repo to a common base path. This is typically
  209.      * [drive]\[repo_location]\repository\[path1]\[path2].
  210.      *
  211.      * @param path the path to trim
  212.      * @param repo the name of the local maven repository
  213.      * @return a string representing the base path.
  214.      */
  215.     private String getBaseRepoPath(final String path, final String repo) {
  216.         int pos = path.indexOf(repo + File.separator) + repo.length() + 1;
  217.         if (pos < repo.length() + 1) {
  218.             return path;
  219.         }
  220.         int tmp = path.indexOf(File.separator, pos);
  221.         if (tmp <= 0) {
  222.             return path;
  223.         }
  224.         pos = tmp + 1;
  225.         tmp = path.indexOf(File.separator, pos);
  226.         if (tmp > 0) {
  227.             pos = tmp + 1;
  228.         }
  229.         return path.substring(0, pos);
  230.     }

  231.     /**
  232.      * Returns true if the file names (and version if it exists) of the two
  233.      * dependencies are sufficiently similar.
  234.      *
  235.      * @param dependency1 a dependency2 to compare
  236.      * @param dependency2 a dependency2 to compare
  237.      * @return true if the identifiers in the two supplied dependencies are
  238.      * equal
  239.      */
  240.     private boolean fileNameMatch(Dependency dependency1, Dependency dependency2) {
  241.         if (dependency1 == null || dependency1.getFileName() == null
  242.                 || dependency2 == null || dependency2.getFileName() == null) {
  243.             return false;
  244.         }
  245.         final String fileName1 = dependency1.getActualFile().getName();
  246.         final String fileName2 = dependency2.getActualFile().getName();

  247.         //version check
  248.         final DependencyVersion version1 = DependencyVersionUtil.parseVersion(fileName1);
  249.         final DependencyVersion version2 = DependencyVersionUtil.parseVersion(fileName2);
  250.         if (version1 != null && version2 != null && !version1.equals(version2)) {
  251.             return false;
  252.         }

  253.         //filename check
  254.         final Matcher match1 = STARTING_TEXT_PATTERN.matcher(fileName1);
  255.         final Matcher match2 = STARTING_TEXT_PATTERN.matcher(fileName2);
  256.         if (match1.find() && match2.find()) {
  257.             return match1.group().equals(match2.group());
  258.         }

  259.         return false;
  260.     }

  261.     /**
  262.      * Returns true if the CPE identifiers in the two supplied dependencies are
  263.      * equal.
  264.      *
  265.      * @param dependency1 a dependency2 to compare
  266.      * @param dependency2 a dependency2 to compare
  267.      * @return true if the identifiers in the two supplied dependencies are
  268.      * equal
  269.      */
  270.     private boolean cpeIdentifiersMatch(Dependency dependency1, Dependency dependency2) {
  271.         if (dependency1 == null || dependency1.getVulnerableSoftwareIdentifiers() == null
  272.                 || dependency2 == null || dependency2.getVulnerableSoftwareIdentifiers() == null) {
  273.             return false;
  274.         }
  275.         boolean matches = false;
  276.         final int cpeCount1 = dependency1.getVulnerableSoftwareIdentifiers().size();
  277.         final int cpeCount2 = dependency2.getVulnerableSoftwareIdentifiers().size();
  278.         if (cpeCount1 > 0 && cpeCount1 == cpeCount2) {
  279.             for (Identifier i : dependency1.getVulnerableSoftwareIdentifiers()) {
  280.                 matches |= dependency2.getVulnerableSoftwareIdentifiers().contains(i);
  281.                 if (!matches) {
  282.                     break;
  283.                 }
  284.             }
  285.         }
  286.         LOGGER.trace("IdentifiersMatch={} ({}, {})", matches, dependency1.getFileName(), dependency2.getFileName());
  287.         return matches;
  288.     }

  289.     /**
  290.      * Returns true if the two dependencies have the same vulnerabilities.
  291.      *
  292.      * @param dependency1 a dependency2 to compare
  293.      * @param dependency2 a dependency2 to compare
  294.      * @return true if the two dependencies have the same vulnerabilities
  295.      */
  296.     private boolean vulnerabilitiesMatch(Dependency dependency1, Dependency dependency2) {
  297.         final Set<Vulnerability> one = dependency1.getVulnerabilities();
  298.         final Set<Vulnerability> two = dependency2.getVulnerabilities();
  299.         return one != null && two != null
  300.                 && one.size() == two.size()
  301.                 && one.containsAll(two);
  302.     }

  303.     /**
  304.      * Determines if the two dependencies have the same base path.
  305.      *
  306.      * @param dependency1 a Dependency object
  307.      * @param dependency2 a Dependency object
  308.      * @return true if the base paths of the dependencies are identical
  309.      */
  310.     private boolean hasSameBasePath(Dependency dependency1, Dependency dependency2) {
  311.         if (dependency1 == null || dependency2 == null) {
  312.             return false;
  313.         }
  314.         final File lFile = new File(dependency1.getFilePath());
  315.         String left = lFile.getParent();
  316.         final File rFile = new File(dependency2.getFilePath());
  317.         String right = rFile.getParent();
  318.         if (left == null) {
  319.             return right == null;
  320.         } else if (right == null) {
  321.             return false;
  322.         }
  323.         if (left.equalsIgnoreCase(right)) {
  324.             return true;
  325.         }
  326.         final String localRepo = getSettings().getString(Settings.KEYS.MAVEN_LOCAL_REPO);
  327.         final Pattern p;
  328.         if (localRepo == null) {
  329.             p = Pattern.compile(".*[/\\\\](?<repo>repository|local-repo)[/\\\\].*");
  330.         } else {
  331.             final File f = new File(localRepo);
  332.             final String dir = f.getName();
  333.             p = Pattern.compile(".*[/\\\\](?<repo>repository|local-repo|" + Pattern.quote(dir) + ")[/\\\\].*");
  334.         }
  335.         final Matcher mleft = p.matcher(left);
  336.         final Matcher mright = p.matcher(right);
  337.         if (mleft.find() && mright.find()) {
  338.             left = getBaseRepoPath(left, mleft.group("repo"));
  339.             right = getBaseRepoPath(right, mright.group("repo"));
  340.         }

  341.         if (left.equalsIgnoreCase(right)) {
  342.             return true;
  343.         }
  344.         //new code
  345.         for (Dependency child : dependency2.getRelatedDependencies()) {
  346.             if (hasSameBasePath(child, dependency1)) {
  347.                 return true;
  348.             }
  349.         }
  350.         return false;
  351.     }

  352.     /**
  353.      * This is likely a very broken attempt at determining if the 'left'
  354.      * dependency is the 'core' library in comparison to the 'right' library.
  355.      *
  356.      * @param left the dependency to test
  357.      * @param right the dependency to test against
  358.      * @return a boolean indicating whether or not the left dependency should be
  359.      * considered the "core" version.
  360.      */
  361.     protected boolean isCore(Dependency left, Dependency right) {
  362.         final String leftName = left.getFileName().toLowerCase();
  363.         final String rightName = right.getFileName().toLowerCase();

  364.         final boolean returnVal;
  365.         //TODO - should we get rid of this merging? It removes a true BOM...

  366.         if (left.isVirtual() && !right.isVirtual()) {
  367.             returnVal = true;
  368.         } else if (!left.isVirtual() && right.isVirtual()) {
  369.             returnVal = false;
  370.         } else if ((!rightName.matches(".*\\.(tar|tgz|gz|zip|ear|war|rpm).+") && leftName.matches(".*\\.(tar|tgz|gz|zip|ear|war|rpm).+"))
  371.                 || (rightName.contains("core") && !leftName.contains("core"))
  372.                 || (rightName.contains("kernel") && !leftName.contains("kernel"))
  373.                 || (rightName.contains("server") && !leftName.contains("server"))
  374.                 || (rightName.contains("project") && !leftName.contains("project"))
  375.                 || (rightName.contains("engine") && !leftName.contains("engine"))
  376.                 || (rightName.contains("akka-stream") && !leftName.contains("akka-stream"))
  377.                 || (rightName.contains("netty-transport") && !leftName.contains("netty-transport"))) {
  378.             returnVal = false;
  379.         } else if ((rightName.matches(".*\\.(tar|tgz|gz|zip|ear|war|rpm).+") && !leftName.matches(".*\\.(tar|tgz|gz|zip|ear|war|rpm).+"))
  380.                 || (!rightName.contains("core") && leftName.contains("core"))
  381.                 || (!rightName.contains("kernel") && leftName.contains("kernel"))
  382.                 || (!rightName.contains("server") && leftName.contains("server"))
  383.                 || (!rightName.contains("project") && leftName.contains("project"))
  384.                 || (!rightName.contains("engine") && leftName.contains("engine"))
  385.                 || (!rightName.contains("akka-stream") && leftName.contains("akka-stream"))
  386.                 || (!rightName.contains("netty-transport") && leftName.contains("netty-transport"))) {
  387.             returnVal = true;
  388.         } else {
  389.             /*
  390.              * considered splitting the names up and comparing the components,
  391.              * but decided that the file name length should be sufficient as the
  392.              * "core" component, if this follows a normal naming protocol should
  393.              * be shorter:
  394.              * axis2-saaj-1.4.1.jar
  395.              * axis2-1.4.1.jar       <-----
  396.              * axis2-kernel-1.4.1.jar
  397.              */
  398.             returnVal = leftName.length() <= rightName.length();
  399.         }
  400.         LOGGER.debug("IsCore={} ({}, {})", returnVal, left.getFileName(), right.getFileName());
  401.         return returnVal;
  402.     }

  403.     /**
  404.      * Compares the SHA1 hashes of two dependencies to determine if they are
  405.      * equal.
  406.      *
  407.      * @param dependency1 a dependency object to compare
  408.      * @param dependency2 a dependency object to compare
  409.      * @return true if the sha1 hashes of the two dependencies match; otherwise
  410.      * false
  411.      */
  412.     private boolean hashesMatch(Dependency dependency1, Dependency dependency2) {
  413.         if (dependency1 == null || dependency2 == null || dependency1.getSha1sum() == null || dependency2.getSha1sum() == null) {
  414.             return false;
  415.         }
  416.         return dependency1.getSha1sum().equals(dependency2.getSha1sum());
  417.     }

  418.     /**
  419.      * Determines if a JS file is from a webjar dependency.
  420.      *
  421.      * @param dependency the first dependency to compare
  422.      * @param nextDependency the second dependency to compare
  423.      * @return <code>true</code> if the dependency is a web jar and the next
  424.      * dependency is a JS file from the web jar; otherwise <code>false</code>
  425.      */
  426.     protected boolean isWebJar(Dependency dependency, Dependency nextDependency) {
  427.         if (dependency == null || dependency.getFileName() == null
  428.                 || nextDependency == null || nextDependency.getFileName() == null
  429.                 || dependency.getSoftwareIdentifiers().isEmpty()
  430.                 || nextDependency.getSoftwareIdentifiers().isEmpty()) {
  431.             return false;
  432.         }
  433.         final String mainName = dependency.getFileName().toLowerCase();
  434.         final String nextName = nextDependency.getFileName().toLowerCase();
  435.         if (mainName.endsWith(".jar") && nextName.endsWith(".js") && nextName.startsWith(mainName)) {
  436.             return dependency.getSoftwareIdentifiers()
  437.                     .stream().map(Identifier::getValue).collect(toSet())
  438.                     .containsAll(nextDependency.getSoftwareIdentifiers().stream().map(this::identifierToWebJarForComparison).collect(toSet()));
  439.         } else if (nextName.endsWith(".jar") && mainName.endsWith("js") && mainName.startsWith(nextName)) {
  440.             return nextDependency.getSoftwareIdentifiers()
  441.                     .stream().map(Identifier::getValue).collect(toSet())
  442.                     .containsAll(dependency.getSoftwareIdentifiers().stream().map(this::identifierToWebJarForComparison).collect(toSet()));
  443.         }
  444.         return false;
  445.     }

  446.     /**
  447.      * Attempts to convert a given JavaScript identifier to a web jar CPE.
  448.      *
  449.      * @param id a JavaScript CPE
  450.      * @return a Maven CPE for a web jar if conversion is possible; otherwise
  451.      * the original CPE is returned
  452.      */
  453.     private String identifierToWebJarForComparison(Identifier id) {
  454.         if (id instanceof PurlIdentifier) {
  455.             final PurlIdentifier pid = (PurlIdentifier) id;
  456.             try {
  457.                 final Identifier nid = new PurlIdentifier("maven", "org.webjars", pid.getName(), pid.getVersion(), pid.getConfidence());
  458.                 return nid.getValue();
  459.             } catch (MalformedPackageURLException ex) {
  460.                 LOGGER.debug("Unable to build webjar purl id", ex);
  461.                 return id.getValue();
  462.             }
  463.         } else {
  464.             return id == null ? "" : id.getValue();
  465.         }
  466.     }

  467.     /**
  468.      * Determines if the jar is shaded and the created pom.xml identified the
  469.      * same CPE as the jar - if so, the pom.xml dependency should be removed.
  470.      *
  471.      * @param dependency a dependency to check
  472.      * @param nextDependency another dependency to check
  473.      * @return true if on of the dependencies is a pom.xml and the identifiers
  474.      * between the two collections match; otherwise false
  475.      */
  476.     protected boolean isShadedJar(Dependency dependency, Dependency nextDependency) {
  477.         if (dependency == null || dependency.getFileName() == null
  478.                 || nextDependency == null || nextDependency.getFileName() == null
  479.                 || dependency.getSoftwareIdentifiers().isEmpty()
  480.                 || nextDependency.getSoftwareIdentifiers().isEmpty()) {
  481.             return false;
  482.         }
  483.         final String mainName = dependency.getFileName().toLowerCase();
  484.         final String nextName = nextDependency.getFileName().toLowerCase();
  485.         if (mainName.endsWith(".jar") && nextName.endsWith("pom.xml")) {
  486.             return dependency.getSoftwareIdentifiers().containsAll(nextDependency.getSoftwareIdentifiers());
  487.         } else if (nextName.endsWith(".jar") && mainName.endsWith("pom.xml")) {
  488.             return nextDependency.getSoftwareIdentifiers().containsAll(dependency.getSoftwareIdentifiers());
  489.         }
  490.         return false;
  491.     }

  492.     /**
  493.      * Determines which path is shortest; if path lengths are equal then we use
  494.      * compareTo of the string method to determine if the first path is smaller.
  495.      *
  496.      * @param left the first path to compare
  497.      * @param right the second path to compare
  498.      * @return <code>true</code> if the leftPath is the shortest; otherwise
  499.      * <code>false</code>
  500.      */
  501.     public static boolean firstPathIsShortest(String left, String right) {
  502.         if (left.contains("dctemp") && !right.contains("dctemp")) {
  503.             return false;
  504.         }
  505.         final String leftPath = left.replace('\\', '/');
  506.         final String rightPath = right.replace('\\', '/');

  507.         final int leftCount = countChar(leftPath, '/');
  508.         final int rightCount = countChar(rightPath, '/');
  509.         if (leftCount == rightCount) {
  510.             return leftPath.compareTo(rightPath) <= 0;
  511.         } else {
  512.             return leftCount < rightCount;
  513.         }
  514.     }

  515.     /**
  516.      * Counts the number of times the character is present in the string.
  517.      *
  518.      * @param string the string to count the characters in
  519.      * @param c the character to count
  520.      * @return the number of times the character is present in the string
  521.      */
  522.     private static int countChar(String string, char c) {
  523.         int count = 0;
  524.         final int max = string.length();
  525.         for (int i = 0; i < max; i++) {
  526.             if (c == string.charAt(i)) {
  527.                 count++;
  528.             }
  529.         }
  530.         return count;
  531.     }

  532.     /**
  533.      * Checks if the given file path is contained within a war or ear file.
  534.      *
  535.      * @param filePath the file path to check
  536.      * @return true if the path contains '.war\' or '.ear\'.
  537.      */
  538.     private boolean containedInWar(String filePath) {
  539.         return filePath != null && filePath.matches(".*\\.(ear|war)[\\\\/].*");
  540.     }

  541.     /**
  542.      * Determine if the dependency ecosystem is equal in the given dependencies.
  543.      *
  544.      * @param ecoSystem the ecosystem to validate against
  545.      * @param dependency a dependency to compare
  546.      * @param nextDependency a dependency to compare
  547.      * @return true if the ecosystem is equal in both dependencies; otherwise
  548.      * false
  549.      */
  550.     private boolean ecosystemIs(String ecoSystem, Dependency dependency, Dependency nextDependency) {
  551.         return ecoSystem.equals(dependency.getEcosystem()) && ecoSystem.equals(nextDependency.getEcosystem());
  552.     }

  553.     /**
  554.      * Determine if the dependency name is equal in the given dependencies.
  555.      *
  556.      * @param dependency a dependency to compare
  557.      * @param nextDependency a dependency to compare
  558.      * @return true if the name is equal in both dependencies; otherwise false
  559.      */
  560.     private boolean namesAreEqual(Dependency dependency, Dependency nextDependency) {
  561.         return dependency.getName() != null && dependency.getName().equals(nextDependency.getName());
  562.     }

  563.     /**
  564.      * Determine if the dependency version is equal in the given dependencies.
  565.      * This method attempts to evaluate version range checks.
  566.      *
  567.      * @param current a dependency version to compare
  568.      * @param next a dependency version to compare
  569.      * @return true if the version is equal in both dependencies; otherwise
  570.      * false
  571.      */
  572.     public static boolean npmVersionsMatch(String current, String next) {
  573.         String left = current;
  574.         String right = next;
  575.         if (left == null || right == null) {
  576.             return false;
  577.         }
  578.         if (left.equals(right) || "*".equals(left) || "*".equals(right)) {
  579.             return true;
  580.         }
  581.         if (left.contains(" ")) { // we have a version string from package.json
  582.             if (right.contains(" ")) { // we can't evaluate this ">=1.5.4 <2.0.0" vs "2 || 3"
  583.                 return false;
  584.             }
  585.             if (!right.matches("^\\d.*$")) {
  586.                 right = stripLeadingNonNumeric(right);
  587.                 if (right == null) {
  588.                     return false;
  589.                 }
  590.             }
  591.             try {
  592.                 final Semver v = new Semver(right);
  593.                 return v.satisfies(left);
  594.             } catch (SemverException ex) {
  595.                 LOGGER.trace("ignore", ex);
  596.             }
  597.         } else {
  598.             if (!left.matches("^\\d.*$")) {
  599.                 left = stripLeadingNonNumeric(left);
  600.                 if (left == null || left.isEmpty()) {
  601.                     return false;
  602.                 }
  603.             }
  604.             try {
  605.                 Semver v = new Semver(left);
  606.                 if (!right.isEmpty() && v.satisfies(right)) {
  607.                     return true;
  608.                 }
  609.                 if (!right.contains(" ")) {
  610.                     left = current;
  611.                     right = stripLeadingNonNumeric(right);
  612.                     if (right != null) {
  613.                         v = new Semver(right);
  614.                         return v.satisfies(left);
  615.                     }
  616.                 }
  617.             } catch (SemverException ex) {
  618.                 LOGGER.trace("ignore", ex);
  619.             } catch (NullPointerException ex) {
  620.                 LOGGER.error("SemVer comparison error: left:\"{}\", right:\"{}\"", left, right);
  621.                 LOGGER.debug("SemVer comparison resulted in NPE", ex);
  622.             }
  623.         }
  624.         return false;
  625.     }

  626.     /**
  627.      * Strips leading non-numeric values from the start of the string. If no
  628.      * numbers are present this will return null.
  629.      *
  630.      * @param str the string to modify
  631.      * @return the string without leading non-numeric characters
  632.      */
  633.     private static String stripLeadingNonNumeric(String str) {
  634.         for (int x = 0; x < str.length(); x++) {
  635.             if (Character.isDigit(str.codePointAt(x))) {
  636.                 return str.substring(x);
  637.             }
  638.         }
  639.         return null;
  640.     }

  641. }