Coverage Report - org.owasp.dependencycheck.analyzer.PythonDistributionAnalyzer
 
Classes in this File Line Coverage Branch Coverage Complexity
PythonDistributionAnalyzer
86%
78/90
65%
30/46
3.308
 
 1  
 /*
 2  
  * This file is part of dependency-check-core.
 3  
  *
 4  
  * Licensed under the Apache License, Version 2.0 (the "License");
 5  
  * you may not use this file except in compliance with the License.
 6  
  * You may obtain a copy of the License at
 7  
  *
 8  
  *     http://www.apache.org/licenses/LICENSE-2.0
 9  
  *
 10  
  * Unless required by applicable law or agreed to in writing, software
 11  
  * distributed under the License is distributed on an "AS IS" BASIS,
 12  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 13  
  * See the License for the specific language governing permissions and
 14  
  * limitations under the License.
 15  
  *
 16  
  * Copyright (c) 2015 Institute for Defense Analyses. All Rights Reserved.
 17  
  */
 18  
 package org.owasp.dependencycheck.analyzer;
 19  
 
 20  
 import java.io.BufferedInputStream;
 21  
 import java.io.File;
 22  
 import java.io.FileInputStream;
 23  
 import java.io.FileNotFoundException;
 24  
 import java.io.FilenameFilter;
 25  
 import java.util.Set;
 26  
 import java.util.logging.Level;
 27  
 import java.util.logging.Logger;
 28  
 import java.util.regex.Pattern;
 29  
 
 30  
 import javax.mail.MessagingException;
 31  
 import javax.mail.internet.InternetHeaders;
 32  
 
 33  
 import org.apache.commons.io.filefilter.NameFileFilter;
 34  
 import org.apache.commons.io.filefilter.SuffixFileFilter;
 35  
 import org.apache.commons.io.input.AutoCloseInputStream;
 36  
 import org.apache.commons.lang.StringUtils;
 37  
 import org.owasp.dependencycheck.Engine;
 38  
 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
 39  
 import org.owasp.dependencycheck.dependency.Confidence;
 40  
 import org.owasp.dependencycheck.dependency.Dependency;
 41  
 import org.owasp.dependencycheck.dependency.EvidenceCollection;
 42  
 import org.owasp.dependencycheck.utils.ExtractionException;
 43  
 import org.owasp.dependencycheck.utils.ExtractionUtil;
 44  
 import org.owasp.dependencycheck.utils.FileUtils;
 45  
 import org.owasp.dependencycheck.utils.Settings;
 46  
 import org.owasp.dependencycheck.utils.UrlStringUtils;
 47  
 
 48  
 /**
 49  
  * Used to analyze a Wheel or egg distribution files, or their contents in unzipped form, and collect information that can be used
 50  
  * to determine the associated CPE.
 51  
  *
 52  
  * @author Dale Visser <dvisser@ida.org>
 53  
  */
 54  11
 public class PythonDistributionAnalyzer extends AbstractFileTypeAnalyzer {
 55  
 
 56  
     /**
 57  
      * Name of egg metatdata files to analyze.
 58  
      */
 59  
     private static final String PKG_INFO = "PKG-INFO";
 60  
 
 61  
     /**
 62  
      * Name of wheel metadata files to analyze.
 63  
      */
 64  
     private static final String METADATA = "METADATA";
 65  
 
 66  
     /**
 67  
      * The logger.
 68  
      */
 69  1
     private static final Logger LOGGER = Logger
 70  
             .getLogger(PythonDistributionAnalyzer.class.getName());
 71  
 
 72  
     /**
 73  
      * The count of directories created during analysis. This is used for creating temporary directories.
 74  
      */
 75  1
     private static int dirCount = 0;
 76  
 
 77  
     /**
 78  
      * The name of the analyzer.
 79  
      */
 80  
     private static final String ANALYZER_NAME = "Python Distribution Analyzer";
 81  
     /**
 82  
      * The phase that this analyzer is intended to run in.
 83  
      */
 84  1
     private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
 85  
 
 86  
     /**
 87  
      * The set of file extensions supported by this analyzer.
 88  
      */
 89  1
     private static final Set<String> EXTENSIONS = newHashSet("whl", "egg",
 90  
             "zip", METADATA, PKG_INFO);
 91  
 
 92  
     /**
 93  
      * Used to match on egg archive candidate extenssions.
 94  
      */
 95  1
     private static final Pattern EGG_OR_ZIP = Pattern.compile("egg|zip");
 96  
 
 97  
     /**
 98  
      * The parent directory for the individual directories per archive.
 99  
      */
 100  
     private File tempFileLocation;
 101  
 
 102  
     /**
 103  
      * Filter that detects *.dist-info files (but doesn't verify they are directories.
 104  
      */
 105  1
     private static final FilenameFilter DIST_INFO_FILTER = new SuffixFileFilter(
 106  
             ".dist-info");
 107  
 
 108  
     /**
 109  
      * Filter that detects files named "METADATA".
 110  
      */
 111  1
     private static final FilenameFilter EGG_INFO_FILTER = new NameFileFilter(
 112  
             "EGG-INFO");
 113  
 
 114  
     /**
 115  
      * Filter that detects files named "METADATA".
 116  
      */
 117  1
     private static final FilenameFilter METADATA_FILTER = new NameFileFilter(
 118  
             METADATA);
 119  
 
 120  
     /**
 121  
      * Filter that detects files named "PKG-INFO".
 122  
      */
 123  1
     private static final FilenameFilter PKG_INFO_FILTER = new NameFileFilter(
 124  
             PKG_INFO);
 125  
 
 126  
     /**
 127  
      * Returns a list of file EXTENSIONS supported by this analyzer.
 128  
      *
 129  
      * @return a list of file EXTENSIONS supported by this analyzer.
 130  
      */
 131  
     @Override
 132  
     public Set<String> getSupportedExtensions() {
 133  854
         return EXTENSIONS;
 134  
     }
 135  
 
 136  
     /**
 137  
      * Returns the name of the analyzer.
 138  
      *
 139  
      * @return the name of the analyzer.
 140  
      */
 141  
     @Override
 142  
     public String getName() {
 143  5
         return ANALYZER_NAME;
 144  
     }
 145  
 
 146  
     /**
 147  
      * Returns the phase that the analyzer is intended to run in.
 148  
      *
 149  
      * @return the phase that the analyzer is intended to run in.
 150  
      */
 151  
     public AnalysisPhase getAnalysisPhase() {
 152  1
         return ANALYSIS_PHASE;
 153  
     }
 154  
 
 155  
     /**
 156  
      * Returns the key used in the properties file to reference the analyzer's enabled property.
 157  
      *
 158  
      * @return the analyzer's enabled property setting key
 159  
      */
 160  
     @Override
 161  
     protected String getAnalyzerEnabledSettingKey() {
 162  11
         return Settings.KEYS.ANALYZER_PYTHON_DISTRIBUTION_ENABLED;
 163  
     }
 164  
 
 165  
     @Override
 166  
     protected void analyzeFileType(Dependency dependency, Engine engine)
 167  
             throws AnalysisException {
 168  6
         if ("whl".equals(dependency.getFileExtension())) {
 169  1
             collectMetadataFromArchiveFormat(dependency, DIST_INFO_FILTER,
 170  
                     METADATA_FILTER);
 171  5
         } else if (EGG_OR_ZIP.matcher(
 172  
                 StringUtils.stripToEmpty(dependency.getFileExtension()))
 173  
                 .matches()) {
 174  2
             collectMetadataFromArchiveFormat(dependency, EGG_INFO_FILTER,
 175  
                     PKG_INFO_FILTER);
 176  
         } else {
 177  3
             final File actualFile = dependency.getActualFile();
 178  3
             final String name = actualFile.getName();
 179  3
             final boolean metadata = METADATA.equals(name);
 180  3
             if (metadata || PKG_INFO.equals(name)) {
 181  3
                 final File parent = actualFile.getParentFile();
 182  3
                 final String parentName = parent.getName();
 183  3
                 dependency.setDisplayFileName(parentName + "/" + name);
 184  3
                 if (parent.isDirectory()
 185  
                         && (metadata && parentName.endsWith(".dist-info")
 186  
                         || parentName.endsWith(".egg-info") || "EGG-INFO"
 187  
                         .equals(parentName))) {
 188  3
                     collectWheelMetadata(dependency, actualFile);
 189  
                 }
 190  
             }
 191  
         }
 192  6
     }
 193  
 
 194  
     /**
 195  
      * Collects the meta data from an archive.
 196  
      *
 197  
      * @param dependency the archive being scanned
 198  
      * @param folderFilter the filter to apply to the folder
 199  
      * @param metadataFilter the filter to apply to the meta data
 200  
      * @throws AnalysisException thrown when there is a problem analyzing the dependency
 201  
      */
 202  
     private void collectMetadataFromArchiveFormat(Dependency dependency,
 203  
             FilenameFilter folderFilter, FilenameFilter metadataFilter)
 204  
             throws AnalysisException {
 205  3
         final File temp = getNextTempDirectory();
 206  3
         LOGGER.fine(String.format("%s exists? %b", temp, temp.exists()));
 207  
         try {
 208  3
             ExtractionUtil.extractFilesUsingFilter(
 209  
                     new File(dependency.getActualFilePath()), temp,
 210  
                     metadataFilter);
 211  0
         } catch (ExtractionException ex) {
 212  0
             throw new AnalysisException(ex);
 213  3
         }
 214  
 
 215  3
         collectWheelMetadata(
 216  
                 dependency,
 217  
                 getMatchingFile(getMatchingFile(temp, folderFilter),
 218  
                         metadataFilter));
 219  3
     }
 220  
 
 221  
     /**
 222  
      * Makes sure a usable temporary directory is available.
 223  
      *
 224  
      * @throws Exception an AnalyzeException is thrown when the temp directory cannot be created
 225  
      */
 226  
     @Override
 227  
     protected void initializeFileTypeAnalyzer() throws Exception {
 228  9
         final File baseDir = Settings.getTempDirectory();
 229  9
         tempFileLocation = File.createTempFile("check", "tmp", baseDir);
 230  9
         if (!tempFileLocation.delete()) {
 231  0
             final String msg = String.format(
 232  
                     "Unable to delete temporary file '%s'.",
 233  
                     tempFileLocation.getAbsolutePath());
 234  0
             throw new AnalysisException(msg);
 235  
         }
 236  9
         if (!tempFileLocation.mkdirs()) {
 237  0
             final String msg = String.format(
 238  
                     "Unable to create directory '%s'.",
 239  
                     tempFileLocation.getAbsolutePath());
 240  0
             throw new AnalysisException(msg);
 241  
         }
 242  9
     }
 243  
 
 244  
     /**
 245  
      * Deletes any files extracted from the Wheel during analysis.
 246  
      */
 247  
     @Override
 248  
     public void close() {
 249  10
         if (tempFileLocation != null && tempFileLocation.exists()) {
 250  9
             LOGGER.log(Level.FINE, "Attempting to delete temporary files");
 251  9
             final boolean success = FileUtils.delete(tempFileLocation);
 252  9
             if (!success) {
 253  1
                 LOGGER.log(Level.WARNING,
 254  
                         "Failed to delete some temporary files, see the log for more details");
 255  
             }
 256  
         }
 257  10
     }
 258  
 
 259  
     /**
 260  
      * Gathers evidence from the METADATA file.
 261  
      *
 262  
      * @param dependency the dependency being analyzed
 263  
      * @param file a reference to the manifest/properties file
 264  
      * @throws AnalysisException thrown when there is an error
 265  
      */
 266  
     private static void collectWheelMetadata(Dependency dependency, File file)
 267  
             throws AnalysisException {
 268  6
         final InternetHeaders headers = getManifestProperties(file);
 269  6
         addPropertyToEvidence(headers, dependency.getVersionEvidence(),
 270  
                 "Version", Confidence.HIGHEST);
 271  6
         addPropertyToEvidence(headers, dependency.getProductEvidence(), "Name",
 272  
                 Confidence.HIGHEST);
 273  6
         final String url = headers.getHeader("Home-page", null);
 274  6
         final EvidenceCollection vendorEvidence = dependency
 275  
                 .getVendorEvidence();
 276  6
         if (StringUtils.isNotBlank(url)) {
 277  6
             if (UrlStringUtils.isUrl(url)) {
 278  6
                 vendorEvidence.addEvidence(METADATA, "vendor", url,
 279  
                         Confidence.MEDIUM);
 280  
             }
 281  
         }
 282  6
         addPropertyToEvidence(headers, vendorEvidence, "Author", Confidence.LOW);
 283  6
         final String summary = headers.getHeader("Summary", null);
 284  6
         if (StringUtils.isNotBlank(summary)) {
 285  6
             JarAnalyzer
 286  
                     .addDescription(dependency, summary, METADATA, "summary");
 287  
         }
 288  6
     }
 289  
 
 290  
     /**
 291  
      * Adds a value to the evidence collection.
 292  
      *
 293  
      * @param headers the properties collection
 294  
      * @param evidence the evidence collection to add the value
 295  
      * @param property the property name
 296  
      * @param confidence the confidence of the evidence
 297  
      */
 298  
     private static void addPropertyToEvidence(InternetHeaders headers,
 299  
             EvidenceCollection evidence, String property, Confidence confidence) {
 300  18
         final String value = headers.getHeader(property, null);
 301  18
         LOGGER.fine(String.format("Property: %s, Value: %s", property, value));
 302  18
         if (StringUtils.isNotBlank(value)) {
 303  18
             evidence.addEvidence(METADATA, property, value, confidence);
 304  
         }
 305  18
     }
 306  
 
 307  
     /**
 308  
      * Returns a list of files that match the given filter, this does not recursively scan the directory.
 309  
      *
 310  
      * @param folder the folder to filter
 311  
      * @param filter the filter to apply to the files in the directory
 312  
      * @return the list of Files in the directory that match the provided filter
 313  
      */
 314  
     private static File getMatchingFile(File folder, FilenameFilter filter) {
 315  6
         File result = null;
 316  6
         final File[] matches = folder.listFiles(filter);
 317  6
         if (null != matches && 1 == matches.length) {
 318  6
             result = matches[0];
 319  
         }
 320  6
         return result;
 321  
     }
 322  
 
 323  
     /**
 324  
      * Reads the manifest entries from the provided file.
 325  
      *
 326  
      * @param manifest the manifest
 327  
      * @return the manifest entries
 328  
      */
 329  
     private static InternetHeaders getManifestProperties(File manifest) {
 330  6
         final InternetHeaders result = new InternetHeaders();
 331  6
         if (null == manifest) {
 332  0
             LOGGER.fine("Manifest file not found.");
 333  
         } else {
 334  
             try {
 335  6
                 result.load(new AutoCloseInputStream(new BufferedInputStream(
 336  
                         new FileInputStream(manifest))));
 337  0
             } catch (MessagingException e) {
 338  0
                 LOGGER.log(Level.WARNING, e.getMessage(), e);
 339  0
             } catch (FileNotFoundException e) {
 340  0
                 LOGGER.log(Level.WARNING, e.getMessage(), e);
 341  6
             }
 342  
         }
 343  6
         return result;
 344  
     }
 345  
 
 346  
     /**
 347  
      * Retrieves the next temporary destingation directory for extracting an archive.
 348  
      *
 349  
      * @return a directory
 350  
      * @throws AnalysisException thrown if unable to create temporary directory
 351  
      */
 352  
     private File getNextTempDirectory() throws AnalysisException {
 353  
         File directory;
 354  
 
 355  
         // getting an exception for some directories not being able to be
 356  
         // created; might be because the directory already exists?
 357  
         do {
 358  3
             dirCount += 1;
 359  3
             directory = new File(tempFileLocation, String.valueOf(dirCount));
 360  3
         } while (directory.exists());
 361  3
         if (!directory.mkdirs()) {
 362  0
             throw new AnalysisException(String.format(
 363  
                     "Unable to create temp directory '%s'.",
 364  
                     directory.getAbsolutePath()));
 365  
         }
 366  3
         return directory;
 367  
     }
 368  
 }