1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 java.util.regex.Matcher;
26 import java.util.regex.Pattern;
27 import org.owasp.dependencycheck.Engine;
28 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
29 import org.owasp.dependencycheck.dependency.Dependency;
30 import org.owasp.dependencycheck.dependency.Identifier;
31 import org.owasp.dependencycheck.utils.DependencyVersion;
32 import org.owasp.dependencycheck.utils.DependencyVersionUtil;
33 import org.owasp.dependencycheck.utils.Settings;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 public class DependencyBundlingAnalyzer extends AbstractAnalyzer {
51
52
53
54
55 private static final Logger LOGGER = LoggerFactory.getLogger(DependencyBundlingAnalyzer.class);
56
57
58
59
60
61 private static final Pattern STARTING_TEXT_PATTERN = Pattern.compile("^[a-zA-Z0-9]*");
62
63
64
65
66 private boolean analyzed = false;
67
68
69
70
71
72
73
74
75 protected boolean getAnalyzed() {
76 return analyzed;
77 }
78
79
80
81
82
83
84 private static final String ANALYZER_NAME = "Dependency Bundling Analyzer";
85
86
87
88 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.FINAL;
89
90
91
92
93
94
95 @Override
96 public String getName() {
97 return ANALYZER_NAME;
98 }
99
100
101
102
103
104
105 @Override
106 public AnalysisPhase getAnalysisPhase() {
107 return ANALYSIS_PHASE;
108 }
109
110
111
112
113
114
115
116
117
118 @Override
119 public boolean supportsParallelProcessing() {
120 return false;
121 }
122
123
124
125
126
127
128
129 @Override
130 protected String getAnalyzerEnabledSettingKey() {
131 return Settings.KEYS.ANALYZER_DEPENDENCY_BUNDLING_ENABLED;
132 }
133
134
135
136
137
138
139
140
141
142
143
144 @Override
145 protected synchronized void analyzeDependency(Dependency ignore, Engine engine) throws AnalysisException {
146 if (!analyzed) {
147 analyzed = true;
148 final Set<Dependency> dependenciesToRemove = new HashSet<Dependency>();
149 final ListIterator<Dependency> mainIterator = engine.getDependencies().listIterator();
150
151 while (mainIterator.hasNext()) {
152 final Dependency dependency = mainIterator.next();
153 if (mainIterator.hasNext() && !dependenciesToRemove.contains(dependency)) {
154 final ListIterator<Dependency> subIterator = engine.getDependencies().listIterator(mainIterator.nextIndex());
155 while (subIterator.hasNext()) {
156 final Dependency nextDependency = subIterator.next();
157 if (hashesMatch(dependency, nextDependency) && !containedInWar(dependency.getFilePath())
158 && !containedInWar(nextDependency.getFilePath())) {
159 if (firstPathIsShortest(dependency.getFilePath(), nextDependency.getFilePath())) {
160 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
161 } else {
162 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
163 break;
164 }
165 } else if (isShadedJar(dependency, nextDependency)) {
166 if (dependency.getFileName().toLowerCase().endsWith("pom.xml")) {
167 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
168 nextDependency.getRelatedDependencies().remove(dependency);
169 break;
170 } else {
171 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
172 dependency.getRelatedDependencies().remove(nextDependency);
173 }
174 } else if (cpeIdentifiersMatch(dependency, nextDependency)
175 && hasSameBasePath(dependency, nextDependency)
176 && vulnCountMatches(dependency, nextDependency)
177 && fileNameMatch(dependency, nextDependency)) {
178 if (isCore(dependency, nextDependency)) {
179 mergeDependencies(dependency, nextDependency, dependenciesToRemove);
180 } else {
181 mergeDependencies(nextDependency, dependency, dependenciesToRemove);
182 break;
183 }
184 }
185 }
186 }
187 }
188
189
190 engine.getDependencies().removeAll(dependenciesToRemove);
191 }
192 }
193
194
195
196
197
198
199
200
201
202
203
204 private void mergeDependencies(final Dependency dependency, final Dependency relatedDependency, final Set<Dependency> dependenciesToRemove) {
205 dependency.addRelatedDependency(relatedDependency);
206 final Iterator<Dependency> i = relatedDependency.getRelatedDependencies().iterator();
207 while (i.hasNext()) {
208 dependency.addRelatedDependency(i.next());
209 i.remove();
210 }
211 if (dependency.getSha1sum().equals(relatedDependency.getSha1sum())) {
212 dependency.addAllProjectReferences(relatedDependency.getProjectReferences());
213 }
214 dependenciesToRemove.add(relatedDependency);
215 }
216
217
218
219
220
221
222
223
224 private String getBaseRepoPath(final String path) {
225 int pos;
226 if (path.contains("local-repo")) {
227 pos = path.indexOf("local-repo" + File.separator) + 11;
228 } else {
229 pos = path.indexOf("repository" + File.separator) + 11;
230 }
231 if (pos < 0) {
232 return path;
233 }
234 int tmp = path.indexOf(File.separator, pos);
235 if (tmp <= 0) {
236 return path;
237 }
238 if (tmp > 0) {
239 pos = tmp + 1;
240 }
241 tmp = path.indexOf(File.separator, pos);
242 if (tmp > 0) {
243 pos = tmp + 1;
244 }
245 return path.substring(0, pos);
246 }
247
248
249
250
251
252
253
254
255
256
257 private boolean fileNameMatch(Dependency dependency1, Dependency dependency2) {
258 if (dependency1 == null || dependency1.getFileName() == null
259 || dependency2 == null || dependency2.getFileName() == null) {
260 return false;
261 }
262 final String fileName1 = dependency1.getActualFile().getName();
263 final String fileName2 = dependency2.getActualFile().getName();
264
265
266 final DependencyVersion version1 = DependencyVersionUtil.parseVersion(fileName1);
267 final DependencyVersion version2 = DependencyVersionUtil.parseVersion(fileName2);
268 if (version1 != null && version2 != null && !version1.equals(version2)) {
269 return false;
270 }
271
272
273 final Matcher match1 = STARTING_TEXT_PATTERN.matcher(fileName1);
274 final Matcher match2 = STARTING_TEXT_PATTERN.matcher(fileName2);
275 if (match1.find() && match2.find()) {
276 return match1.group().equals(match2.group());
277 }
278
279 return false;
280 }
281
282
283
284
285
286
287
288
289
290
291 private boolean cpeIdentifiersMatch(Dependency dependency1, Dependency dependency2) {
292 if (dependency1 == null || dependency1.getIdentifiers() == null
293 || dependency2 == null || dependency2.getIdentifiers() == null) {
294 return false;
295 }
296 boolean matches = false;
297 int cpeCount1 = 0;
298 int cpeCount2 = 0;
299 for (Identifier i : dependency1.getIdentifiers()) {
300 if ("cpe".equals(i.getType())) {
301 cpeCount1 += 1;
302 }
303 }
304 for (Identifier i : dependency2.getIdentifiers()) {
305 if ("cpe".equals(i.getType())) {
306 cpeCount2 += 1;
307 }
308 }
309 if (cpeCount1 > 0 && cpeCount1 == cpeCount2) {
310 for (Identifier i : dependency1.getIdentifiers()) {
311 if ("cpe".equals(i.getType())) {
312 matches |= dependency2.getIdentifiers().contains(i);
313 if (!matches) {
314 break;
315 }
316 }
317 }
318 }
319 LOGGER.debug("IdentifiersMatch={} ({}, {})", matches, dependency1.getFileName(), dependency2.getFileName());
320 return matches;
321 }
322
323
324
325
326
327
328
329
330 private boolean vulnCountMatches(Dependency dependency1, Dependency dependency2) {
331 return dependency1.getVulnerabilities() != null && dependency2.getVulnerabilities() != null
332 && dependency1.getVulnerabilities().size() == dependency2.getVulnerabilities().size();
333
334 }
335
336
337
338
339
340
341
342
343 private boolean hasSameBasePath(Dependency dependency1, Dependency dependency2) {
344 if (dependency1 == null || dependency2 == null) {
345 return false;
346 }
347 final File lFile = new File(dependency1.getFilePath());
348 String left = lFile.getParent();
349 final File rFile = new File(dependency2.getFilePath());
350 String right = rFile.getParent();
351 if (left == null) {
352 return right == null;
353 } else if (right == null) {
354 return false;
355 }
356 if (left.equalsIgnoreCase(right)) {
357 return true;
358 }
359
360 if (left.matches(".*[/\\\\](repository|local-repo)[/\\\\].*") && right.matches(".*[/\\\\](repository|local-repo)[/\\\\].*")) {
361 left = getBaseRepoPath(left);
362 right = getBaseRepoPath(right);
363 }
364 if (left.equalsIgnoreCase(right)) {
365 return true;
366 }
367
368 for (Dependency child : dependency2.getRelatedDependencies()) {
369 if (hasSameBasePath(dependency1, child)) {
370 return true;
371 }
372 }
373 return false;
374 }
375
376
377
378
379
380
381
382
383
384
385 boolean isCore(Dependency left, Dependency right) {
386 final String leftName = left.getFileName().toLowerCase();
387 final String rightName = right.getFileName().toLowerCase();
388
389 final boolean returnVal;
390 if (!rightName.matches(".*\\.(tar|tgz|gz|zip|ear|war).+") && leftName.matches(".*\\.(tar|tgz|gz|zip|ear|war).+")
391 || rightName.contains("core") && !leftName.contains("core")
392 || rightName.contains("kernel") && !leftName.contains("kernel")) {
393 returnVal = false;
394 } else if (rightName.matches(".*\\.(tar|tgz|gz|zip|ear|war).+") && !leftName.matches(".*\\.(tar|tgz|gz|zip|ear|war).+")
395 || !rightName.contains("core") && leftName.contains("core")
396 || !rightName.contains("kernel") && leftName.contains("kernel")) {
397 returnVal = true;
398 } else {
399
400
401
402
403
404
405
406
407
408 returnVal = leftName.length() <= rightName.length();
409 }
410 LOGGER.debug("IsCore={} ({}, {})", returnVal, left.getFileName(), right.getFileName());
411 return returnVal;
412 }
413
414
415
416
417
418
419
420
421
422
423 private boolean hashesMatch(Dependency dependency1, Dependency dependency2) {
424 if (dependency1 == null || dependency2 == null || dependency1.getSha1sum() == null || dependency2.getSha1sum() == null) {
425 return false;
426 }
427 return dependency1.getSha1sum().equals(dependency2.getSha1sum());
428 }
429
430
431
432
433
434
435
436
437
438
439 private boolean isShadedJar(Dependency dependency, Dependency nextDependency) {
440 final String mainName = dependency.getFileName().toLowerCase();
441 final String nextName = nextDependency.getFileName().toLowerCase();
442 if (mainName.endsWith(".jar") && nextName.endsWith("pom.xml")) {
443 return dependency.getIdentifiers().containsAll(nextDependency.getIdentifiers());
444 } else if (nextName.endsWith(".jar") && mainName.endsWith("pom.xml")) {
445 return nextDependency.getIdentifiers().containsAll(dependency.getIdentifiers());
446 }
447 return false;
448 }
449
450
451
452
453
454
455
456
457
458
459 protected boolean firstPathIsShortest(String left, String right) {
460 if (left.contains("dctemp")) {
461 return false;
462 }
463 final String leftPath = left.replace('\\', '/');
464 final String rightPath = right.replace('\\', '/');
465
466 final int leftCount = countChar(leftPath, '/');
467 final int rightCount = countChar(rightPath, '/');
468 if (leftCount == rightCount) {
469 return leftPath.compareTo(rightPath) <= 0;
470 } else {
471 return leftCount < rightCount;
472 }
473 }
474
475
476
477
478
479
480
481
482 private int countChar(String string, char c) {
483 int count = 0;
484 final int max = string.length();
485 for (int i = 0; i < max; i++) {
486 if (c == string.charAt(i)) {
487 count++;
488 }
489 }
490 return count;
491 }
492
493
494
495
496
497
498
499 private boolean containedInWar(String filePath) {
500 return filePath == null ? false : filePath.matches(".*\\.(ear|war)[\\\\/].*");
501 }
502
503 }