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.xpath.XPath;
41  import javax.xml.xpath.XPathExpressionException;
42  import javax.xml.xpath.XPathFactory;
43  import java.util.ArrayList;
44  import java.util.List;
45  import javax.xml.parsers.ParserConfigurationException;
46  import org.owasp.dependencycheck.exception.InitializationException;
47  import org.apache.commons.lang3.SystemUtils;
48  import org.owasp.dependencycheck.utils.XmlUtils;
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 analyzeDependency(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             final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder();
127 
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             LOGGER.error("----------------------------------------------------");
182             LOGGER.error("Failed to read the Assembly Analyzer results. "
183                     + "On some systems mono-runtime and mono-devel need to be installed.");
184             LOGGER.error("----------------------------------------------------");
185             throw new AnalysisException("Couldn't parse Assembly Analzyzer results (GrokAssembly)", saxe);
186         } catch (XPathExpressionException xpe) {
187             // This shouldn't happen
188             throw new AnalysisException(xpe);
189         }
190     }
191 
192     /**
193      * Initialize the analyzer. In this case, extract GrokAssembly.exe to a
194      * temporary location.
195      *
196      * @throws InitializationException thrown if anything goes wrong
197      */
198     @Override
199     public void initializeFileTypeAnalyzer() throws InitializationException {
200         final File tempFile;
201         try {
202             tempFile = File.createTempFile("GKA", ".exe", Settings.getTempDirectory());
203         } catch (IOException ex) {
204             setEnabled(false);
205             throw new InitializationException("Unable to create temporary file for the assembly analyzerr", ex);
206         }
207         FileOutputStream fos = null;
208         InputStream is = null;
209         try {
210             fos = new FileOutputStream(tempFile);
211             is = AssemblyAnalyzer.class.getClassLoader().getResourceAsStream("GrokAssembly.exe");
212             IOUtils.copy(is, fos);
213 
214             grokAssemblyExe = tempFile;
215             LOGGER.debug("Extracted GrokAssembly.exe to {}", grokAssemblyExe.getPath());
216         } catch (IOException ioe) {
217             this.setEnabled(false);
218             LOGGER.warn("Could not extract GrokAssembly.exe: {}", ioe.getMessage());
219             throw new InitializationException("Could not extract GrokAssembly.exe", ioe);
220         } finally {
221             if (fos != null) {
222                 try {
223                     fos.close();
224                 } catch (Throwable e) {
225                     LOGGER.debug("Error closing output stream");
226                 }
227             }
228             if (is != null) {
229                 try {
230                     is.close();
231                 } catch (Throwable e) {
232                     LOGGER.debug("Error closing input stream");
233                 }
234             }
235         }
236 
237         // Now, need to see if GrokAssembly actually runs from this location.
238         final List<String> args = buildArgumentList();
239         //TODO this creates an "unreported" error - if someone doesn't look
240         // at the command output this could easily be missed (especially in an
241         // Ant or Maven build.
242         //
243         // We need to create a non-fatal warning error type that will
244         // get added to the report.
245         //TOOD this idea needs to get replicated to the bundle audit analyzer.
246         if (args == null) {
247             setEnabled(false);
248             LOGGER.error("----------------------------------------------------");
249             LOGGER.error(".NET Assembly Analyzer could not be initialized and at least one "
250                     + "'exe' or 'dll' was scanned. The 'mono' executable could not be found on "
251                     + "the path; either disable the Assembly Analyzer or configure the path mono. "
252                     + "On some systems mono-runtime and mono-devel need to be installed.");
253             LOGGER.error("----------------------------------------------------");
254             return;
255         }
256         try {
257             final ProcessBuilder pb = new ProcessBuilder(args);
258             final Process p = pb.start();
259             // Try evacuating the error stream
260             IOUtils.copy(p.getErrorStream(), NullOutputStream.NULL_OUTPUT_STREAM);
261 
262             final DocumentBuilder builder = XmlUtils.buildSecureDocumentBuilder();
263             final Document doc = builder.parse(p.getInputStream());
264             final XPath xpath = XPathFactory.newInstance().newXPath();
265             final String error = xpath.evaluate("/assembly/error", doc);
266             if (p.waitFor() != 1 || error == null || error.isEmpty()) {
267                 LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer, please see the log for more details.");
268                 LOGGER.debug("GrokAssembly.exe is not working properly");
269                 grokAssemblyExe = null;
270                 setEnabled(false);
271                 throw new InitializationException("Could not execute .NET AssemblyAnalyzer");
272             }
273         } catch (InitializationException e) {
274             setEnabled(false);
275             throw e;
276         } catch (Throwable e) {
277             LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
278                     + "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
279             LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
280             setEnabled(false);
281             throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer", e);
282         }
283     }
284 
285     /**
286      * Removes resources used from the local file system.
287      *
288      * @throws Exception thrown if there is a problem closing the analyzer
289      */
290     @Override
291     public void closeAnalyzer() throws Exception {
292         try {
293             if (grokAssemblyExe != null && !grokAssemblyExe.delete()) {
294                 LOGGER.debug("Unable to delete temporary GrokAssembly.exe; attempting delete on exit");
295                 grokAssemblyExe.deleteOnExit();
296             }
297         } catch (SecurityException se) {
298             LOGGER.debug("Can't delete temporary GrokAssembly.exe");
299             grokAssemblyExe.deleteOnExit();
300         }
301     }
302 
303     /**
304      * The File Filter used to filter supported extensions.
305      */
306     private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(
307             SUPPORTED_EXTENSIONS).build();
308 
309     @Override
310     protected FileFilter getFileFilter() {
311         return FILTER;
312     }
313 
314     /**
315      * Gets this analyzer's name.
316      *
317      * @return the analyzer name
318      */
319     @Override
320     public String getName() {
321         return ANALYZER_NAME;
322     }
323 
324     /**
325      * Returns the phase this analyzer runs under.
326      *
327      * @return the phase this runs under
328      */
329     @Override
330     public AnalysisPhase getAnalysisPhase() {
331         return ANALYSIS_PHASE;
332     }
333 
334     /**
335      * Returns the key used in the properties file to reference the analyzer's
336      * enabled property.
337      *
338      * @return the analyzer's enabled property setting key
339      */
340     @Override
341     protected String getAnalyzerEnabledSettingKey() {
342         return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
343     }
344 
345     /**
346      * Tests to see if a file is in the system path. <b>Note</b> - the current
347      * implementation only works on non-windows platforms. For purposes of the
348      * AssemblyAnalyzer this is okay as this is only needed on Mac/*nix.
349      *
350      * @param file the executable to look for
351      * @return <code>true</code> if the file exists; otherwise
352      * <code>false</code>
353      */
354     private boolean isInPath(String file) {
355         final ProcessBuilder pb = new ProcessBuilder("which", file);
356         try {
357             final Process proc = pb.start();
358             final int retCode = proc.waitFor();
359             if (retCode == 0) {
360                 return true;
361             }
362         } catch (IOException ex) {
363             LOGGER.debug("Path seach failed for " + file);
364         } catch (InterruptedException ex) {
365             LOGGER.debug("Path seach failed for " + file);
366         }
367         return false;
368     }
369 }