##### Coverage Report - org.owasp.dependencycheck.analyzer.ArchiveAnalyzer

Classes in this File | Line Coverage | Branch Coverage | Complexity
ArchiveAnalyzer |
65% | 98/150
69% | 43/62
| 6.2727272727272725;6.273

1 |  |
     /*
2 |  |
      * This file is part of dependency-check-core.
3 |  |
      *
4 |  |
      * Dependency-check-core is free software: you can redistribute it and/or modify it
5 |  |
      * under the terms of the GNU General Public License as published by the Free
6 |  |
      * Software Foundation, either version 3 of the License, or (at your option) any
7 |  |
      * later version.
8 |  |
      *
9 |  |
      * Dependency-check-core is distributed in the hope that it will be useful, but
10 |  |
      * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 |  |
      * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
12 |  |
      * details.
13 |  |
      *
14 |  |
      * You should have received a copy of the GNU General Public License along with
15 |  |
      * dependency-check-core. If not, see http://www.gnu.org/licenses/.
16 |  |
      *
17 |  |
      * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
18 |  |
      */
19 |  |
     package org.owasp.dependencycheck.analyzer;
20 |  |
21 |  |
     import java.io.BufferedInputStream;
22 |  |
     import java.io.BufferedOutputStream;
23 |  |
     import java.io.File;
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.HashSet;
31 |  |
     import java.util.List;
32 |  |
     import java.util.Set;
33 |  |
     import java.util.logging.Level;
34 |  |
     import java.util.logging.Logger;
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.ZipArchiveInputStream;
39 |  |
     import org.apache.commons.compress.compressors.CompressorInputStream;
40 |  |
     import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
41 |  |
     import org.apache.commons.compress.compressors.gzip.GzipUtils;
42 |  |
     import org.h2.store.fs.FileUtils;
43 |  |
     import org.owasp.dependencycheck.Engine;
44 |  |
     import org.owasp.dependencycheck.dependency.Dependency;
45 |  |
     import org.owasp.dependencycheck.utils.Settings;
46 |  |
47 |  |
     /**
48 |  |
      * <p>An analyzer that extracts files from archives and ensures any supported
49 |  |
      * files contained within the archive are added to the dependency list.</p>
50 |  |
      *
51 |  |
      * @author Jeremy Long (jeremy.long@owasp.org)
52 |  |
      */
