##### Coverage Report - org.owasp.dependencycheck.analyzer.DependencyBundlingAnalyzer

Classes in this File | Line Coverage | Branch Coverage | Complexity
DependencyBundlingAnalyzer |
33% | 37/112
26% | 25/96
| 6.9;6.9

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 | 1 |
     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 | 1 |
         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 | 1 |
         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 | 1 |
         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 | 1 |
         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 | 132 |
             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 | 9 |
             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 | 9 |
             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 | 6 |
             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 | 9 |
             if (!analyzed) {
125 | 1 |
                 analyzed = true;
126 | 1 |
                 final Set<Dependency> dependenciesToRemove = new HashSet<Dependency>();
127 | 1 |
                 final ListIterator<Dependency> mainIterator = engine.getDependencies().listIterator();
128 |  |
                 //for (Dependency nextDependency : engine.getDependencies()) {
129 | 4 |
                 while (mainIterator.hasNext()) {
130 | 3 |
                     final Dependency dependency = mainIterator.next();
131 | 3 |
                     if (mainIterator.hasNext()) {
132 | 2 |
                         final ListIterator<Dependency> subIterator = engine.getDependencies().listIterator(mainIterator.nextIndex());
133 | 5 |
                         while (subIterator.hasNext()) {
134 | 3 |
                             final Dependency nextDependency = subIterator.next();
135 |  |
136 | 3 |
                             if (identifiersMatch(dependency, nextDependency)
137 |  |
                                     && hasSameBasePath(dependency, nextDependency)
138 |  |
                                     && fileNameMatch(dependency, nextDependency)) {
139 |  |
140 | 0 |
                                 if (isCore(dependency, nextDependency)) {
141 | 0 |
                                     dependency.addRelatedDependency(nextDependency);
142 |  |
                                     //move any "related dependencies" to the new "parent" dependency
143 | 0 |
                                     final Iterator<Dependency> i = nextDependency.getRelatedDependencies().iterator();
144 | 0 |
                                     while (i.hasNext()) {
145 | 0 |
                                         dependency.addRelatedDependency(i.next());
146 | 0 |
                                         i.remove();
147 |  |
                                     }
148 | 0 |
                                     dependenciesToRemove.add(nextDependency);
149 | 0 |
                                 } else {
150 | 0 |
                                     nextDependency.addRelatedDependency(dependency);
151 |  |
                                     //move any "related dependencies" to the new "parent" dependency
152 | 0 |
                                     final Iterator<Dependency> i = dependency.getRelatedDependencies().iterator();
153 | 0 |
                                     while (i.hasNext()) {
154 | 0 |
                                         nextDependency.addRelatedDependency(i.next());
155 | 0 |
                                         i.remove();
156 |  |
                                     }
157 | 0 |
                                     dependenciesToRemove.add(dependency);
158 |  |
                                 }
159 |  |
                             }
160 | 3 |
                         }
161 |  |
                     }
162 | 3 |
                 }
163 |  |
                 //removing dependencies here as ensuring correctness and avoiding ConcurrentUpdateExceptions
164 |  |
                 // was difficult because of the inner iterator.
165 | 1 |
                 for (Dependency d : dependenciesToRemove) {
166 | 0 |
                     engine.getDependencies().remove(d);
167 |  |
                 }
168 |  |
             }
169 | 9 |
         }
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 | 0 |
             int pos = path.indexOf("repository" + File.separator) + 11;
180 | 0 |
             if (pos < 0) {
181 | 0 |
                 return path;
182 |  |
             }
183 | 0 |
             int tmp = path.indexOf(File.separator, pos);
184 | 0 |
             if (tmp <= 0) {
185 | 0 |
                 return path;
186 |  |
             }
187 | 0 |
             if (tmp > 0) {
188 | 0 |
                 pos = tmp + 1;
189 |  |
             }
190 | 0 |
             tmp = path.indexOf(File.separator, pos);
191 | 0 |
             if (tmp > 0) {
192 | 0 |
                 pos = tmp + 1;
193 |  |
             }
194 | 0 |
             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 | 0 |
             if (dependency1 == null || dependency1.getFileName() == null
208 |  |
                     || dependency2 == null || dependency2.getFileName() == null) {
209 | 0 |
                 return false;
210 |  |
             }
211 | 0 |
             String fileName1 = dependency1.getFileName();
212 | 0 |
             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 | 0 |
             final File one = new File(fileName1);
217 | 0 |
             final File two = new File(fileName2);
218 | 0 |
             final String oneParent = one.getParent();
