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