53 | 11 |
     public class ArchiveAnalyzer extends AbstractAnalyzer implements Analyzer {
54 |  |
55 |  |
         /**
56 |  |
          * The buffer size to use when extracting files from the archive.
57 |  |
          */
58 |  |
         private static final int BUFFER_SIZE = 4096;
59 |  |
         /**
60 |  |
          * The count of directories created during analysis. This is used for
61 |  |
          * creating temporary directories.
62 |  |
          */
63 | 1 |
         private static int dirCount = 0;
64 |  |
         /**
65 |  |
          * The parent directory for the individual directories per archive.
66 |  |
          */
67 | 11 |
         private File tempFileLocation = null;
68 |  |
         /**
69 |  |
          * The max scan depth that the analyzer will recursively extract nested
70 |  |
          * archives.
71 |  |
          */
72 | 1 |
         private static final int MAX_SCAN_DEPTH = Settings.getInt("archive.scan.depth", 3);
73 |  |
         /**
74 |  |
          * Tracks the current scan/extraction depth for nested archives.
75 |  |
          */
76 | 11 |
         private int scanDepth = 0;
77 |  |
         //<editor-fold defaultstate="collapsed" desc="All standard implmentation details of Analyzer">
78 |  |
         /**
79 |  |
          * The name of the analyzer.
80 |  |
          */
81 |  |
         private static final String ANALYZER_NAME = "Archive Analyzer";
82 |  |
         /**
83 |  |
          * The phase that this analyzer is intended to run in.
84 |  |
          */
85 | 1 |
         private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INITIAL;
86 |  |
         /**
87 |  |
          * The set of file extensions supported by this analyzer.
88 |  |
          */
89 | 1 |
         private static final Set<String> EXTENSIONS = newHashSet("zip", "ear", "war", "tar", "gz", "tgz");
90 |  |
91 |  |
         /**
92 |  |
          * Returns a list of file EXTENSIONS supported by this analyzer.
93 |  |
          *
94 |  |
          * @return a list of file EXTENSIONS supported by this analyzer.
95 |  |
          */
96 |  |
         public Set<String> getSupportedExtensions() {
97 | 149 |
             return EXTENSIONS;
98 |  |
         }
99 |  |
100 |  |
         /**
101 |  |
          * Returns the name of the analyzer.
102 |  |
          *
103 |  |
          * @return the name of the analyzer.
104 |  |
          */
105 |  |
         public String getName() {
106 | 10 |
             return ANALYZER_NAME;
107 |  |
         }
108 |  |
109 |  |
         /**
110 |  |
          * Returns whether or not this analyzer can process the given extension.
111 |  |
          *
112 |  |
          * @param extension the file extension to test for support.
113 |  |
          * @return whether or not the specified file extension is supported by this
114 |  |
          * analyzer.
115 |  |
          */
116 |  |
         public boolean supportsExtension(String extension) {
117 | 154 |
             return EXTENSIONS.contains(extension);
118 |  |
         }
119 |  |
120 |  |
         /**
121 |  |
          * Returns the phase that the analyzer is intended to run in.
122 |  |
          *
123 |  |
          * @return the phase that the analyzer is intended to run in.
124 |  |
          */
125 |  |
         public AnalysisPhase getAnalysisPhase() {
126 | 7 |
             return ANALYSIS_PHASE;
127 |  |
         }
128 |  |
         //</editor-fold>
129 |  |
130 |  |
         /**
131 |  |
          * The initialize method does nothing for this Analyzer.
132 |  |
          *
133 |  |
          * @throws Exception is thrown if there is an exception deleting or creating
134 |  |
          * temporary files
135 |  |
          */
136 |  |
         @Override
137 |  |
         public void initialize() throws Exception {
138 | 9 |
             final File baseDir = Settings.getTempDirectory();
139 | 9 |
             if (!baseDir.exists()) {
140 | 0 |
                 if (!baseDir.mkdirs()) {
141 | 0 |
                     final String msg = String.format("Unable to make a temporary folder '%s'", baseDir.getPath());
142 | 0 |
                     throw new AnalysisException(msg);
143 |  |
                 }
144 |  |
             }
145 | 9 |
             tempFileLocation = File.createTempFile("check", "tmp", baseDir);
146 | 9 |
             if (!tempFileLocation.delete()) {
147 | 0 |
                 final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
148 | 0 |
                 throw new AnalysisException(msg);
149 |  |
             }
150 | 9 |
             if (!tempFileLocation.mkdirs()) {
151 | 0 |
                 final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
152 | 0 |
                 throw new AnalysisException(msg);
153 |  |
             }
154 | 9 |
         }
155 |  |
156 |  |
         /**
157 |  |
          * The close method does nothing for this Analyzer.
158 |  |
          *
159 |  |
          * @throws Exception thrown if there is an exception deleting temporary
160 |  |
          * files
161 |  |
          */
162 |  |
         @Override
163 |  |
         public void close() throws Exception {
164 | 9 |
             if (tempFileLocation != null && tempFileLocation.exists()) {
165 | 9 |
                 FileUtils.deleteRecursive(tempFileLocation.getAbsolutePath(), true);
166 |  |
             }
167 | 9 |
         }
168 |  |
169 |  |
         /**
170 |  |
          * Analyzes a given dependency. If the dependency is an archive, such as a
171 |  |
          * WAR or EAR, the contents are extracted, scanned, and added to the list of
172 |  |
          * dependencies within the engine.
173 |  |
          *
174 |  |
          * @param dependency the dependency to analyze
175 |  |
          * @param engine the engine scanning
176 |  |
          * @throws AnalysisException thrown if there is an analysis exception
177 |  |
          */
178 |  |
         @Override
179 |  |
         public void analyze(Dependency dependency, Engine engine) throws AnalysisException {
180 | 8 |
             final File f = new File(dependency.getActualFilePath());
181 | 8 |
             final File tmpDir = getNextTempDirectory();
182 | 8 |
             extractFiles(f, tmpDir, engine);
183 |  |
184 |  |
             //make a copy
185 | 8 |
             final List<Dependency> dependencies = new ArrayList<Dependency>(engine.getDependencies());
186 | 8 |
             engine.scan(tmpDir);
187 | 8 |
             final List<Dependency> newDependencies = engine.getDependencies();
188 | 8 |
             if (dependencies.size() != newDependencies.size()) {
189 |  |
                 //get the new dependencies
190 | 6 |
                 final Set<Dependency> dependencySet = new HashSet<Dependency>();
191 | 6 |
                 dependencySet.addAll(newDependencies);
192 | 6 |
                 dependencySet.removeAll(dependencies);
193 |  |
194 | 6 |
                 for (Dependency d : dependencySet) {
195 |  |
                     //fix the dependency's display name and path
196 | 10 |
                     final String displayPath = String.format("%s%s",
197 |  |
                             dependency.getFilePath(),
198 |  |
                             d.getActualFilePath().substring(tmpDir.getAbsolutePath().length()));
199 | 10 |
                     final String displayName = String.format("%s%s%s",
200 |  |
                             dependency.getFileName(),
201 |  |
                             File.separator,
202 |  |
                             d.getFileName());
203 | 10 |
                     d.setFilePath(displayPath);
204 | 10 |
                     d.setFileName(displayName);
205 |  |
206 |  |
                     //TODO - can we get more evidence from the parent? EAR contains module name, etc.
207 |  |
208 |  |
                     //analyze the dependency (i.e. extract files) if it is a supported type.
209 | 10 |
                     if (this.supportsExtension(d.getFileExtension()) && scanDepth < MAX_SCAN_DEPTH) {
210 | 3 |
                         scanDepth += 1;
211 | 3 |
                         analyze(d, engine);
212 | 3 |
                         scanDepth -= 1;
213 |  |
                     }
214 | 10 |
                 }
215 |  |
             }
216 | 8 |
             Collections.sort(engine.getDependencies());
217 | 8 |
         }
218 |  |
219 |  |
         /**
220 |  |
          * Retrieves the next temporary directory to extract an archive too.
221 |  |
          *
222 |  |
          * @return a directory
223 |  |
          * @throws AnalysisException thrown if unable to create temporary directory
224 |  |
          */
225 |  |
         private File getNextTempDirectory() throws AnalysisException {
226 | 8 |
             dirCount += 1;
227 | 8 |
             final File directory = new File(tempFileLocation, String.valueOf(dirCount));
228 |  |
             //getting an exception for some directories not being able to be created; might be because the directory already exists?
229 | 8 |
             if (directory.exists()) {
230 | 0 |
                 return getNextTempDirectory();
231 |  |
             }
232 | 8 |
             if (!directory.mkdirs()) {
233 | 0 |
                 final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
234 | 0 |
                 throw new AnalysisException(msg);
235 |  |
             }
236 | 8 |
             return directory;
237 |  |
         }
238 |  |
239 |  |
         /**
240 |  |
          * Extracts the contents of an archive into the specified directory.
241 |  |
          *
242 |  |
          * @param archive an archive file such as a WAR or EAR
243 |  |
          * @param destination a directory to extract the contents to
244 |  |
          * @param engine the scanning engine
245 |  |
          * @throws AnalysisException thrown if the archive is not found
246 |  |
          */
247 |  |
         private void extractFiles(File archive, File destination, Engine engine) throws AnalysisException {
248 | 8 |
             if (archive == null || destination == null) {
249 | 0 |
                 return;
250 |  |
             }
251 |  |
252 | 8 |
             FileInputStream fis = null;
253 |  |
             try {
254 | 8 |
                 fis = new FileInputStream(archive);
255 | 0 |
             } catch (FileNotFoundException ex) {
256 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.INFO, null, ex);
257 | 0 |
                 throw new AnalysisException("Archive file was not found.", ex);
258 | 8 |
             }
259 | 8 |
             final String archiveExt = org.owasp.dependencycheck.utils.FileUtils.getFileExtension(archive.getName()).toLowerCase();
260 |  |
             try {
261 | 8 |
                 if ("zip".equals(archiveExt) || "war".equals(archiveExt) || "ear".equals(archiveExt)) {
262 | 3 |
                     extractArchive(new ZipArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
263 | 5 |
                 } else if ("tar".equals(archiveExt)) {
264 | 3 |
                     extractArchive(new TarArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
265 | 2 |
                 } else if ("gz".equals(archiveExt) || "tgz".equals(archiveExt)) {
266 | 2 |
                     final String uncompressedName = GzipUtils.getUncompressedFilename(archive.getName());
267 | 2 |
                     final String uncompressedExt = org.owasp.dependencycheck.utils.FileUtils.getFileExtension(uncompressedName).toLowerCase();
268 | 2 |
                     if (engine.supportsExtension(uncompressedExt)) {
269 | 2 |
                         decompressFile(new GzipCompressorInputStream(new BufferedInputStream(fis)), new File(destination, uncompressedName));
270 |  |
                     }
271 |  |
                 }
272 | 0 |
             } catch (ArchiveExtractionException ex) {
273 | 0 |
                 final String msg = String.format("Exception extracting archive '%s'.", archive.getName());
274 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.WARNING, msg);
275 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
276 | 0 |
             } catch (IOException ex) {
277 | 0 |
                 final String msg = String.format("Exception reading archive '%s'.", archive.getName());
278 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.WARNING, msg);
279 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
280 |  |
             } finally {
281 | 0 |
                 try {
282 | 8 |
                     fis.close();
283 | 0 |
                 } catch (IOException ex) {
284 | 0 |
                     Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
285 | 8 |
                 }
286 | 0 |
             }
287 | 8 |
         }
