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.utils.FileFilterBuilder;
53 import org.owasp.dependencycheck.utils.FileUtils;
54 import org.owasp.dependencycheck.utils.Settings;
55
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
58
59
60
61
62
63
64
65
66 public class ArchiveAnalyzer extends AbstractFileTypeAnalyzer {
67
68
69
70
71 private static final Logger LOGGER = LoggerFactory.getLogger(ArchiveAnalyzer.class);
72
73
74
75 private static int dirCount = 0;
76
77
78
79 private File tempFileLocation = null;
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 private static final Set<String> EXTENSIONS = newHashSet("tar", "gz", "tgz", "bz2", "tbz2");
107
108
109
110
111 private static final FileFilter REMOVE_FROM_ANALYSIS = FileFilterBuilder.newInstance().addExtensions("zip", "tar", "gz", "tgz", "bz2", "tbz2")
112 .build();
113
114 static {
115 final String additionalZipExt = Settings.getString(Settings.KEYS.ADDITIONAL_ZIP_EXTENSIONS);
116 if (additionalZipExt != null) {
117 final Set<String> ext = new HashSet<String>(Collections.singletonList(additionalZipExt));
118 ZIPPABLES.addAll(ext);
119 }
120 EXTENSIONS.addAll(ZIPPABLES);
121 }
122
123
124
125
126 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(EXTENSIONS).build();
127
128 @Override
129 protected FileFilter getFileFilter() {
130 return FILTER;
131 }
132
133
134
135
136 private static final FileFilter ZIP_FILTER = FileFilterBuilder.newInstance().addExtensions("zip").build();
137
138
139
140
141
142
143 @Override
144 public String getName() {
145 return ANALYZER_NAME;
146 }
147
148
149
150
151
152
153 @Override
154 public AnalysisPhase getAnalysisPhase() {
155 return ANALYSIS_PHASE;
156 }
157
158
159
160
161
162
163
164 @Override
165 protected String getAnalyzerEnabledSettingKey() {
166 return Settings.KEYS.ANALYZER_ARCHIVE_ENABLED;
167 }
168
169
170
171
172
173
174 @Override
175 public void initializeFileTypeAnalyzer() throws Exception {
176 final File baseDir = Settings.getTempDirectory();
177 tempFileLocation = File.createTempFile("check", "tmp", baseDir);
178 if (!tempFileLocation.delete()) {
179 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
180 throw new AnalysisException(msg);
181 }
182 if (!tempFileLocation.mkdirs()) {
183 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
184 throw new AnalysisException(msg);
185 }
186 }
187
188
189
190
191
192
193 @Override
194 public void close() throws Exception {
195 if (tempFileLocation != null && tempFileLocation.exists()) {
196 LOGGER.debug("Attempting to delete temporary files");
197 final boolean success = FileUtils.delete(tempFileLocation);
198 if (!success && tempFileLocation.exists() && tempFileLocation.list().length > 0) {
199 LOGGER.warn("Failed to delete some temporary files, see the log for more details");
200 }
201 }
202 }
203
204
205
206
207
208
209
210
211
212 @Override
213 public void analyzeFileType(Dependency dependency, Engine engine) throws AnalysisException {
214 final File f = new File(dependency.getActualFilePath());
215 final File tmpDir = getNextTempDirectory();
216 extractFiles(f, tmpDir, engine);
217
218
219 final Set<Dependency> dependencySet = findMoreDependencies(engine, tmpDir);
220 if (!dependencySet.isEmpty()) {
221 for (Dependency d : dependencySet) {
222
223 final String displayPath = String.format("%s%s",
224 dependency.getFilePath(),
225 d.getActualFilePath().substring(tmpDir.getAbsolutePath().length()));
226 final String displayName = String.format("%s: %s",
227 dependency.getFileName(),
228 d.getFileName());
229 d.setFilePath(displayPath);
230 d.setFileName(displayName);
231
232
233
234 if (this.accept(d.getActualFile()) && scanDepth < MAX_SCAN_DEPTH) {
235 scanDepth += 1;
236 analyze(d, engine);
237 scanDepth -= 1;
238 }
239 }
240 }
241 if (REMOVE_FROM_ANALYSIS.accept(dependency.getActualFile())) {
242 addDisguisedJarsToDependencies(dependency, engine);
243 engine.getDependencies().remove(dependency);
244 }
245 Collections.sort(engine.getDependencies());
246 }
247
248
249
250
251
252
253
254
255 private void addDisguisedJarsToDependencies(Dependency dependency, Engine engine) throws AnalysisException {
256 if (ZIP_FILTER.accept(dependency.getActualFile()) && isZipFileActuallyJarFile(dependency)) {
257 final File tdir = getNextTempDirectory();
258 final String fileName = dependency.getFileName();
259
260 LOGGER.info("The zip file '{}' appears to be a JAR file, making a copy and analyzing it as a JAR.", fileName);
261
262 final File tmpLoc = new File(tdir, fileName.substring(0, fileName.length() - 3) + "jar");
263 try {
264 org.apache.commons.io.FileUtils.copyFile(tdir, tmpLoc);
265 final Set<Dependency> dependencySet = findMoreDependencies(engine, tmpLoc);
266 if (!dependencySet.isEmpty()) {
267 if (dependencySet.size() != 1) {
268 LOGGER.info("Deep copy of ZIP to JAR file resulted in more than one dependency?");
269 }
270 for (Dependency d : dependencySet) {
271
272 d.setFilePath(dependency.getFilePath());
273 d.setDisplayFileName(dependency.getFileName());
274 }
275 }
276 } catch (IOException ex) {
277 LOGGER.debug("Unable to perform deep copy on '{}'", dependency.getActualFile().getPath(), ex);
278 }
279 }
280 }
281
282
283
284 private static final Set<Dependency> EMPTY_DEPENDENCY_SET = Collections.emptySet();
285
286
287
288
289
290
291
292
293 private static Set<Dependency> findMoreDependencies(Engine engine, File file) {
294 final List<Dependency> before = new ArrayList<Dependency>(engine.getDependencies());
295 engine.scan(file);
296 final List<Dependency> after = engine.getDependencies();
297 final boolean sizeChanged = before.size() != after.size();
298 final Set<Dependency> newDependencies;
299 if (sizeChanged) {
300
301 newDependencies = new HashSet<Dependency>(after);
302 newDependencies.removeAll(before);
303 } else {
304 newDependencies = EMPTY_DEPENDENCY_SET;
305 }
306 return newDependencies;
307 }
308
309
310
311
312
313
314
315 private File getNextTempDirectory() throws AnalysisException {
316 dirCount += 1;
317 final File directory = new File(tempFileLocation, String.valueOf(dirCount));
318
319 if (directory.exists()) {
320 return getNextTempDirectory();
321 }
322 if (!directory.mkdirs()) {
323 final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
324 throw new AnalysisException(msg);
325 }
326 return directory;
327 }
328
329
330
331
332
333
334
335
336
337 private void extractFiles(File archive, File destination, Engine engine) throws AnalysisException {
338 if (archive != null && destination != null) {
339 FileInputStream fis;
340 try {
341 fis = new FileInputStream(archive);
342 } catch (FileNotFoundException ex) {
343 LOGGER.debug("", ex);
344 throw new AnalysisException("Archive file was not found.", ex);
345 }
346 final String archiveExt = FileUtils.getFileExtension(archive.getName()).toLowerCase();
347 try {
348 if (ZIPPABLES.contains(archiveExt)) {
349 extractArchive(new ZipArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
350 } else if ("tar".equals(archiveExt)) {
351 extractArchive(new TarArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
352 } else if ("gz".equals(archiveExt) || "tgz".equals(archiveExt)) {
353 final String uncompressedName = GzipUtils.getUncompressedFilename(archive.getName());
354 final File f = new File(destination, uncompressedName);
355 if (engine.accept(f)) {
356 decompressFile(new GzipCompressorInputStream(new BufferedInputStream(fis)), f);
357 }
358 } else if ("bz2".equals(archiveExt) || "tbz2".equals(archiveExt)) {
359 final String uncompressedName = BZip2Utils.getUncompressedFilename(archive.getName());
360 final File f = new File(destination, uncompressedName);
361 if (engine.accept(f)) {
362 decompressFile(new BZip2CompressorInputStream(new BufferedInputStream(fis)), f);
363 }
364 }
365 } catch (ArchiveExtractionException ex) {
366 LOGGER.warn("Exception extracting archive '{}'.", archive.getName());
367 LOGGER.debug("", ex);
368 } catch (IOException ex) {
369 LOGGER.warn("Exception reading archive '{}'.", archive.getName());
370 LOGGER.debug("", ex);
371 } finally {
372 close(fis);
373 }
374 }
375 }
376
377
378
379
380
381
382
383
384
385 private void extractArchive(ArchiveInputStream input, File destination, Engine engine) throws ArchiveExtractionException {
386 ArchiveEntry entry;
387 try {
388 while ((entry = input.getNextEntry()) != null) {
389 final File file = new File(destination, entry.getName());
390 if (entry.isDirectory()) {
391 if (!file.exists() && !file.mkdirs()) {
392 final String msg = String.format("Unable to create directory '%s'.", file.getAbsolutePath());
393 throw new AnalysisException(msg);
394 }
395 } else if (engine.accept(file)) {
396 extractAcceptedFile(input, file);
397 }
398 }
399 } catch (Throwable ex) {
400 throw new ArchiveExtractionException(ex);
401 } finally {
402 close(input);
403 }
404 }
405
406
407
408
409
410
411
412
413 private static void extractAcceptedFile(ArchiveInputStream input, File file) throws AnalysisException {
414 LOGGER.debug("Extracting '{}'", file.getPath());
415 FileOutputStream fos = null;
416 try {
417 final File parent = file.getParentFile();
418 if (!parent.isDirectory()) {
419 if (!parent.mkdirs()) {
420 final String msg = String.format("Unable to build directory '%s'.", parent.getAbsolutePath());
421 throw new AnalysisException(msg);
422 }
423 }
424 fos = new FileOutputStream(file);
425 IOUtils.copy(input, fos);
426 } catch (FileNotFoundException ex) {
427 LOGGER.debug("", ex);
428 final String msg = String.format("Unable to find file '%s'.", file.getName());
429 throw new AnalysisException(msg, ex);
430 } catch (IOException ex) {
431 LOGGER.debug("", ex);
432 final String msg = String.format("IO Exception while parsing file '%s'.", file.getName());
433 throw new AnalysisException(msg, ex);
434 } finally {
435 close(fos);
436 }
437 }
438
439
440
441
442
443
444
445
446 private void decompressFile(CompressorInputStream inputStream, File outputFile) throws ArchiveExtractionException {
447 LOGGER.debug("Decompressing '{}'", outputFile.getPath());
448 FileOutputStream out = null;
449 try {
450 out = new FileOutputStream(outputFile);
451 IOUtils.copy(inputStream, out);
452 } catch (FileNotFoundException ex) {
453 LOGGER.debug("", ex);
454 throw new ArchiveExtractionException(ex);
455 } catch (IOException ex) {
456 LOGGER.debug("", ex);
457 throw new ArchiveExtractionException(ex);
458 } finally {
459 close(out);
460 }
461 }
462
463
464
465
466
467
468 private static void close(Closeable closeable) {
469 if (null != closeable) {
470 try {
471 closeable.close();
472 } catch (IOException ex) {
473 LOGGER.trace("", ex);
474 }
475 }
476 }
477
478
479
480
481
482
483
484 private boolean isZipFileActuallyJarFile(Dependency dependency) {
485 boolean isJar = false;
486 ZipFile zip = null;
487 try {
488 zip = new ZipFile(dependency.getActualFilePath());
489 if (zip.getEntry("META-INF/MANIFEST.MF") != null
490 || zip.getEntry("META-INF/maven") != null) {
491 final Enumeration<ZipArchiveEntry> entries = zip.getEntries();
492 while (entries.hasMoreElements()) {
493 final ZipArchiveEntry entry = entries.nextElement();
494 if (!entry.isDirectory()) {
495 final String name = entry.getName().toLowerCase();
496 if (name.endsWith(".class")) {
497 isJar = true;
498 break;
499 }
500 }
501 }
502 }
503 } catch (IOException ex) {
504 LOGGER.debug("Unable to unzip zip file '{}'", dependency.getFilePath(), ex);
505 } finally {
506 ZipFile.closeQuietly(zip);
507 }
508
509 return isJar;
510 }
511 }