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