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