View Javadoc
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  public class DependencyMergingAnalyzer extends AbstractAnalyzer {
40  
41      //<editor-fold defaultstate="collapsed" desc="Constants and Member Variables">
42      /**
43       * The Logger.
44       */
45      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      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          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      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          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          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         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         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         if (!analyzed) {
131             analyzed = true;
132             final Set<Dependency> dependenciesToRemove = new HashSet<Dependency>();
133             final ListIterator<Dependency> mainIterator = engine.getDependencies().listIterator();
134             //for (Dependency nextDependency : engine.getDependencies()) {
135             while (mainIterator.hasNext()) {
136                 final Dependency dependency = mainIterator.next();
137                 if (mainIterator.hasNext() && !dependenciesToRemove.contains(dependency)) {
138                     final ListIterator<Dependency> subIterator = engine.getDependencies().listIterator(mainIterator.nextIndex());
139                     while (subIterator.hasNext()) {
140                         final Dependency nextDependency = subIterator.next();
141                         Dependency main = null;
142                         if ((main = getMainGemspecDependency(dependency, nextDependency)) != null) {
143                             if (main == dependency) {
144                                 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
145                             } else {
146                                 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
147                                 break; //since we merged into the next dependency - skip forward to the next in mainIterator
148                             }
149                         } else if ((main = getMainSwiftDependency(dependency, nextDependency)) != null) {
150                             if (main == dependency) {
151                                 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
152                             } else {
153                                 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
154                                 break; //since we merged into the next dependency - skip forward to the next in mainIterator
155                             }
156                         }
157                     }
158                 }
159             }
160             //removing dependencies here as ensuring correctness and avoiding ConcurrentUpdateExceptions
161             // was difficult because of the inner iterator.
162             engine.getDependencies().removeAll(dependenciesToRemove);
163         }
164     }
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         LOGGER.debug("Merging '{}' into '{}'", relatedDependency.getFilePath(), dependency.getFilePath());
178         dependency.addRelatedDependency(relatedDependency);
179         dependency.getVendorEvidence().getEvidence().addAll(relatedDependency.getVendorEvidence().getEvidence());
180         dependency.getProductEvidence().getEvidence().addAll(relatedDependency.getProductEvidence().getEvidence());
181         dependency.getVersionEvidence().getEvidence().addAll(relatedDependency.getVersionEvidence().getEvidence());
182 
183         final Iterator<Dependency> i = relatedDependency.getRelatedDependencies().iterator();
184         while (i.hasNext()) {
185             dependency.addRelatedDependency(i.next());
186             i.remove();
187         }
188         if (dependency.getSha1sum().equals(relatedDependency.getSha1sum())) {
189             dependency.addAllProjectReferences(relatedDependency.getProjectReferences());
190         }
191         dependenciesToRemove.add(relatedDependency);
192     }
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         if (dependency1 == null || dependency2 == null
206                 || !dependency1.getFileName().endsWith(".gemspec")
207                 || !dependency2.getFileName().endsWith(".gemspec")
208                 || dependency1.getPackagePath() == null
209                 || dependency2.getPackagePath() == null) {
210             return false;
211         }
212         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         if (isSameRubyGem(dependency1, dependency2)) {
234             final File lFile = dependency1.getActualFile();
235             final File left = lFile.getParentFile();
236             if (left != null && left.getName().equalsIgnoreCase("specifications")) {
237                 return dependency1;
238             }
239             return dependency2;
240         }
241         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         if (dependency1 == null || dependency2 == null
255                 || (!dependency1.getFileName().endsWith(".podspec")
256                 && !dependency1.getFileName().equals("Package.swift"))
257                 || (!dependency2.getFileName().endsWith(".podspec")
258                 && !dependency2.getFileName().equals("Package.swift"))
259                 || dependency1.getPackagePath() == null
260                 || dependency2.getPackagePath() == null) {
261             return false;
262         }
263         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         if (isSameSwiftPackage(dependency1, dependency2)) {
276             if (dependency1.getFileName().endsWith(".podspec")) {
277                 return dependency1;
278             }
279             return dependency2;
280         }
281         return null;
282     }
283 }