288 |  |
289 |  |
         /**
290 |  |
          * Extracts files from an archive.
291 |  |
          *
292 |  |
          * @param input the archive to extract files from
293 |  |
          * @param destination the location to write the files too
294 |  |
          * @param engine the dependency-check engine
295 |  |
          * @throws ArchiveExtractionException thrown if there is an exception
296 |  |
          * extracting files from the archive
297 |  |
          */
298 |  |
         private void extractArchive(ArchiveInputStream input, File destination, Engine engine) throws ArchiveExtractionException {
299 |  |
             ArchiveEntry entry;
300 |  |
             try {
301 | 172 |
                 while ((entry = input.getNextEntry()) != null) {
302 | 166 |
                     if (entry.isDirectory()) {
303 | 25 |
                         final File d = new File(destination, entry.getName());
304 | 25 |
                         if (!d.exists()) {
305 | 25 |
                             if (!d.mkdirs()) {
306 | 0 |
                                 final String msg = String.format("Unable to create '%s'.", d.getAbsolutePath());
307 | 0 |
                                 throw new AnalysisException(msg);
308 |  |
                             }
309 |  |
                         }
310 | 25 |
                     } else {
311 | 141 |
                         final File file = new File(destination, entry.getName());
312 | 141 |
                         final String ext = org.owasp.dependencycheck.utils.FileUtils.getFileExtension(file.getName());
313 | 141 |
                         if (engine.supportsExtension(ext)) {
314 | 8 |
                             BufferedOutputStream bos = null;
315 |  |
                             FileOutputStream fos;
316 |  |
                             try {
317 | 8 |
                                 fos = new FileOutputStream(file);
318 | 8 |
                                 bos = new BufferedOutputStream(fos, BUFFER_SIZE);
319 |  |
                                 int count;
320 | 8 |
                                 final byte data[] = new byte[BUFFER_SIZE];
321 | 1709 |
                                 while ((count = input.read(data, 0, BUFFER_SIZE)) != -1) {
322 | 1701 |
                                     bos.write(data, 0, count);
323 |  |
                                 }
324 | 8 |
                                 bos.flush();
325 | 0 |
                             } catch (FileNotFoundException ex) {
326 | 0 |
                                 Logger.getLogger(ArchiveAnalyzer.class
327 |  |
                                         .getName()).log(Level.FINE, null, ex);
328 | 0 |
                                 final String msg = String.format("Unable to find file '%s'.", file.getName());
329 | 0 |
                                 throw new AnalysisException(msg, ex);
330 | 0 |
                             } catch (IOException ex) {
331 | 0 |
                                 Logger.getLogger(ArchiveAnalyzer.class
332 |  |
                                         .getName()).log(Level.FINE, null, ex);
333 | 0 |
                                 final String msg = String.format("IO Exception while parsing file '%s'.", file.getName());
334 | 0 |
                                 throw new AnalysisException(msg, ex);
335 |  |
                             } finally {
336 | 8 |
                                 if (bos != null) {
337 |  |
                                     try {
338 | 8 |
                                         bos.close();
339 | 0 |
                                     } catch (IOException ex) {
340 | 0 |
                                         Logger.getLogger(ArchiveAnalyzer.class
341 |  |
                                                 .getName()).log(Level.FINEST, null, ex);
342 | 8 |
                                     }
343 |  |
                                 }
344 |  |
                             }
345 |  |
                         }
346 | 141 |
                     }
347 |  |
                 }
348 | 0 |
             } catch (IOException ex) {
349 | 0 |
                 throw new ArchiveExtractionException(ex);
350 | 0 |
             } catch (Throwable ex) {
351 | 0 |
                 throw new ArchiveExtractionException(ex);
352 |  |
             } finally {
353 | 6 |
                 if (input != null) {
354 |  |
                     try {
355 | 6 |
                         input.close();
356 | 0 |
                     } catch (IOException ex) {
357 | 0 |
                         Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
358 | 6 |
                     }
359 |  |
                 }
360 |  |
             }
361 | 6 |
         }
