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 }