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 String[] ext = additionalZipExt.split("\\s*,\\s*");
118 Collections.addAll(ZIPPABLES, 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()) {
199 final String[] l = tempFileLocation.list();
200 if (l != null && l.length > 0) {
201 LOGGER.warn("Failed to delete some temporary files, see the log for more details");
202 }
203 }
204 }
205 }
206
207
208
209
210
211
212
213
214
215 @Override
216 public void analyzeFileType(Dependency dependency, Engine engine) throws AnalysisException {
217 final File f = new File(dependency.getActualFilePath());
218 final File tmpDir = getNextTempDirectory();
219 extractFiles(f, tmpDir, engine);
220
221
222 final Set<Dependency> dependencySet = findMoreDependencies(engine, tmpDir);
223 if (!dependencySet.isEmpty()) {
224 for (Dependency d : dependencySet) {
225
226 final String displayPath = String.format("%s%s",
227 dependency.getFilePath(),
228 d.getActualFilePath().substring(tmpDir.getAbsolutePath().length()));
229 final String displayName = String.format("%s: %s",
230 dependency.getFileName(),
231 d.getFileName());
232 d.setFilePath(displayPath);
233 d.setFileName(displayName);
234
235
236
237 if (this.accept(d.getActualFile()) && scanDepth < MAX_SCAN_DEPTH) {
238 scanDepth += 1;
239 analyze(d, engine);
240 scanDepth -= 1;
241 }
242 }
243 }
244 if (REMOVE_FROM_ANALYSIS.accept(dependency.getActualFile())) {
245 addDisguisedJarsToDependencies(dependency, engine);
246 engine.getDependencies().remove(dependency);
247 }
248 Collections.sort(engine.getDependencies());
249 }
250
251
252
253
254
255
256
257
258 private void addDisguisedJarsToDependencies(Dependency dependency, Engine engine) throws AnalysisException {
259 if (ZIP_FILTER.accept(dependency.getActualFile()) && isZipFileActuallyJarFile(dependency)) {
260 final File tdir = getNextTempDirectory();
261 final String fileName = dependency.getFileName();
262
263 LOGGER.info("The zip file '{}' appears to be a JAR file, making a copy and analyzing it as a JAR.", fileName);
264
265 final File tmpLoc = new File(tdir, fileName.substring(0, fileName.length() - 3) + "jar");
266 try {
267 org.apache.commons.io.FileUtils.copyFile(tdir, tmpLoc);
268 final Set<Dependency> dependencySet = findMoreDependencies(engine, tmpLoc);
269 if (!dependencySet.isEmpty()) {
270 if (dependencySet.size() != 1) {
271 LOGGER.info("Deep copy of ZIP to JAR file resulted in more than one dependency?");
272 }
273 for (Dependency d : dependencySet) {
274
275 d.setFilePath(dependency.getFilePath());
276 d.setDisplayFileName(dependency.getFileName());
277 }
278 }
279 } catch (IOException ex) {
280 LOGGER.debug("Unable to perform deep copy on '{}'", dependency.getActualFile().getPath(), ex);
281 }
282 }
283 }
284
285
286
287 private static final Set<Dependency> EMPTY_DEPENDENCY_SET = Collections.emptySet();
288
289
290
291
292
293
294
295
296 private static Set<Dependency> findMoreDependencies(Engine engine, File file) {
297 final List<Dependency> before = new ArrayList<Dependency>(engine.getDependencies());
298 engine.scan(file);
299 final List<Dependency> after = engine.getDependencies();
300 final boolean sizeChanged = before.size() != after.size();
301 final Set<Dependency> newDependencies;
302 if (sizeChanged) {
303
304 newDependencies = new HashSet<Dependency>(after);
305 newDependencies.removeAll(before);
306 } else {
307 newDependencies = EMPTY_DEPENDENCY_SET;
308 }
309 return newDependencies;
310 }
311
312
313
314
315
316
317
318 private File getNextTempDirectory() throws AnalysisException {
319 dirCount += 1;
320 final File directory = new File(tempFileLocation, String.valueOf(dirCount));
321
322 if (directory.exists()) {
323 return getNextTempDirectory();
324 }
325 if (!directory.mkdirs()) {
326 final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
327 throw new AnalysisException(msg);
328 }
329 return directory;
330 }
331
332
333
334
335
336
337
338
339
340 private void extractFiles(File archive, File destination, Engine engine) throws AnalysisException {
341 if (archive != null && destination != null) {
342 FileInputStream fis;
343 try {
344 fis = new FileInputStream(archive);
345 } catch (FileNotFoundException ex) {
346 LOGGER.debug("", ex);
347 throw new AnalysisException("Archive file was not found.", ex);
348 }
349 final String archiveExt = FileUtils.getFileExtension(archive.getName()).toLowerCase();
350 try {
351 if (ZIPPABLES.contains(archiveExt)) {
352 extractArchive(new ZipArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
353 } else if ("tar".equals(archiveExt)) {
354 extractArchive(new TarArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
355 } else if ("gz".equals(archiveExt) || "tgz".equals(archiveExt)) {
356 final String uncompressedName = GzipUtils.getUncompressedFilename(archive.getName());
357 final File f = new File(destination, uncompressedName);
358 if (engine.accept(f)) {
359 decompressFile(new GzipCompressorInputStream(new BufferedInputStream(fis)), f);
360 }
361 } else if ("bz2".equals(archiveExt) || "tbz2".equals(archiveExt)) {
362 final String uncompressedName = BZip2Utils.getUncompressedFilename(archive.getName());
363 final File f = new File(destination, uncompressedName);
364 if (engine.accept(f)) {
365 decompressFile(new BZip2CompressorInputStream(new BufferedInputStream(fis)), f);
366 }
367 }
368 } catch (ArchiveExtractionException ex) {
369 LOGGER.warn("Exception extracting archive '{}'.", archive.getName());
370 LOGGER.debug("", ex);
371 } catch (IOException ex) {
372 LOGGER.warn("Exception reading archive '{}'.", archive.getName());
373 LOGGER.debug("", ex);
374 } finally {
375 close(fis);
376 }
377 }
378 }
379
380
381
382
383
384
385
386
387
388 private void extractArchive(ArchiveInputStream input, File destination, Engine engine) throws ArchiveExtractionException {
389 ArchiveEntry entry;
390 try {
391 while ((entry = input.getNextEntry()) != null) {
392 final File file = new File(destination, entry.getName());
393 if (entry.isDirectory()) {
394 if (!file.exists() && !file.mkdirs()) {
395 final String msg = String.format("Unable to create directory '%s'.", file.getAbsolutePath());
396 throw new AnalysisException(msg);
397 }
398 } else if (engine.accept(file)) {
399 extractAcceptedFile(input, file);
400 }
401 }
402 } catch (Throwable ex) {
403 throw new ArchiveExtractionException(ex);
404 } finally {
405 close(input);
406 }
407 }
408
409
410
411
412
413
414
415
416 private static void extractAcceptedFile(ArchiveInputStream input, File file) throws AnalysisException {
417 LOGGER.debug("Extracting '{}'", file.getPath());
418 FileOutputStream fos = null;
419 try {
420 final File parent = file.getParentFile();
421 if (!parent.isDirectory() && !parent.mkdirs()) {
422 final String msg = String.format("Unable to build directory '%s'.", parent.getAbsolutePath());
423 throw new AnalysisException(msg);
424 }
425 fos = new FileOutputStream(file);
426 IOUtils.copy(input, fos);
427 } catch (FileNotFoundException ex) {
428 LOGGER.debug("", ex);
429 final String msg = String.format("Unable to find file '%s'.", file.getName());
430 throw new AnalysisException(msg, ex);
431 } catch (IOException ex) {
432 LOGGER.debug("", ex);
433 final String msg = String.format("IO Exception while parsing file '%s'.", file.getName());
434 throw new AnalysisException(msg, ex);
435 } finally {
436 close(fos);
437 }
438 }
439
440
441
442
443
444
445
446
447 private void decompressFile(CompressorInputStream inputStream, File outputFile) throws ArchiveExtractionException {
448 LOGGER.debug("Decompressing '{}'", outputFile.getPath());
449 FileOutputStream out = null;
450 try {
451 out = new FileOutputStream(outputFile);
452 IOUtils.copy(inputStream, out);
453 } catch (FileNotFoundException ex) {
454 LOGGER.debug("", ex);
455 throw new ArchiveExtractionException(ex);
456 } catch (IOException ex) {
457 LOGGER.debug("", ex);
458 throw new ArchiveExtractionException(ex);
459 } finally {
460 close(out);
461 }
462 }
463
464
465
466
467
468
469 private static void close(Closeable closeable) {
470 if (null != closeable) {
471 try {
472 closeable.close();
473 } catch (IOException ex) {
474 LOGGER.trace("", ex);
475 }
476 }
477 }
478
479
480
481
482
483
484
485 private boolean isZipFileActuallyJarFile(Dependency dependency) {
486 boolean isJar = false;
487 ZipFile zip = null;
488 try {
489 zip = new ZipFile(dependency.getActualFilePath());
490 if (zip.getEntry("META-INF/MANIFEST.MF") != null
491 || zip.getEntry("META-INF/maven") != null) {
492 final Enumeration<ZipArchiveEntry> entries = zip.getEntries();
493 while (entries.hasMoreElements()) {
494 final ZipArchiveEntry entry = entries.nextElement();
495 if (!entry.isDirectory()) {
496 final String name = entry.getName().toLowerCase();
497 if (name.endsWith(".class")) {
498 isJar = true;
499 break;
500 }
501 }
502 }
503 }
504 } catch (IOException ex) {
505 LOGGER.debug("Unable to unzip zip file '{}'", dependency.getFilePath(), ex);
506 } finally {
507 ZipFile.closeQuietly(zip);
508 }
509
510 return isJar;
511 }
512 }