362 |  |
363 |  |
         /**
364 |  |
          * Decompresses a file.
365 |  |
          *
366 |  |
          * @param inputStream the compressed file
367 |  |
          * @param outputFile the location to write the decompressed file
368 |  |
          * @throws ArchiveExtractionException thrown if there is an exception
369 |  |
          * decompressing the file
370 |  |
          */
371 |  |
         private void decompressFile(CompressorInputStream inputStream, File outputFile) throws ArchiveExtractionException {
372 | 2 |
             FileOutputStream out = null;
373 |  |
             try {
374 | 2 |
                 out = new FileOutputStream(outputFile);
375 | 2 |
                 final byte[] buffer = new byte[BUFFER_SIZE];
376 | 2 |
                 int n = 0;
377 | 272 |
                 while (-1 != (n = inputStream.read(buffer))) {
378 | 270 |
                     out.write(buffer, 0, n);
379 |  |
                 }
380 | 0 |
             } catch (FileNotFoundException ex) {
381 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
382 | 0 |
                 throw new ArchiveExtractionException(ex);
383 | 0 |
             } catch (IOException ex) {
384 | 0 |
                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
385 | 0 |
                 throw new ArchiveExtractionException(ex);
386 |  |
             } finally {
387 | 2 |
                 if (out != null) {
388 |  |
                     try {
389 | 2 |
                         out.close();
390 | 0 |
                     } catch (IOException ex) {
391 | 0 |
                         Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
392 | 2 |
                     }
393 |  |
                 }
394 |  |
             }
395 | 2 |
         }
396 |  |
     }