View Javadoc

    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  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      private static int dirCount = 0;
    64      /**
    65       * The parent directory for the individual directories per archive.
    66       */
    67      private File tempFileLocation = null;
    68      /**
    69       * The max scan depth that the analyzer will recursively extract nested
    70       * archives.
    71       */
    72      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      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      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INITIAL;
    86      /**
    87       * The set of file extensions supported by this analyzer.
    88       */
    89      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          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         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         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         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         final File baseDir = Settings.getTempDirectory();
    139         if (!baseDir.exists()) {
    140             if (!baseDir.mkdirs()) {
    141                 final String msg = String.format("Unable to make a temporary folder '%s'", baseDir.getPath());
    142                 throw new AnalysisException(msg);
    143             }
    144         }
    145         tempFileLocation = File.createTempFile("check", "tmp", baseDir);
    146         if (!tempFileLocation.delete()) {
    147             final String msg = String.format("Unable to delete temporary file '%s'.", tempFileLocation.getAbsolutePath());
    148             throw new AnalysisException(msg);
    149         }
    150         if (!tempFileLocation.mkdirs()) {
    151             final String msg = String.format("Unable to create directory '%s'.", tempFileLocation.getAbsolutePath());
    152             throw new AnalysisException(msg);
    153         }
    154     }
    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         if (tempFileLocation != null && tempFileLocation.exists()) {
    165             FileUtils.deleteRecursive(tempFileLocation.getAbsolutePath(), true);
    166         }
    167     }
    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         final File f = new File(dependency.getActualFilePath());
    181         final File tmpDir = getNextTempDirectory();
    182         extractFiles(f, tmpDir, engine);
    183
    184         //make a copy
    185         final List<Dependency> dependencies = new ArrayList<Dependency>(engine.getDependencies());
    186         engine.scan(tmpDir);
    187         final List<Dependency> newDependencies = engine.getDependencies();
    188         if (dependencies.size() != newDependencies.size()) {
    189             //get the new dependencies
    190             final Set<Dependency> dependencySet = new HashSet<Dependency>();
    191             dependencySet.addAll(newDependencies);
    192             dependencySet.removeAll(dependencies);
    193
    194             for (Dependency d : dependencySet) {
    195                 //fix the dependency's display name and path
    196                 final String displayPath = String.format("%s%s",
    197                         dependency.getFilePath(),
    198                         d.getActualFilePath().substring(tmpDir.getAbsolutePath().length()));
    199                 final String displayName = String.format("%s%s%s",
    200                         dependency.getFileName(),
    201                         File.separator,
    202                         d.getFileName());
    203                 d.setFilePath(displayPath);
    204                 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                 if (this.supportsExtension(d.getFileExtension()) && scanDepth < MAX_SCAN_DEPTH) {
    210                     scanDepth += 1;
    211                     analyze(d, engine);
    212                     scanDepth -= 1;
    213                 }
    214             }
    215         }
    216         Collections.sort(engine.getDependencies());
    217     }
    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         dirCount += 1;
    227         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         if (directory.exists()) {
    230             return getNextTempDirectory();
    231         }
    232         if (!directory.mkdirs()) {
    233             final String msg = String.format("Unable to create temp directory '%s'.", directory.getAbsolutePath());
    234             throw new AnalysisException(msg);
    235         }
    236         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         if (archive == null || destination == null) {
    249             return;
    250         }
    251
    252         FileInputStream fis = null;
    253         try {
    254             fis = new FileInputStream(archive);
    255         } catch (FileNotFoundException ex) {
    256             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.INFO, null, ex);
    257             throw new AnalysisException("Archive file was not found.", ex);
    258         }
    259         final String archiveExt = org.owasp.dependencycheck.utils.FileUtils.getFileExtension(archive.getName()).toLowerCase();
    260         try {
    261             if ("zip".equals(archiveExt) || "war".equals(archiveExt) || "ear".equals(archiveExt)) {
    262                 extractArchive(new ZipArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
    263             } else if ("tar".equals(archiveExt)) {
    264                 extractArchive(new TarArchiveInputStream(new BufferedInputStream(fis)), destination, engine);
    265             } else if ("gz".equals(archiveExt) || "tgz".equals(archiveExt)) {
    266                 final String uncompressedName = GzipUtils.getUncompressedFilename(archive.getName());
    267                 final String uncompressedExt = org.owasp.dependencycheck.utils.FileUtils.getFileExtension(uncompressedName).toLowerCase();
    268                 if (engine.supportsExtension(uncompressedExt)) {
    269                     decompressFile(new GzipCompressorInputStream(new BufferedInputStream(fis)), new File(destination, uncompressedName));
    270                 }
    271             }
    272         } catch (ArchiveExtractionException ex) {
    273             final String msg = String.format("Exception extracting archive '%s'.", archive.getName());
    274             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.WARNING, msg);
    275             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
    276         } catch (IOException ex) {
    277             final String msg = String.format("Exception reading archive '%s'.", archive.getName());
    278             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.WARNING, msg);
    279             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
    280         } finally {
    281             try {
    282                 fis.close();
    283             } catch (IOException ex) {
    284                 Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
    285             }
    286         }
    287     }
    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             while ((entry = input.getNextEntry()) != null) {
    302                 if (entry.isDirectory()) {
    303                     final File d = new File(destination, entry.getName());
    304                     if (!d.exists()) {
    305                         if (!d.mkdirs()) {
    306                             final String msg = String.format("Unable to create '%s'.", d.getAbsolutePath());
    307                             throw new AnalysisException(msg);
    308                         }
    309                     }
    310                 } else {
    311                     final File file = new File(destination, entry.getName());
    312                     final String ext = org.owasp.dependencycheck.utils.FileUtils.getFileExtension(file.getName());
    313                     if (engine.supportsExtension(ext)) {
    314                         BufferedOutputStream bos = null;
    315                         FileOutputStream fos;
    316                         try {
    317                             fos = new FileOutputStream(file);
    318                             bos = new BufferedOutputStream(fos, BUFFER_SIZE);
    319                             int count;
    320                             final byte data[] = new byte[BUFFER_SIZE];
    321                             while ((count = input.read(data, 0, BUFFER_SIZE)) != -1) {
    322                                 bos.write(data, 0, count);
    323                             }
    324                             bos.flush();
    325                         } catch (FileNotFoundException ex) {
    326                             Logger.getLogger(ArchiveAnalyzer.class
    327                                     .getName()).log(Level.FINE, null, ex);
    328                             final String msg = String.format("Unable to find file '%s'.", file.getName());
    329                             throw new AnalysisException(msg, ex);
    330                         } catch (IOException ex) {
    331                             Logger.getLogger(ArchiveAnalyzer.class
    332                                     .getName()).log(Level.FINE, null, ex);
    333                             final String msg = String.format("IO Exception while parsing file '%s'.", file.getName());
    334                             throw new AnalysisException(msg, ex);
    335                         } finally {
    336                             if (bos != null) {
    337                                 try {
    338                                     bos.close();
    339                                 } catch (IOException ex) {
    340                                     Logger.getLogger(ArchiveAnalyzer.class
    341                                             .getName()).log(Level.FINEST, null, ex);
    342                                 }
    343                             }
    344                         }
    345                     }
    346                 }
    347             }
    348         } catch (IOException ex) {
    349             throw new ArchiveExtractionException(ex);
    350         } catch (Throwable ex) {
    351             throw new ArchiveExtractionException(ex);
    352         } finally {
    353             if (input != null) {
    354                 try {
    355                     input.close();
    356                 } catch (IOException ex) {
    357                     Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
    358                 }
    359             }
    360         }
    361     }
    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         FileOutputStream out = null;
    373         try {
    374             out = new FileOutputStream(outputFile);
    375             final byte[] buffer = new byte[BUFFER_SIZE];
    376             int n = 0;
    377             while (-1 != (n = inputStream.read(buffer))) {
    378                 out.write(buffer, 0, n);
    379             }
    380         } catch (FileNotFoundException ex) {
    381             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
    382             throw new ArchiveExtractionException(ex);
    383         } catch (IOException ex) {
    384             Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINE, null, ex);
    385             throw new ArchiveExtractionException(ex);
    386         } finally {
    387             if (out != null) {
    388                 try {
    389                     out.close();
    390                 } catch (IOException ex) {
    391                     Logger.getLogger(ArchiveAnalyzer.class.getName()).log(Level.FINEST, null, ex);
    392                 }
    393             }
    394         }
    395     }
    396 }