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