Coverage Report - org.owasp.dependencycheck.analyzer.DependencyMergingAnalyzer
 
Classes in this File Line Coverage Branch Coverage Complexity
DependencyMergingAnalyzer
48%
36/75
33%
20/60
4.273
 
 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  
 
 20  
 import java.io.File;
 21  
 import java.util.HashSet;
 22  
 import java.util.Iterator;
 23  
 import java.util.ListIterator;
 24  
 import java.util.Set;
 25  
 import org.owasp.dependencycheck.Engine;
 26  
 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
 27  
 import org.owasp.dependencycheck.dependency.Dependency;
 28  
 import org.owasp.dependencycheck.utils.Settings;
 29  
 import org.slf4j.Logger;
 30  
 import org.slf4j.LoggerFactory;
 31  
 
 32  
 /**
 33  
  * <p>
 34  
  * This analyzer will merge dependencies, created from different source, into a
 35  
  * single dependency.</p>
 36  
  *
 37  
  * @author Jeremy Long
 38  
  */
 39  8
 public class DependencyMergingAnalyzer extends AbstractAnalyzer {
 40  
 
 41  
     //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
 42  
     /**
 43  
      * The Logger.
 44  
      */
 45  1
     private static final Logger LOGGER = LoggerFactory.getLogger(DependencyMergingAnalyzer.class);
 46  
     /**
 47  
      * a flag indicating if this analyzer has run. This analyzer only runs once.
 48  
      */
 49  8
     private boolean analyzed = false;
 50  
 
 51  
     /**
 52  
      * Returns a flag indicating if this analyzer has run. This analyzer only
 53  
      * runs once. Note this is currently only used in the unit tests.
 54  
      *
 55  
      * @return a flag indicating if this analyzer has run. This analyzer only
 56  
      * runs once
 57  
      */
 58  
     protected boolean getAnalyzed() {
 59  0
         return analyzed;
 60  
     }
 61  
 
 62  
     //</editor-fold>
 63  
     //<editor-fold defaultstate="collapsed" desc="All standard implementation details of Analyzer">
 64  
     /**
 65  
      * The name of the analyzer.
 66  
      */
 67  
     private static final String ANALYZER_NAME = "Dependency Merging Analyzer";
 68  
     /**
 69  
      * The phase that this analyzer is intended to run in.
 70  
      */
 71  1
     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.POST_INFORMATION_COLLECTION;
 72  
 
 73  
     /**
 74  
      * Returns the name of the analyzer.
 75  
      *
 76  
      * @return the name of the analyzer.
 77  
      */
 78  
     @Override
 79  
     public String getName() {
 80  26
         return ANALYZER_NAME;
 81  
     }
 82  
 
 83  
     /**
 84  
      * Returns the phase that the analyzer is intended to run in.
 85  
      *
 86  
      * @return the phase that the analyzer is intended to run in.
 87  
      */
 88  
     @Override
 89  
     public AnalysisPhase getAnalysisPhase() {
 90  6
         return ANALYSIS_PHASE;
 91  
     }
 92  
 
 93  
     /**
 94  
      * Does not support parallel processing as it only runs once and then
 95  
      * operates on <em>all</em> dependencies.
 96  
      *
 97  
      * @return whether or not parallel processing is enabled
 98  
      * @see #analyze(Dependency, Engine)
 99  
      */
 100  
     @Override
 101  
     public boolean supportsParallelProcessing() {
 102  2
         return false;
 103  
     }
 104  
 
 105  
     /**
 106  
      * <p>
 107  
      * Returns the setting key to determine if the analyzer is enabled.</p>
 108  
      *
 109  
      * @return the key for the analyzer's enabled property
 110  
      */
 111  
     @Override
 112  
     protected String getAnalyzerEnabledSettingKey() {
 113  2
         return Settings.KEYS.ANALYZER_DEPENDENCY_MERGING_ENABLED;
 114  
     }
 115  
     //</editor-fold>
 116  
 
 117  
     /**
 118  
      * Analyzes a set of dependencies. If they have been found to be the same
 119  
      * dependency created by more multiple FileTypeAnalyzers (i.e. a gemspec
 120  
      * dependency and a dependency from the Bundle Audit Analyzer. The
 121  
      * dependencies are then merged into a single reportable item.
 122  
      *
 123  
      * @param ignore this analyzer ignores the dependency being analyzed
 124  
      * @param engine the engine that is scanning the dependencies
 125  
      * @throws AnalysisException is thrown if there is an error reading the JAR
 126  
      * file.
 127  
      */
 128  
     @Override
 129  
     protected synchronized void analyzeDependency(Dependency ignore, Engine engine) throws AnalysisException {
 130  4
         if (!analyzed) {
 131  2
             analyzed = true;
 132  2
             final Set<Dependency> dependenciesToRemove = new HashSet<Dependency>();
 133  2
             final ListIterator<Dependency> mainIterator = engine.getDependencies().listIterator();
 134  
             //for (Dependency nextDependency : engine.getDependencies()) {
 135  6
             while (mainIterator.hasNext()) {
 136  4
                 final Dependency dependency = mainIterator.next();
 137  4
                 if (mainIterator.hasNext() && !dependenciesToRemove.contains(dependency)) {
 138  2
                     final ListIterator<Dependency> subIterator = engine.getDependencies().listIterator(mainIterator.nextIndex());
 139  4
                     while (subIterator.hasNext()) {
 140  2
                         final Dependency nextDependency = subIterator.next();
 141  2
                         Dependency main = null;
 142  2
                         if ((main = getMainGemspecDependency(dependency, nextDependency)) != null) {
 143  0
                             if (main == dependency) {
 144  0
                                 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
 145  
                             } else {
 146  0
                                 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
 147  0
                                 break; //since we merged into the next dependency - skip forward to the next in mainIterator
 148  
                             }
 149  2
                         } else if ((main = getMainSwiftDependency(dependency, nextDependency)) != null) {
 150  0
                             if (main == dependency) {
 151  0
                                 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
 152  
                             } else {
 153  0
                                 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
 154  0
                                 break; //since we merged into the next dependency - skip forward to the next in mainIterator
 155  
                             }
 156  
                         }
 157  2
                     }
 158  
                 }
 159  4
             }
 160  
             //removing dependencies here as ensuring correctness and avoiding ConcurrentUpdateExceptions
 161  
             // was difficult because of the inner iterator.
 162  2
             engine.getDependencies().removeAll(dependenciesToRemove);
 163  
         }
 164  4
     }
 165  
 
 166  
     /**
 167  
      * Adds the relatedDependency to the dependency's related dependencies.
 168  
      *
 169  
      * @param dependency the main dependency
 170  
      * @param relatedDependency a collection of dependencies to be removed from
 171  
      * the main analysis loop, this is the source of dependencies to remove
 172  
      * @param dependenciesToRemove a collection of dependencies that will be
 173  
      * removed from the main analysis loop, this function adds to this
 174  
      * collection
 175  
      */
 176  
     private void mergeDependencies(final Dependency dependency, final Dependency relatedDependency, final Set<Dependency> dependenciesToRemove) {
 177  0
         LOGGER.debug("Merging '{}' into '{}'", relatedDependency.getFilePath(), dependency.getFilePath());
 178  0
         dependency.addRelatedDependency(relatedDependency);
 179  0
         dependency.getVendorEvidence().getEvidence().addAll(relatedDependency.getVendorEvidence().getEvidence());
 180  0
         dependency.getProductEvidence().getEvidence().addAll(relatedDependency.getProductEvidence().getEvidence());
 181  0
         dependency.getVersionEvidence().getEvidence().addAll(relatedDependency.getVersionEvidence().getEvidence());
 182  
 
 183  0
         final Iterator<Dependency> i = relatedDependency.getRelatedDependencies().iterator();
 184  0
         while (i.hasNext()) {
 185  0
             dependency.addRelatedDependency(i.next());
 186  0
             i.remove();
 187  
         }
 188  0
         if (dependency.getSha1sum().equals(relatedDependency.getSha1sum())) {
 189  0
             dependency.addAllProjectReferences(relatedDependency.getProjectReferences());
 190  
         }
 191  0
         dependenciesToRemove.add(relatedDependency);
 192  0
     }
 193  
 
 194  
     /**
 195  
      * Bundling Ruby gems that are identified from different .gemspec files but
 196  
      * denote the same package path. This happens when Ruby bundler installs an
 197  
      * application's dependencies by running "bundle install".
 198  
      *
 199  
      * @param dependency1 dependency to compare
 200  
      * @param dependency2 dependency to compare
 201  
      * @return true if the the dependencies being analyzed appear to be the
 202  
      * same; otherwise false
 203  
      */
 204  
     private boolean isSameRubyGem(Dependency dependency1, Dependency dependency2) {
 205  2
         if (dependency1 == null || dependency2 == null
 206  2
                 || !dependency1.getFileName().endsWith(".gemspec")
 207  0
                 || !dependency2.getFileName().endsWith(".gemspec")
 208  0
                 || dependency1.getPackagePath() == null
 209  0
                 || dependency2.getPackagePath() == null) {
 210  2
             return false;
 211  
         }
 212  0
         return dependency1.getPackagePath().equalsIgnoreCase(dependency2.getPackagePath());
 213  
     }
 214  
 
 215  
     /**
 216  
      * Ruby gems installed by "bundle install" can have zero or more *.gemspec
 217  
      * files, all of which have the same packagePath and should be grouped. If
 218  
      * one of these gemspec is from <parent>/specifications/*.gemspec, because
 219  
      * it is a stub with fully resolved gem meta-data created by Ruby bundler,
 220  
      * this dependency should be the main one. Otherwise, use dependency2 as
 221  
      * main.
 222  
      *
 223  
      * This method returns null if any dependency is not from *.gemspec, or the
 224  
      * two do not have the same packagePath. In this case, they should not be
 225  
      * grouped.
 226  
      *
 227  
      * @param dependency1 dependency to compare
 228  
      * @param dependency2 dependency to compare
 229  
      * @return the main dependency; or null if a gemspec is not included in the
 230  
      * analysis
 231  
      */
 232  
     private Dependency getMainGemspecDependency(Dependency dependency1, Dependency dependency2) {
 233  2
         if (isSameRubyGem(dependency1, dependency2)) {
 234  0
             final File lFile = dependency1.getActualFile();
 235  0
             final File left = lFile.getParentFile();
 236  0
             if (left != null && left.getName().equalsIgnoreCase("specifications")) {
 237  0
                 return dependency1;
 238  
             }
 239  0
             return dependency2;
 240  
         }
 241  2
         return null;
 242  
     }
 243  
 
 244  
     /**
 245  
      * Bundling same swift dependencies with the same packagePath but identified
 246  
      * by different file type analyzers.
 247  
      *
 248  
      * @param dependency1 dependency to test
 249  
      * @param dependency2 dependency to test
 250  
      * @return <code>true</code> if the dependencies appear to be the same;
 251  
      * otherwise <code>false</code>
 252  
      */
 253  
     private boolean isSameSwiftPackage(Dependency dependency1, Dependency dependency2) {
 254  2
         if (dependency1 == null || dependency2 == null
 255  2
                 || (!dependency1.getFileName().endsWith(".podspec")
 256  2
                 && !dependency1.getFileName().equals("Package.swift"))
 257  0
                 || (!dependency2.getFileName().endsWith(".podspec")
 258  0
                 && !dependency2.getFileName().equals("Package.swift"))
 259  0
                 || dependency1.getPackagePath() == null
 260  0
                 || dependency2.getPackagePath() == null) {
 261  2
             return false;
 262  
         }
 263  0
         return dependency1.getPackagePath().equalsIgnoreCase(dependency2.getPackagePath());
 264  
     }
 265  
 
 266  
     /**
 267  
      * Determines which of the swift dependencies should be considered the
 268  
      * primary.
 269  
      *
 270  
      * @param dependency1 the first swift dependency to compare
 271  
      * @param dependency2 the second swift dependency to compare
 272  
      * @return the primary swift dependency
 273  
      */
 274  
     private Dependency getMainSwiftDependency(Dependency dependency1, Dependency dependency2) {
 275  2
         if (isSameSwiftPackage(dependency1, dependency2)) {
 276  0
             if (dependency1.getFileName().endsWith(".podspec")) {
 277  0
                 return dependency1;
 278  
             }
 279  0
             return dependency2;
 280  
         }
 281  2
         return null;
 282  
     }
 283  
 }