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.BufferedInputStream;
21 import java.io.File;
22 import java.io.FileFilter;
23 import java.io.FileInputStream;
24 import java.io.FileNotFoundException;
25 import java.io.FileOutputStream;
26 import java.io.IOException;
27 import java.util.Collections;
28 import java.util.Enumeration;
29 import java.util.List;
30 import java.util.Set;
31
32 import org.apache.commons.compress.archivers.ArchiveEntry;
33 import org.apache.commons.compress.archivers.ArchiveInputStream;
34 import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
35 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
36 import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
37 import org.apache.commons.compress.archivers.zip.ZipFile;
38 import org.apache.commons.compress.compressors.CompressorInputStream;
39 import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
40 import org.apache.commons.compress.compressors.bzip2.BZip2Utils;
41 import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
42 import org.apache.commons.compress.compressors.gzip.GzipUtils;
43 import org.apache.commons.compress.utils.IOUtils;
44
45 import org.owasp.dependencycheck.Engine;
46 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
47 import org.owasp.dependencycheck.analyzer.exception.ArchiveExtractionException;
48 import org.owasp.dependencycheck.dependency.Dependency;
49 import org.owasp.dependencycheck.exception.InitializationException;
50 import org.owasp.dependencycheck.utils.FileFilterBuilder;
51 import org.owasp.dependencycheck.utils.FileUtils;
52 import org.owasp.dependencycheck.utils.Settings;
53
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
56
57
58
59
60
61
62
63
64 public class ArchiveAnalyzer extends AbstractFileTypeAnalyzer {
65
66
67
68
69 private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveAnalyzer.class);
70
71
72
73
74 private static int dirCount = 0;
75
76
77
78 private File tempFileLocation = null;
79
80
81
82
83 private static final int MAX_SCAN_DEPTH = Settings.getInt("archive.scan.depth", 3);
84
85
86
87 private int scanDepth = 0;
88
89
90
91
92
93 private static final String ANALYZER_NAME = "Archive Analyzer";
94
95
96
97 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INITIAL;
98
99
100
101 private static final Set<String> ZIPPABLES = newHashSet("zip", "ear", "war", "jar", "sar", "apk", "nupkg");
102
103
104
105
106
107 private static final Set<String> EXTENSIONS = newHashSet("tar", "gz", "tgz", "bz2", "tbz2");
108
109
110
111
112
113 private static final FileFilter REMOVE_FROM_ANALYSIS = FileFilterBuilder.newInstance().addExtensions("zip", "tar", "gz", "tgz", "bz2", "tbz2")
114 .build();
115
116 static {
117 final String additionalZipExt = Settings.getString(Settings.KEYS.ADDITIONAL_ZIP_EXTENSIONS);
118 if (additionalZipExt != null) {
119 final String[] ext = additionalZipExt.split("\\s*,\\s*");
120 Collections.addAll(ZIPPABLES, ext);
121 }
122 EXTENSIONS.addAll(ZIPPABLES);
123 }
124
125
126
127
128 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
129
130 @Override
131 protected FileFilter getFileFilter() {
132 return FILTER;
133 }
134
135
136
137
138 private static final FileFilter ZIP_FILTER = FileFilterBuilder.newInstance().addExtensions("zip").build();
139
140
141
142
143
144
145 @Override
146 public String getName() {
147 return ANALYZER_NAME;
148 }
149
150
151
152
153
154
155 @Override
156 public AnalysisPhase getAnalysisPhase() {
157 return ANALYSIS_PHASE;
158 }
159
160
161
162
163
164
165
166
167 @Override
168 protected String getAnalyzerEnabledSettingKey() {
169 return Settings.KEYS.ANALYZER_ARCHIVE_ENABLED;
170 }
171
172
173
174
175
176
177
178 @Override
179 public void initializeFileTypeAnalyzer() throws InitializationException {
180 try {
181 final File baseDir = Settings.getTempDirectory();
182 tempFileLocation = File.createTempFile("check", "tmp", baseDir);
183 if (!tempFileLocation.delete()) {
184 setEnabled(false);
185 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
186 throw new InitializationException(msg);
187 }
188 if (!tempFileLocation.mkdirs()) {
189 setEnabled(false);
190 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
191 throw new InitializationException(msg);
192 }
193 } catch (IOException ex) {
194 setEnabled(false);
195 throw new InitializationException("Unable to create a temporary file", ex);
196 }
197 }
198
199
200
201
202
203
204
205
206 @Override
207 public void closeAnalyzer() throws Exception {
208 if (tempFileLocation != null && tempFileLocation.exists()) {
209 LOGGER.debug("Attempting to delete temporary files");
210 final boolean success = FileUtils.delete(tempFileLocation);
211 if (!success && tempFileLocation.exists()) {
212 final String[] l = tempFileLocation.list();
213 if (l != null && l.length > 0) {
214 LOGGER.warn("Failed to delete some temporary files, see the log for more details");
215 }
216 }
217 }
218 }
219
220
221
222
223
224
225
226
227 @Override
228 public boolean supportsParallelProcessing() {
229 return false;
230 }
231
232
233
234
235
236
237
238
239
240
241 @Override
242 public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
243 final File f = new File(dependency.getActualFilePath());
244 final File tmpDir = getNextTempDirectory();
245 extractFiles(f, tmpDir, engine);
246
247
248 final List<Dependency> dependencySet = findMoreDependencies(engine, tmpDir);
249
250 if (dependencySet != null && !dependencySet.isEmpty()) {
251 for (Dependency d : dependencySet) {
252 if (d.getFilePath().startsWith(tmpDir.getAbsolutePath())) {
253
254 final String displayPath = String.format("%s%s",
255 dependency.getFilePath(),
256 d.getActualFilePath().substring(tmpDir.getAbsolutePath().length()));
257 final String displayName = String.format("%s: %s",
258 dependency.getFileName(),
259 d.getFileName());
260 d.setFilePath(displayPath);
261 d.setFileName(displayName);
262 d.setProjectReferences(dependency.getProjectReferences());
263
264
265
266 if (this.accept(d.getActualFile()) && scanDepth < MAX_SCAN_DEPTH) {
267 scanDepth += 1;
268 analyze(d, engine);
269 scanDepth -= 1;
270 }
271 } else {
272 for (Dependency sub : dependencySet) {
273 if (sub.getFilePath().startsWith(tmpDir.getAbsolutePath())) {
274 final String displayPath = String.format("%s%s",
275 dependency.getFilePath(),
276 sub.getActualFilePath().substring(tmpDir.getAbsolutePath().length()));
277 final String displayName = String.format("%s: %s",
278 dependency.getFileName(),
279 sub.getFileName());
280 sub.setFilePath(displayPath);
281 sub.setFileName(displayName);
282 }
283 }
284 }
285 }
286 }
287 if (REMOVE_FROM_ANALYSIS.accept(dependency.getActualFile())) {
288 addDisguisedJarsToDependencies(dependency, engine);
289 engine.getDependencies().remove(dependency);
290 }
291 Collections.sort(engine.getDependencies());
292 }
293
294
295
296
297
298
299
300
301
302 private void addDisguisedJarsToDependencies(Dependency dependency, Engine engine) throws AnalysisException {
303 if (ZIP_FILTER.accept(dependency.getActualFile()) && isZipFileActuallyJarFile(dependency)) {
304 final File tdir = getNextTempDirectory();
305 final String fileName = dependency.getFileName();
306
307 LOGGER.info("The zip file '{}' appears to be a JAR file, making a copy and analyzing it as a JAR.", fileName);
308 final File tmpLoc = new File(tdir, fileName.substring(0, fileName.length() - 3) + "jar");
309
310
311 final String archiveSha1 = dependency.getSha1sum();
312 try {
313 dependency.setSha1sum("");
314 org.apache.commons.io.FileUtils.copyFile(dependency.getActualFile(), tmpLoc);
315 final List<Dependency> dependencySet = findMoreDependencies(engine, tmpLoc);
316 if (dependencySet != null && !dependencySet.isEmpty()) {
317 for (Dependency d : dependencySet) {
318
319 if (d.getActualFile().equals(tmpLoc)) {
320 d.setFilePath(dependency.getFilePath());
321 d.setDisplayFileName(dependency.getFileName());
322 } else {
323 for (Dependency sub : d.getRelatedDependencies()) {
324 if (sub.getActualFile().equals(tmpLoc)) {
325 sub.setFilePath(dependency.getFilePath());
326 sub.setDisplayFileName(dependency.getFileName());
327 }
328 }
329 }
330 }
331 }
332 } catch (IOException ex) {
333 LOGGER.debug("Unable to perform deep copy on '{}'", dependency.getActualFile().getPath(), ex);
334 } finally {
335 dependency.setSha1sum(archiveSha1);
336 }
337 }
338 }
339
340
341
342
343
344
345
346
347 private static List<Dependency> findMoreDependencies(Engine engine, File file) {
348 final List<Dependency> added = engine.scan(file);
349 return added;
350 }
351
352
353
354
355
356
357
358 private File getNextTempDirectory() throws AnalysisException {
359 dirCount += 1;
360 final File directory = new File(tempFileLocation, String.valueOf(dirCount));
361
362 if (directory.exists()) {
363 return getNextTempDirectory();
364 }
365 if (!directory.mkdirs()) {
366 final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
367 throw new AnalysisException(msg);
368 }
369 return directory;
370 }
371
372
373
374
375
376
377
378
379
380 private void extractFiles(File archive, File destination, Engine engine) throws AnalysisException {
381 if (archive != null && destination != null) {
382 String archiveExt = FileUtils.getFileExtension(archive.getName());
383 if (archiveExt == null) {
384 return;
385 }
386 archiveExt = archiveExt.toLowerCase();
387
388 final FileInputStream fis;
389 try {
390 fis = new FileInputStream(archive);
391 } catch (FileNotFoundException ex) {
392 LOGGER.debug("", ex);
393 throw new AnalysisException("Archive file was not found.", ex);
394 }
395 BufferedInputStream in = null;
396 ZipArchiveInputStream zin = null;
397 TarArchiveInputStream tin = null;
398 GzipCompressorInputStream gin = null;
399 BZip2CompressorInputStream bzin = null;
400 try {
401 if (ZIPPABLES.contains(archiveExt)) {
402 in = new BufferedInputStream(fis);
403 ensureReadableJar(archiveExt, in);
404 zin = new ZipArchiveInputStream(in);
405 extractArchive(zin, destination, engine);
406 } else if ("tar".equals(archiveExt)) {
407 in = new BufferedInputStream(fis);
408 tin = new TarArchiveInputStream(in);
409 extractArchive(tin, destination, engine);
410 } else if ("gz".equals(archiveExt) || "tgz".equals(archiveExt)) {
411 final String uncompressedName = GzipUtils.getUncompressedFilename(archive.getName());
412 final File f = new File(destination, uncompressedName);
413 if (engine.accept(f)) {
414 in = new BufferedInputStream(fis);
415 gin = new GzipCompressorInputStream(in);
416 decompressFile(gin, f);
417 }
418 } else if ("bz2".equals(archiveExt) || "tbz2".equals(archiveExt)) {
419 final String uncompressedName = BZip2Utils.getUncompressedFilename(archive.getName());
420 final File f = new File(destination, uncompressedName);
421 if (engine.accept(f)) {
422 in = new BufferedInputStream(fis);
423 bzin = new BZip2CompressorInputStream(in);
424 decompressFile(bzin, f);
425 }
426 }
427 } catch (ArchiveExtractionException ex) {
428 LOGGER.warn("Exception extracting archive '{}'.", archive.getName());
429 LOGGER.debug("", ex);
430 } catch (IOException ex) {
431 LOGGER.warn("Exception reading archive '{}'.", archive.getName());
432 LOGGER.debug("", ex);
433 } finally {
434
435
436 FileUtils.close(fis);
437 FileUtils.close(in);
438 FileUtils.close(zin);
439 FileUtils.close(tin);
440 FileUtils.close(gin);
441 FileUtils.close(bzin);
442 }
443 }
444 }
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459 private void ensureReadableJar(final String archiveExt, BufferedInputStream in) throws IOException {
460 if ("jar".equals(archiveExt) && in.markSupported()) {
461 in.mark(7);
462 final byte[] b = new byte[7];
463 final int read = in.read(b);
464 if (read == 7
465 && b[0] == '#'
466 && b[1] == '!'
467 && b[2] == '/'
468 && b[3] == 'b'
469 && b[4] == 'i'
470 && b[5] == 'n'
471 && b[6] == '/') {
472 boolean stillLooking = true;
473 int chr, nxtChr;
474 while (stillLooking && (chr = in.read()) != -1) {
475 if (chr == '\n' || chr == '\r') {
476 in.mark(4);
477 if ((chr = in.read()) != -1) {
478 if (chr == 'P' && (chr = in.read()) != -1) {
479 if (chr == 'K' && (chr = in.read()) != -1) {
480 if ((chr == 3 || chr == 5 || chr == 7) && (nxtChr = in.read()) != -1) {
481 if (nxtChr == chr + 1) {
482 stillLooking = false;
483 in.reset();
484 }
485 }
486 }
487 }
488 }
489 }
490 }
491 } else {
492 in.reset();
493 }
494 }
495 }
496
497
498
499
500
501
502
503
504
505
506 private void extractArchive(ArchiveInputStream input, File destination, Engine engine) throws ArchiveExtractionException {
507 ArchiveEntry entry;
508 try {
509 while ((entry = input.getNextEntry()) != null) {
510 final File file = new File(destination, entry.getName());
511 if (entry.isDirectory()) {
512 if (!file.exists() && !file.mkdirs()) {
513 final String msg = String.format("Unable to create directory '%s'.", file.getAbsolutePath());
514 throw new AnalysisException(msg);
515 }
516 } else if (engine.accept(file)) {
517 extractAcceptedFile(input, file);
518 }
519 }
520 } catch (Throwable ex) {
521 throw new ArchiveExtractionException(ex);
522 } finally {
523 FileUtils.close(input);
524 }
525 }
526
527
528
529
530
531
532
533
534 private static void extractAcceptedFile(ArchiveInputStream input, File file) throws AnalysisException {
535 LOGGER.debug("Extracting '{}'", file.getPath());
536 FileOutputStream fos = null;
537 try {
538 final File parent = file.getParentFile();
539 if (!parent.isDirectory() && !parent.mkdirs()) {
540 final String msg = String.format("Unable to build directory '%s'.", parent.getAbsolutePath());
541 throw new AnalysisException(msg);
542 }
543 fos = new FileOutputStream(file);
544 IOUtils.copy(input, fos);
545 } catch (FileNotFoundException ex) {
546 LOGGER.debug("", ex);
547 final String msg = String.format("Unable to find file '%s'.", file.getName());
548 throw new AnalysisException(msg, ex);
549 } catch (IOException ex) {
550 LOGGER.debug("", ex);
551 final String msg = String.format("IO Exception while parsing file '%s'.", file.getName());
552 throw new AnalysisException(msg, ex);
553 } finally {
554 FileUtils.close(fos);
555 }
556 }
557
558
559
560
561
562
563
564
565
566 private void decompressFile(CompressorInputStream inputStream, File outputFile) throws ArchiveExtractionException {
567 LOGGER.debug("Decompressing '{}'", outputFile.getPath());
568 FileOutputStream out = null;
569 try {
570 out = new FileOutputStream(outputFile);
571 IOUtils.copy(inputStream, out);
572 } catch (FileNotFoundException ex) {
573 LOGGER.debug("", ex);
574 throw new ArchiveExtractionException(ex);
575 } catch (IOException ex) {
576 LOGGER.debug("", ex);
577 throw new ArchiveExtractionException(ex);
578 } finally {
579 FileUtils.close(out);
580 }
581 }
582
583
584
585
586
587
588
589 private boolean isZipFileActuallyJarFile(Dependency dependency) {
590 boolean isJar = false;
591 ZipFile zip = null;
592 try {
593 zip = new ZipFile(dependency.getActualFilePath());
594 if (zip.getEntry("META-INF/MANIFEST.MF") != null
595 || zip.getEntry("META-INF/maven") != null) {
596 final Enumeration<ZipArchiveEntry> entries = zip.getEntries();
597 while (entries.hasMoreElements()) {
598 final ZipArchiveEntry entry = entries.nextElement();
599 if (!entry.isDirectory()) {
600 final String name = entry.getName().toLowerCase();
601 if (name.endsWith(".class")) {
602 isJar = true;
603 break;
604 }
605 }
606 }
607 }
608 } catch (IOException ex) {
609 LOGGER.debug("Unable to unzip zip file '{}'", dependency.getFilePath(), ex);
610 } finally {
611 ZipFile.closeQuietly(zip);
612 }
613
614 return isJar;
615 }
616 }