View Javadoc

    1   /*
    2    * This file is part of dependency-check-core.
    3    *
    4    * Dependency-check-core is free software: you can redistribute it and/or modify it
    5    * under the terms of the GNU General Public License as published by the Free
    6    * Software Foundation, either version 3 of the License, or (at your option) any
    7    * later version.
    8    *
    9    * Dependency-check-core is distributed in the hope that it will be useful, but
    10   * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    11   * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
    12   * details.
    13   *
    14   * You should have received a copy of the GNU General Public License along with
    15   * dependency-check-core. If not, see http://www.gnu.org/licenses/.
    16   *
    17   * Copyright (c) 2012 Jeremy Long. All Rights Reserved.
    18   */
    19  package org.owasp.dependencycheck.analyzer;
    20
    21  import java.io.File;
    22  import java.util.HashSet;
    23  import java.util.Iterator;
    24  import java.util.ListIterator;
    25  import java.util.Set;
    26  import java.util.logging.Level;
    27  import java.util.logging.Logger;
    28  import java.util.regex.Matcher;
    29  import java.util.regex.Pattern;
    30  import org.owasp.dependencycheck.Engine;
    31  import org.owasp.dependencycheck.dependency.Dependency;
    32  import org.owasp.dependencycheck.utils.DependencyVersion;
    33  import org.owasp.dependencycheck.utils.DependencyVersionUtil;
    34  import org.owasp.dependencycheck.utils.LogUtils;
    35
    36  /**
    37   * <p>This analyzer ensures dependencies that should be grouped together, to
    38   * remove excess noise from the report, are grouped. An example would be Spring,
    39   * Spring Beans, Spring MVC, etc. If they are all for the same version and have
    40   * the same relative path then these should be grouped into a single dependency
    41   * under the core/main library.</p>
    42   * <p>Note, this grouping only works on dependencies with identified CVE
    43   * entries</p>
    44   *
    45   * @author Jeremy Long (jeremy.long@owasp.org)
    46   */
    47  public class DependencyBundlingAnalyzer extends AbstractAnalyzer implements Analyzer {
    48
    49      //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
    50      /**
    51       * A pattern for obtaining the first part of a filename.
    52       */
    53      private static final Pattern STARTING_TEXT_PATTERN = Pattern.compile("^[a-zA-Z]*");
    54      /**
    55       * a flag indicating if this analyzer has run. This analyzer only runs once.
    56       */
    57      private boolean analyzed = false;
    58      //</editor-fold>
    59      //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
    60      /**
    61       * The set of file extensions supported by this analyzer.
    62       */
    63      private static final Set<String> EXTENSIONS = null;
    64      /**
    65       * The name of the analyzer.
    66       */
    67      private static final String ANALYZER_NAME = "Dependency Bundling Analyzer";
    68      /**
    69       * The phase that this analyzer is intended to run in.
    70       */
    71      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.PRE_FINDING_ANALYSIS;
    72
    73      /**
    74       * Returns a list of file EXTENSIONS supported by this analyzer.
    75       *
    76       * @return a list of file EXTENSIONS supported by this analyzer.
    77       */
    78      public Set<String> getSupportedExtensions() {
    79          return EXTENSIONS;
    80      }
    81
    82      /**
    83       * Returns the name of the analyzer.
    84       *
    85       * @return the name of the analyzer.
    86       */
    87      public String getName() {
    88          return ANALYZER_NAME;
    89      }
    90
    91      /**
    92       * Returns whether or not this analyzer can process the given extension.
    93       *
    94       * @param extension the file extension to test for support
    95       * @return whether or not the specified file extension is supported by this
    96       * analyzer.
    97       */
    98      public boolean supportsExtension(String extension) {
    99          return true;
    100     }
    101
    102     /**
    103      * Returns the phase that the analyzer is intended to run in.
    104      *
    105      * @return the phase that the analyzer is intended to run in.
    106      */
    107     public AnalysisPhase getAnalysisPhase() {
    108         return ANALYSIS_PHASE;
    109     }
    110     //</editor-fold>
    111
    112     /**
    113      * Analyzes a set of dependencies. If they have been found to have the same
    114      * base path and the same set of identifiers they are likely related. The
    115      * related dependencies are bundled into a single reportable item.
    116      *
    117      * @param ignore this analyzer ignores the dependency being analyzed
    118      * @param engine the engine that is scanning the dependencies
    119      * @throws AnalysisException is thrown if there is an error reading the JAR
    120      * file.
    121      */
    122     @Override
    123     public void analyze(Dependency ignore, Engine engine) throws AnalysisException {
    124         if (!analyzed) {
    125             analyzed = true;
    126             final Set<Dependency> dependenciesToRemove = new HashSet<Dependency>();
    127             final ListIterator<Dependency> mainIterator = engine.getDependencies().listIterator();
    128             //for (Dependency nextDependency : engine.getDependencies()) {
    129             while (mainIterator.hasNext()) {
    130                 final Dependency dependency = mainIterator.next();
    131                 if (mainIterator.hasNext()) {
    132                     final ListIterator<Dependency> subIterator = engine.getDependencies().listIterator(mainIterator.nextIndex());
    133                     while (subIterator.hasNext()) {
    134                         final Dependency nextDependency = subIterator.next();
    135
    136                         if (identifiersMatch(dependency, nextDependency)
    137                                 && hasSameBasePath(dependency, nextDependency)
    138                                 && fileNameMatch(dependency, nextDependency)) {
    139
    140                             if (isCore(dependency, nextDependency)) {
    141                                 dependency.addRelatedDependency(nextDependency);
    142                                 //move any "related dependencies" to the new "parent" dependency
    143                                 final Iterator<Dependency> i = nextDependency.getRelatedDependencies().iterator();
    144                                 while (i.hasNext()) {
    145                                     dependency.addRelatedDependency(i.next());
    146                                     i.remove();
    147                                 }
    148                                 dependenciesToRemove.add(nextDependency);
    149                             } else {
    150                                 nextDependency.addRelatedDependency(dependency);
    151                                 //move any "related dependencies" to the new "parent" dependency
    152                                 final Iterator<Dependency> i = dependency.getRelatedDependencies().iterator();
    153                                 while (i.hasNext()) {
    154                                     nextDependency.addRelatedDependency(i.next());
    155                                     i.remove();
    156                                 }
    157                                 dependenciesToRemove.add(dependency);
    158                             }
    159                         }
    160                     }
    161                 }
    162             }
    163             //removing dependencies here as ensuring correctness and avoiding ConcurrentUpdateExceptions
    164             // was difficult because of the inner iterator.
    165             for (Dependency d : dependenciesToRemove) {
    166                 engine.getDependencies().remove(d);
    167             }
    168         }
    169     }
    170
    171     /**
    172      * Attempts to trim a maven repo to a common base path. This is typically
    173      * [drive]\[repo_location]\repository\[path1]\[path2].
    174      *
    175      * @param path the path to trim
    176      * @return a string representing the base path.
    177      */
    178     private String getBaseRepoPath(final String path) {
    179         int pos = path.indexOf("repository" + File.separator) + 11;
    180         if (pos < 0) {
    181             return path;
    182         }
    183         int tmp = path.indexOf(File.separator, pos);
    184         if (tmp <= 0) {
    185             return path;
    186         }
    187         if (tmp > 0) {
    188             pos = tmp + 1;
    189         }
    190         tmp = path.indexOf(File.separator, pos);
    191         if (tmp > 0) {
    192             pos = tmp + 1;
    193         }
    194         return path.substring(0, pos);
    195     }
    196
    197     /**
    198      * Returns true if the file names (and version if it exists) of the two
    199      * dependencies are sufficiently similiar.
    200      *
    201      * @param dependency1 a dependency2 to compare
    202      * @param dependency2 a dependency2 to compare
    203      * @return true if the identifiers in the two supplied dependencies are
    204      * equal
    205      */
    206     private boolean fileNameMatch(Dependency dependency1, Dependency dependency2) {
    207         if (dependency1 == null || dependency1.getFileName() == null
    208                 || dependency2 == null || dependency2.getFileName() == null) {
    209             return false;
    210         }
    211         String fileName1 = dependency1.getFileName();
    212         String fileName2 = dependency2.getFileName();
    213
    214         //update to deal with archive analyzer, the starting name maybe the same
    215         // as this is incorrectly looking at the starting path
    216         final File one = new File(fileName1);
    217         final File two = new File(fileName2);
    218         final String oneParent = one.getParent();
    219         final String twoParent = two.getParent();
    220         if (oneParent != null) {
    221             if (twoParent != null && oneParent.equals(twoParent)) {
    222                 fileName1 = one.getName();
    223                 fileName2 = two.getName();
    224             } else {
    225                 return false;
    226             }
    227         } else if (twoParent != null) {
    228             return false;
    229         }
    230
    231         //version check
    232         final DependencyVersion version1 = DependencyVersionUtil.parseVersion(fileName1);
    233         final DependencyVersion version2 = DependencyVersionUtil.parseVersion(fileName2);
    234         if (version1 != null && version2 != null) {
    235             if (!version1.equals(version2)) {
    236                 return false;
    237             }
    238         }
    239
    240         //filename check
    241         final Matcher match1 = STARTING_TEXT_PATTERN.matcher(fileName1);
    242         final Matcher match2 = STARTING_TEXT_PATTERN.matcher(fileName2);
    243         if (match1.find() && match2.find()) {
    244             return match1.group().equals(match2.group());
    245         }
    246
    247         return false;
    248     }
    249
    250     /**
    251      * Returns true if the identifiers in the two supplied dependencies are
    252      * equal.
    253      *
    254      * @param dependency1 a dependency2 to compare
    255      * @param dependency2 a dependency2 to compare
    256      * @return true if the identifiers in the two supplied dependencies are
    257      * equal
    258      */
    259     private boolean identifiersMatch(Dependency dependency1, Dependency dependency2) {
    260         if (dependency1 == null || dependency1.getIdentifiers() == null
    261                 || dependency2 == null || dependency2.getIdentifiers() == null) {
    262             return false;
    263         }
    264         final boolean matches = dependency1.getIdentifiers().size() > 0
    265                 && dependency2.getIdentifiers().equals(dependency1.getIdentifiers());
    266         if (LogUtils.isVerboseLoggingEnabled()) {
    267             final String msg = String.format("IdentifiersMatch=%s (%s, %s)", matches, dependency1.getFileName(), dependency2.getFileName());
    268             Logger.getLogger(DependencyBundlingAnalyzer.class.getName()).log(Level.FINE, msg);
    269         }
    270         return matches;
    271     }
    272
    273     /**
    274      * Determines if the two dependencies have the same base path.
    275      *
    276      * @param dependency1 a Dependency object
    277      * @param dependency2 a Dependency object
    278      * @return true if the base paths of the dependencies are identical
    279      */
    280     private boolean hasSameBasePath(Dependency dependency1, Dependency dependency2) {
    281         if (dependency1 == null || dependency2 == null) {
    282             return false;
    283         }
    284         final File lFile = new File(dependency1.getFilePath());
    285         String left = lFile.getParent();
    286         final File rFile = new File(dependency2.getFilePath());
    287         String right = rFile.getParent();
    288         if (left == null) {
    289             if (right == null) {
    290                 return true;
    291             }
    292             return false;
    293         }
    294         if (left.equalsIgnoreCase(right)) {
    295             return true;
    296         }
    297         if (left.matches(".*[/\\\\]repository[/\\\\].*") && right.matches(".*[/\\\\]repository[/\\\\].*")) {
    298             left = getBaseRepoPath(left);
    299             right = getBaseRepoPath(right);
    300         }
    301         return left.equalsIgnoreCase(right);
    302     }
    303
    304     /**
    305      * This is likely a very broken attempt at determining if the 'left'
    306      * dependency is the 'core' library in comparison to the 'right' library.
    307      *
    308      * @param left the dependency to test
    309      * @param right the dependency to test against
    310      * @return a boolean indicating whether or not the left dependency should be
    311      * considered the "core" version.
    312      */
    313     private boolean isCore(Dependency left, Dependency right) {
    314         final String leftName = left.getFileName().toLowerCase();
    315         final String rightName = right.getFileName().toLowerCase();
    316         final boolean returnVal;
    317         if (rightName.contains("core") && !leftName.contains("core")) {
    318             returnVal = false;
    319         } else if (!rightName.contains("core") && leftName.contains("core")) {
    320             returnVal = true;
    321         } else {
    322             /*
    323              * considered splitting the names up and comparing the components,
    324              * but decided that the file name length should be sufficient as the
    325              * "core" component, if this follows a normal namming protocol should
    326              * be shorter:
    327              * axis2-saaj-1.4.1.jar
    328              * axis2-1.4.1.jar       <-----
    329              * axis2-kernal-1.4.1.jar
    330              */
    331             if (leftName.length() > rightName.length()) {
    332                 returnVal = false;
    333             } else {
    334                 returnVal = true;
    335             }
    336         }
    337         if (LogUtils.isVerboseLoggingEnabled()) {
    338             final String msg = String.format("IsCore=%s (%s, %s)", returnVal, left.getFileName(), right.getFileName());
    339             Logger.getLogger(DependencyBundlingAnalyzer.class.getName()).log(Level.FINE, msg);
    340         }
    341         return returnVal;
    342     }
    343 }