219 | 0 |
             final String twoParent = two.getParent();
220 | 0 |
             if (oneParent != null) {
221 | 0 |
                 if (twoParent != null && oneParent.equals(twoParent)) {
222 | 0 |
                     fileName1 = one.getName();
223 | 0 |
                     fileName2 = two.getName();
224 |  |
                 } else {
225 | 0 |
                     return false;
226 |  |
                 }
227 | 0 |
             } else if (twoParent != null) {
228 | 0 |
                 return false;
229 |  |
             }
230 |  |
231 |  |
             //version check
232 | 0 |
             final DependencyVersion version1 = DependencyVersionUtil.parseVersion(fileName1);
233 | 0 |
             final DependencyVersion version2 = DependencyVersionUtil.parseVersion(fileName2);
234 | 0 |
             if (version1 != null && version2 != null) {
235 | 0 |
                 if (!version1.equals(version2)) {
236 | 0 |
                     return false;
237 |  |
                 }
238 |  |
             }
239 |  |
240 |  |
             //filename check
241 | 0 |
             final Matcher match1 = STARTING_TEXT_PATTERN.matcher(fileName1);
242 | 0 |
             final Matcher match2 = STARTING_TEXT_PATTERN.matcher(fileName2);
243 | 0 |
             if (match1.find() && match2.find()) {
244 | 0 |
                 return match1.group().equals(match2.group());
245 |  |
             }
246 |  |
247 | 0 |
             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 | 3 |
             if (dependency1 == null || dependency1.getIdentifiers() == null
261 |  |
                     || dependency2 == null || dependency2.getIdentifiers() == null) {
262 | 0 |
                 return false;
263 |  |
             }
264 | 3 |
             final boolean matches = dependency1.getIdentifiers().size() > 0
265 |  |
                     && dependency2.getIdentifiers().equals(dependency1.getIdentifiers());
266 | 3 |
             if (LogUtils.isVerboseLoggingEnabled()) {
267 | 0 |
                 final String msg = String.format("IdentifiersMatch=%s (%s, %s)", matches, dependency1.getFileName(), dependency2.getFileName());
268 | 0 |
                 Logger.getLogger(DependencyBundlingAnalyzer.class.getName()).log(Level.FINE, msg);
269 |  |
             }
270 | 3 |
             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 | 1 |
             if (dependency1 == null || dependency2 == null) {
282 | 0 |
                 return false;
283 |  |
             }
284 | 1 |
             final File lFile = new File(dependency1.getFilePath());
285 | 1 |
             String left = lFile.getParent();
286 | 1 |
             final File rFile = new File(dependency2.getFilePath());
287 | 1 |
             String right = rFile.getParent();
288 | 1 |
             if (left == null) {
289 | 0 |
                 if (right == null) {
290 | 0 |
                     return true;
291 |  |
                 }
292 | 0 |
                 return false;
293 |  |
             }
294 | 1 |
             if (left.equalsIgnoreCase(right)) {
295 | 0 |
                 return true;
296 |  |
             }
297 | 1 |
             if (left.matches(".*[/\\\\]repository[/\\\\].*") && right.matches(".*[/\\\\]repository[/\\\\].*")) {
298 | 0 |
                 left = getBaseRepoPath(left);
299 | 0 |
                 right = getBaseRepoPath(right);
300 |  |
             }
301 | 1 |
             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 | 0 |
             final String leftName = left.getFileName().toLowerCase();
315 | 0 |
             final String rightName = right.getFileName().toLowerCase();
316 |  |
             final boolean returnVal;
317 | 0 |
             if (rightName.contains("core") && !leftName.contains("core")) {
318 | 0 |
                 returnVal = false;
319 | 0 |
             } else if (!rightName.contains("core") && leftName.contains("core")) {
320 | 0 |
                 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 | 0 |
                 if (leftName.length() > rightName.length()) {
332 | 0 |
                     returnVal = false;
333 |  |
                 } else {
334 | 0 |
                     returnVal = true;
335 |  |
                 }
336 |  |
             }
337 | 0 |
             if (LogUtils.isVerboseLoggingEnabled()) {
338 | 0 |
                 final String msg = String.format("IsCore=%s (%s, %s)", returnVal, left.getFileName(), right.getFileName());
339 | 0 |
                 Logger.getLogger(DependencyBundlingAnalyzer.class.getName()).log(Level.FINE, msg);
340 |  |
             }
341 | 0 |
             return returnVal;
342 |  |
         }
343 |  |
     }