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) 2012 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.analyzer;
19  
20  import java.io.File;
21  import java.io.FileFilter;
22  import java.io.FileOutputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import org.apache.commons.io.IOUtils;
26  import org.apache.commons.io.output.NullOutputStream;
27  import org.owasp.dependencycheck.Engine;
28  import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
29  import org.owasp.dependencycheck.dependency.Confidence;
30  import org.owasp.dependencycheck.dependency.Dependency;
31  import org.owasp.dependencycheck.dependency.Evidence;
32  import org.owasp.dependencycheck.utils.FileFilterBuilder;
33  import org.owasp.dependencycheck.utils.Settings;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import org.w3c.dom.Document;
37  import org.xml.sax.SAXException;
38  
39  import javax.xml.parsers.DocumentBuilder;
40  import javax.xml.parsers.DocumentBuilderFactory;
41  import javax.xml.xpath.XPath;
42  import javax.xml.xpath.XPathExpressionException;
43  import javax.xml.xpath.XPathFactory;
44  import java.util.ArrayList;
45  import java.util.List;
46  import javax.xml.parsers.ParserConfigurationException;
47  import org.owasp.dependencycheck.exception.InitializationException;
48  import org.apache.commons.lang3.SystemUtils;
49  
50  /**
51   * Analyzer for getting company, product, and version information from a .NET
52   * assembly.
53   *
54   * @author colezlaw
55   *
56   */
57  public class AssemblyAnalyzer extends AbstractFileTypeAnalyzer {
58  
59      /**
60       * The analyzer name
61       */
62      private static final String ANALYZER_NAME = "Assembly Analyzer";
63      /**
64       * The analysis phase
65       */
66      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
67      /**
68       * The list of supported extensions
69       */
70      private static final String[] SUPPORTED_EXTENSIONS = {"dll", "exe"};
71      /**
72       * The temp value for GrokAssembly.exe
73       */
74      private File grokAssemblyExe = null;
75      /**
76       * Logger
77       */
78      private static final Logger LOGGER = LoggerFactory.getLogger(AssemblyAnalyzer.class);
79  
80      /**
81       * Builds the beginnings of a List for ProcessBuilder
82       *
83       * @return the list of arguments to begin populating the ProcessBuilder
84       */
85      protected List<String> buildArgumentList() {
86          // Use file.separator as a wild guess as to whether this is Windows
87          final List<String> args = new ArrayList<String>();
88          if (!SystemUtils.IS_OS_WINDOWS) {
89              if (Settings.getString(Settings.KEYS.ANALYZER_ASSEMBLY_MONO_PATH) != null) {
90                  args.add(Settings.getString(Settings.KEYS.ANALYZER_ASSEMBLY_MONO_PATH));
91              } else if (isInPath("mono")) {
92                  args.add("mono");
93              } else {
94                  return null;
95              }
96          }
97          args.add(grokAssemblyExe.getPath());
98          return args;
99      }
100 
101     /**
102      * Performs the analysis on a single Dependency.
103      *
104      * @param dependency the dependency to analyze
105      * @param engine the engine to perform the analysis under
106      * @throws AnalysisException if anything goes sideways
107      */
108     @Override
109     public void analyzeFileType(Dependency dependency, Engine engine)
110             throws AnalysisException {
111         if (grokAssemblyExe == null) {
112             LOGGER.warn("GrokAssembly didn't get deployed");
113             return;
114         }
115 
116         final List<String> args = buildArgumentList();
117         if (args == null) {
118             LOGGER.warn("Assembly Analyzer was unable to execute");
119             return;
120         }
121         args.add(dependency.getActualFilePath());
122         final ProcessBuilder pb = new ProcessBuilder(args);
123         Document doc = null;
124         try {
125             final Process proc = pb.start();
126 
127             final DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
128             doc = builder.parse(proc.getInputStream());
129 
130             // Try evacuating the error stream
131             final String errorStream = IOUtils.toString(proc.getErrorStream(), "UTF-8");
132             if (null != errorStream && !errorStream.isEmpty()) {
133                 LOGGER.warn("Error from GrokAssembly: {}", errorStream);
134             }
135 
136             int rc = 0;
137             try {
138                 rc = proc.waitFor();
139             } catch (InterruptedException ie) {
140                 return;
141             }
142             if (rc == 3) {
143                 LOGGER.debug("{} is not a .NET assembly or executable and as such cannot be analyzed by dependency-check",
144                         dependency.getActualFilePath());
145                 return;
146             } else if (rc != 0) {
147                 LOGGER.warn("Return code {} from GrokAssembly", rc);
148             }
149 
150             final XPath xpath = XPathFactory.newInstance().newXPath();
151 
152             // First, see if there was an error
153             final String error = xpath.evaluate("/assembly/error", doc);
154             if (error != null && !error.isEmpty()) {
155                 throw new AnalysisException(error);
156             }
157 
158             final String version = xpath.evaluate("/assembly/version", doc);
159             if (version != null) {
160                 dependency.getVersionEvidence().addEvidence(new Evidence("grokassembly", "version",
161                         version, Confidence.HIGHEST));
162             }
163 
164             final String vendor = xpath.evaluate("/assembly/company", doc);
165             if (vendor != null) {
166                 dependency.getVendorEvidence().addEvidence(new Evidence("grokassembly", "vendor",
167                         vendor, Confidence.HIGH));
168             }
169 
170             final String product = xpath.evaluate("/assembly/product", doc);
171             if (product != null) {
172                 dependency.getProductEvidence().addEvidence(new Evidence("grokassembly", "product",
173                         product, Confidence.HIGH));
174             }
175 
176         } catch (ParserConfigurationException pce) {
177             throw new AnalysisException("Error initializing the assembly analyzer", pce);
178         } catch (IOException ioe) {
179             throw new AnalysisException(ioe);
180         } catch (SAXException saxe) {
181             throw new AnalysisException("Couldn't parse GrokAssembly result", saxe);
182         } catch (XPathExpressionException xpe) {
183             // This shouldn't happen
184             throw new AnalysisException(xpe);
185         }
186     }
187 
188     /**
189      * Initialize the analyzer. In this case, extract GrokAssembly.exe to a
190      * temporary location.
191      *
192      * @throws InitializationException thrown if anything goes wrong
193      */
194     @Override
195     public void initializeFileTypeAnalyzer() throws InitializationException {
196         final File tempFile;
197         try {
198             tempFile = File.createTempFile("GKA", ".exe", Settings.getTempDirectory());
199         } catch (IOException ex) {
200             setEnabled(false);
201             throw new InitializationException("Unable to create temporary file for the assembly analyzerr", ex);
202         }
203         FileOutputStream fos = null;
204         InputStream is = null;
205         try {
206             fos = new FileOutputStream(tempFile);
207             is = AssemblyAnalyzer.class.getClassLoader().getResourceAsStream("GrokAssembly.exe");
208             IOUtils.copy(is, fos);
209 
210             grokAssemblyExe = tempFile;
211             LOGGER.debug("Extracted GrokAssembly.exe to {}", grokAssemblyExe.getPath());
212         } catch (IOException ioe) {
213             this.setEnabled(false);
214             LOGGER.warn("Could not extract GrokAssembly.exe: {}", ioe.getMessage());
215             throw new InitializationException("Could not extract GrokAssembly.exe", ioe);
216         } finally {
217             if (fos != null) {
218                 try {
219                     fos.close();
220                 } catch (Throwable e) {
221                     LOGGER.debug("Error closing output stream");
222                 }
223             }
224             if (is != null) {
225                 try {
226                     is.close();
227                 } catch (Throwable e) {
228                     LOGGER.debug("Error closing input stream");
229                 }
230             }
231         }
232 
233         // Now, need to see if GrokAssembly actually runs from this location.
234         final List<String> args = buildArgumentList();
235         //TODO this creates an "unreported" error - if someone doesn't look
236         // at the command output this could easily be missed (especially in an
237         // Ant or Maven build.
238         //
239         // We need to create a non-fatal warning error type that will
240         // get added to the report.
241         //TOOD this idea needs to get replicated to the bundle audit analyzer.
242         if (args == null) {
243             setEnabled(false);
244             LOGGER.error("----------------------------------------------------");
245             LOGGER.error(".NET Assembly Analyzer could not be initialized and at least one "
246                     + "'exe' or 'dll' was scanned. The 'mono' executable could not be found on "
247                     + "the path; either disable the Assembly Analyzer or configure the path mono.");
248             LOGGER.error("----------------------------------------------------");
249             return;
250         }
251         try {
252             final ProcessBuilder pb = new ProcessBuilder(args);
253             final Process p = pb.start();
254             // Try evacuating the error stream
255             IOUtils.copy(p.getErrorStream(), NullOutputStream.NULL_OUTPUT_STREAM);
256 
257             final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
258             factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
259             final DocumentBuilder builder = factory.newDocumentBuilder();
260             final Document doc = builder.parse(p.getInputStream());
261             final XPath xpath = XPathFactory.newInstance().newXPath();
262             final String error = xpath.evaluate("/assembly/error", doc);
263             if (p.waitFor() != 1 || error == null || error.isEmpty()) {
264                 LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer, please see the log for more details.");
265                 LOGGER.debug("GrokAssembly.exe is not working properly");
266                 grokAssemblyExe = null;
267                 setEnabled(false);
268                 throw new InitializationException("Could not execute .NET AssemblyAnalyzer");
269             }
270         } catch (InitializationException e) {
271             setEnabled(false);
272             throw e;
273         } catch (Throwable e) {
274             LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
275                     + "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
276             LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
277             setEnabled(false);
278             throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer", e);
279         }
280     }
281 
282     /**
283      * Removes resources used from the local file system.
284      *
285      * @throws Exception thrown if there is a problem closing the analyzer
286      */
287     @Override
288     public void close() throws Exception {
289         super.close();
290         try {
291             if (grokAssemblyExe != null && !grokAssemblyExe.delete()) {
292                 LOGGER.debug("Unable to delete temporary GrokAssembly.exe; attempting delete on exit");
293                 grokAssemblyExe.deleteOnExit();
294             }
295         } catch (SecurityException se) {
296             LOGGER.debug("Can't delete temporary GrokAssembly.exe");
297             grokAssemblyExe.deleteOnExit();
298         }
299     }
300 
301     /**
302      * The File Filter used to filter supported extensions.
303      */
304     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(
305             SUPPORTED_EXTENSIONS).build();
306 
307     @Override
308     protected FileFilter getFileFilter() {
309         return FILTER;
310     }
311 
312     /**
313      * Gets this analyzer's name.
314      *
315      * @return the analyzer name
316      */
317     @Override
318     public String getName() {
319         return ANALYZER_NAME;
320     }
321 
322     /**
323      * Returns the phase this analyzer runs under.
324      *
325      * @return the phase this runs under
326      */
327     @Override
328     public AnalysisPhase getAnalysisPhase() {
329         return ANALYSIS_PHASE;
330     }
331 
332     /**
333      * Returns the key used in the properties file to reference the analyzer's
334      * enabled property.
335      *
336      * @return the analyzer's enabled property setting key
337      */
338     @Override
339     protected String getAnalyzerEnabledSettingKey() {
340         return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
341     }
342 
343     /**
344      * Tests to see if a file is in the system path. <b>Note</b> - the current
345      * implementation only works on non-windows platforms. For purposes of the
346      * AssemblyAnalyzer this is okay as this is only needed on Mac/*nix.
347      *
348      * @param file the executable to look for
349      * @return <code>true</code> if the file exists; otherwise
350      * <code>false</code>
351      */
352     private boolean isInPath(String file) {
353         final ProcessBuilder pb = new ProcessBuilder("which", file);
354         try {
355             final Process proc = pb.start();
356             final int retCode = proc.waitFor();
357             if (retCode == 0) {
358                 return true;
359             }
360         } catch (IOException ex) {
361             LOGGER.debug("Path seach failed for " + file);
362         } catch (InterruptedException ex) {
363             LOGGER.debug("Path seach failed for " + file);
364         }
365         return false;
366     }
367 }