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 com.github.packageurl.MalformedPackageURLException;
21  
22  import java.io.File;
23  import java.io.FileFilter;
24  import java.io.IOException;
25  import java.io.InputStream;
26  
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.utils.FileFilterBuilder;
32  import org.owasp.dependencycheck.utils.FileUtils;
33  import org.owasp.dependencycheck.utils.Settings;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  
37  import java.util.ArrayList;
38  import java.util.List;
39  import javax.annotation.concurrent.ThreadSafe;
40  
41  import org.apache.commons.lang3.StringUtils;
42  import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
43  import org.owasp.dependencycheck.exception.InitializationException;
44  import org.owasp.dependencycheck.dependency.EvidenceType;
45  import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
46  import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
47  import org.owasp.dependencycheck.processing.GrokAssemblyProcessor;
48  import org.owasp.dependencycheck.utils.DependencyVersion;
49  import org.owasp.dependencycheck.utils.DependencyVersionUtil;
50  import org.owasp.dependencycheck.utils.ExtractionException;
51  import org.owasp.dependencycheck.utils.ExtractionUtil;
52  import org.owasp.dependencycheck.utils.processing.ProcessReader;
53  import org.owasp.dependencycheck.xml.assembly.AssemblyData;
54  import org.owasp.dependencycheck.xml.assembly.GrokParseException;
55  
56  /**
57   * Analyzer for getting company, product, and version information from a .NET
58   * assembly.
59   *
60   * @author colezlaw
61   */
62  @ThreadSafe
63  public class AssemblyAnalyzer extends AbstractFileTypeAnalyzer {
64  
65      /**
66       * Logger
67       */
68      private static final Logger LOGGER = LoggerFactory.getLogger(AssemblyAnalyzer.class);
69      /**
70       * The analyzer name
71       */
72      private static final String ANALYZER_NAME = "Assembly Analyzer";
73      /**
74       * The analysis phase
75       */
76      private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
77      /**
78       * A descriptor for the type of dependencies processed or added by this
79       * analyzer.
80       */
81      public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.DOTNET;
82      /**
83       * The list of supported extensions
84       */
85      private static final String[] SUPPORTED_EXTENSIONS = {"dll", "exe"};
86      /**
87       * The File Filter used to filter supported extensions.
88       */
89      private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(
90              SUPPORTED_EXTENSIONS).build();
91      /**
92       * The file path to `GrokAssembly.dll`.
93       */
94      private File grokAssembly = null;
95  
96      /**
97       * The base argument list to call GrokAssembly.
98       */
99      private List<String> baseArgumentList = null;
100 
101     /**
102      * Builds the beginnings of a List for ProcessBuilder
103      *
104      * @return the list of arguments to begin populating the ProcessBuilder
105      */
106     protected List<String> buildArgumentList() {
107         // Use file.separator as a wild guess as to whether this is Windows
108         final List<String> args = new ArrayList<>();
109         if (!StringUtils.isBlank(getSettings().getString(Settings.KEYS.ANALYZER_ASSEMBLY_DOTNET_PATH))) {
110             args.add(getSettings().getString(Settings.KEYS.ANALYZER_ASSEMBLY_DOTNET_PATH));
111         } else if (isDotnetPath()) {
112             args.add("dotnet");
113         } else {
114             return null;
115         }
116         args.add(grokAssembly.getPath());
117         return args;
118     }
119 
120     /**
121      * Performs the analysis on a single Dependency.
122      *
123      * @param dependency the dependency to analyze
124      * @param engine the engine to perform the analysis under
125      * @throws AnalysisException if anything goes sideways
126      */
127     @Override
128     public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
129         final File test = new File(dependency.getActualFilePath());
130         if (!test.isFile()) {
131             throw new AnalysisException(String.format("%s does not exist and cannot be analyzed by dependency-check",
132                     dependency.getActualFilePath()));
133         }
134         if (grokAssembly == null) {
135             LOGGER.warn("GrokAssembly didn't get deployed");
136             return;
137         }
138         if (baseArgumentList == null) {
139             LOGGER.warn("Assembly Analyzer was unable to execute");
140             return;
141         }
142         final AssemblyData data;
143         final List<String> args = new ArrayList<>(baseArgumentList);
144         args.add(dependency.getActualFilePath());
145         final ProcessBuilder pb = new ProcessBuilder(args);
146         try {
147             final Process proc = pb.start();
148             try (GrokAssemblyProcessor processor = new GrokAssemblyProcessor();
149                     ProcessReader processReader = new ProcessReader(proc, processor)) {
150                 processReader.readAll();
151 
152                 final String errorOutput = processReader.getError();
153                 if (!StringUtils.isBlank(errorOutput)) {
154                     LOGGER.warn("Error from GrokAssembly: {}", errorOutput);
155                 }
156                 final int exitValue = proc.exitValue();
157 
158                 if (exitValue == 3) {
159                     LOGGER.debug("{} is not a .NET assembly or executable and as such cannot be analyzed by dependency-check",
160                             dependency.getActualFilePath());
161                     return;
162                 } else if (exitValue != 0) {
163                     LOGGER.debug("Return code {} from GrokAssembly; dependency-check is unable to analyze the library: {}",
164                             exitValue, dependency.getActualFilePath());
165                     return;
166                 }
167                 data = processor.getAssemblyData();
168             }
169             // First, see if there was an error
170             final String error = data.getError();
171             if (error != null && !error.isEmpty()) {
172                 throw new AnalysisException(error);
173             }
174             if (data.getWarning() != null) {
175                 LOGGER.debug("Grok Assembly - could not get namespace on dependency `{}` - {}", dependency.getActualFilePath(), data.getWarning());
176             }
177             updateDependency(data, dependency);
178         } catch (GrokParseException saxe) {
179             LOGGER.error("----------------------------------------------------");
180             LOGGER.error("Failed to read the Assembly Analyzer results.");
181             LOGGER.error("----------------------------------------------------");
182             throw new AnalysisException("Couldn't parse Assembly Analyzer results (GrokAssembly)", saxe);
183         } catch (IOException ioe) {
184             throw new AnalysisException(ioe);
185         } catch (InterruptedException ex) {
186             Thread.currentThread().interrupt();
187             throw new AnalysisException("GrokAssembly process interrupted", ex);
188         }
189     }
190 
191     /**
192      * Updates the dependency information with the provided assembly data.
193      *
194      * @param data the assembly data
195      * @param dependency the dependency to update
196      */
197     void updateDependency(final AssemblyData data, Dependency dependency) {
198         final StringBuilder sb = new StringBuilder();
199         if (!StringUtils.isBlank(data.getFileDescription())) {
200             sb.append(data.getFileDescription());
201         }
202         if (!StringUtils.isBlank(data.getComments())) {
203             if (sb.length() > 0) {
204                 sb.append("\n\n");
205             }
206             sb.append(data.getComments());
207         }
208         if (!StringUtils.isBlank(data.getLegalCopyright())) {
209             if (sb.length() > 0) {
210                 sb.append("\n\n");
211             }
212             sb.append(data.getLegalCopyright());
213         }
214         if (!StringUtils.isBlank(data.getLegalTrademarks())) {
215             if (sb.length() > 0) {
216                 sb.append("\n");
217             }
218             sb.append(data.getLegalTrademarks());
219         }
220         final String description = sb.toString();
221         if (description.length() > 0) {
222             dependency.setDescription(description);
223             addMatchingValues(data.getNamespaces(), description, dependency, EvidenceType.VENDOR);
224             addMatchingValues(data.getNamespaces(), description, dependency, EvidenceType.PRODUCT);
225         }
226 
227         if (!StringUtils.isBlank(data.getProductVersion())) {
228             dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "ProductVersion", data.getProductVersion(), Confidence.HIGHEST);
229         }
230         if (!StringUtils.isBlank(data.getFileVersion())) {
231             dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "FileVersion", data.getFileVersion(), Confidence.HIGH);
232         }
233 
234         if (data.getFileVersion() != null && data.getProductVersion() != null) {
235             final int max = Math.min(data.getFileVersion().length(), data.getProductVersion().length());
236             int pos;
237             for (pos = 0; pos < max; pos++) {
238                 if (data.getFileVersion().charAt(pos) != data.getProductVersion().charAt(pos)) {
239                     break;
240                 }
241             }
242             final DependencyVersion fileVersion = DependencyVersionUtil.parseVersion(data.getFileVersion(), true);
243             final DependencyVersion productVersion = DependencyVersionUtil.parseVersion(data.getProductVersion(), true);
244             if (pos > 0) {
245                 final DependencyVersion matchingVersion = DependencyVersionUtil.parseVersion(data.getFileVersion().substring(0, pos), true);
246                 if (fileVersion != null && data.getFileVersion() != null
247                         && fileVersion.toString().length() == data.getFileVersion().length()) {
248                     if (matchingVersion != null && matchingVersion.getVersionParts().size() > 2) {
249                         dependency.addEvidence(EvidenceType.VERSION, "AssemblyAnalyzer", "FilteredVersion",
250                                 matchingVersion.toString(), Confidence.HIGHEST);
251                         dependency.setVersion(matchingVersion.toString());
252                     }
253                 }
254             }
255             if (dependency.getVersion() == null) {
256                 if (fileVersion != null && productVersion != null) {
257                     if (fileVersion.toString().startsWith(productVersion.toString())) {
258                         dependency.setVersion(fileVersion.toString());
259                     } else if (productVersion.toString().startsWith(fileVersion.toString())) {
260                         dependency.setVersion(productVersion.toString());
261                     }
262                 }
263             }
264         }
265         if (dependency.getVersion() == null && data.getProductVersion() != null) {
266             final DependencyVersion version = DependencyVersionUtil.parseVersion(data.getProductVersion(), true);
267             if (version != null) {
268                 dependency.setVersion(version.toString());
269             }
270         }
271         if (dependency.getVersion() == null && data.getFileVersion() != null) {
272             final DependencyVersion version = DependencyVersionUtil.parseVersion(data.getFileVersion(), true);
273             if (version != null) {
274                 dependency.setVersion(version.toString());
275             }
276         }
277 
278         if (!StringUtils.isBlank(data.getCompanyName())) {
279             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "CompanyName", data.getCompanyName(), Confidence.HIGHEST);
280             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "CompanyName", data.getCompanyName(), Confidence.LOW);
281             addMatchingValues(data.getNamespaces(), data.getCompanyName(), dependency, EvidenceType.VENDOR);
282         }
283         if (!StringUtils.isBlank(data.getProductName())) {
284             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "ProductName", data.getProductName(), Confidence.HIGHEST);
285             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "ProductName", data.getProductName(), Confidence.MEDIUM);
286             addMatchingValues(data.getNamespaces(), data.getProductName(), dependency, EvidenceType.PRODUCT);
287         }
288         if (!StringUtils.isBlank(data.getFileDescription())) {
289             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "FileDescription", data.getFileDescription(), Confidence.HIGH);
290             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "FileDescription", data.getFileDescription(), Confidence.LOW);
291             addMatchingValues(data.getNamespaces(), data.getFileDescription(), dependency, EvidenceType.PRODUCT);
292         }
293 
294         final String internalName = data.getInternalName();
295         if (!StringUtils.isBlank(internalName)) {
296             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "InternalName", internalName, Confidence.MEDIUM);
297             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "InternalName", internalName, Confidence.LOW);
298             addMatchingValues(data.getNamespaces(), internalName, dependency, EvidenceType.PRODUCT);
299             addMatchingValues(data.getNamespaces(), internalName, dependency, EvidenceType.VENDOR);
300             if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), internalName)) {
301                 final String ext = FileUtils.getFileExtension(internalName);
302                 if (ext != null) {
303                     dependency.setName(internalName.substring(0, internalName.length() - ext.length() - 1));
304                 } else {
305                     dependency.setName(internalName);
306                 }
307             }
308         }
309 
310         final String originalFilename = data.getOriginalFilename();
311         if (!StringUtils.isBlank(originalFilename)) {
312             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "OriginalFilename", originalFilename, Confidence.MEDIUM);
313             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "OriginalFilename", originalFilename, Confidence.LOW);
314             addMatchingValues(data.getNamespaces(), originalFilename, dependency, EvidenceType.PRODUCT);
315             if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), originalFilename)) {
316                 final String ext = FileUtils.getFileExtension(originalFilename);
317                 if (ext != null) {
318                     dependency.setName(originalFilename.substring(0, originalFilename.length() - ext.length() - 1));
319                 } else {
320                     dependency.setName(originalFilename);
321                 }
322             }
323         }
324         if (dependency.getName() != null && dependency.getVersion() != null) {
325             try {
326                 dependency.addSoftwareIdentifier(new PurlIdentifier("generic", dependency.getName(), dependency.getVersion(), Confidence.MEDIUM));
327             } catch (MalformedPackageURLException ex) {
328                 LOGGER.debug("Unable to create Package URL Identifier for " + dependency.getName(), ex);
329                 dependency.addSoftwareIdentifier(new GenericIdentifier(
330                         String.format("%s@%s", dependency.getName(), dependency.getVersion()),
331                         Confidence.MEDIUM));
332             }
333         }
334         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
335     }
336 
337     /**
338      * Initialize the analyzer. In this case, extract GrokAssembly.dll to a
339      * temporary location.
340      *
341      * @param engine a reference to the dependency-check engine
342      * @throws InitializationException thrown if anything goes wrong
343      */
344     @Override
345     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
346         grokAssembly = extractGrokAssembly();
347 
348         baseArgumentList = buildArgumentList();
349         if (baseArgumentList == null) {
350             setEnabled(false);
351             LOGGER.error("----------------------------------------------------");
352             LOGGER.error(".NET Assembly Analyzer could not be initialized and at least one "
353                     + "'exe' or 'dll' was scanned. The 'dotnet' executable could not be found on "
354                     + "the path; either disable the Assembly Analyzer or add the path to dotnet "
355                     + "core in the configuration.");
356             LOGGER.error("The dotnet 8.0 core runtime or SDK is required to analyze assemblies");
357             LOGGER.error("----------------------------------------------------");
358             return;
359         }
360         try {
361             final ProcessBuilder pb = new ProcessBuilder(baseArgumentList);
362             final Process p = pb.start();
363             try (ProcessReader processReader = new ProcessReader(p)) {
364                 processReader.readAll();
365                 final String error = processReader.getError();
366                 if (p.exitValue() != 1 || !StringUtils.isBlank(error)) {
367                     LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer, please see the log for more details.\n"
368                     + "dependency-check requires dotnet 8.0 core runtime or sdk to be installed to analyze assemblies.");
369                     LOGGER.debug("GrokAssembly.dll is not working properly: {}", error);
370                     grokAssembly = null;
371                     setEnabled(false);
372                     throw new InitializationException("Could not execute .NET AssemblyAnalyzer, is the dotnet 8.0 runtime or sdk installed?");
373                 }
374             }
375         } catch (InterruptedException e) {
376             Thread.currentThread().interrupt();
377             LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
378                     + "dependency-check requires dotnet 8.0 core runtime or sdk to be installed to analyze assemblies;\n"
379                     + "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
380             LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
381             setEnabled(false);
382             throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer", e);
383         } catch (IOException e) {
384             LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
385                     + "dependency-check requires dotnet 8.0 core to be installed to analyze assemblies;\n"
386                     + "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
387             LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
388             setEnabled(false);
389             throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer, is the dotnet 8.0 runtime or sdk installed?", e);
390         }
391     }
392 
393     /**
394      * Extracts the GrokAssembly executable.
395      *
396      * @return the path to the extracted executable
397      * @throws InitializationException thrown if the executable could not be
398      * extracted
399      */
400     private File extractGrokAssembly() throws InitializationException {
401         final File location;
402         try (InputStream in = FileUtils.getResourceAsStream("GrokAssembly.zip")) {
403             location = FileUtils.createTempDirectory(getSettings().getTempDirectory());
404             ExtractionUtil.extractFiles(in, location);
405         } catch (ExtractionException ex) {
406             throw new InitializationException("Unable to extract GrokAssembly.dll", ex);
407         } catch (IOException ex) {
408             throw new InitializationException("Unable to create temp directory for GrokAssembly", ex);
409         }
410         return new File(location, "GrokAssembly.dll");
411     }
412 
413     /**
414      * Removes resources used from the local file system.
415      *
416      * @throws Exception thrown if there is a problem closing the analyzer
417      */
418     @Override
419     public void closeAnalyzer() throws Exception {
420         if (grokAssembly != null) {
421             FileUtils.delete(grokAssembly.getParentFile());
422         }
423     }
424 
425     @Override
426     protected FileFilter getFileFilter() {
427         return FILTER;
428     }
429 
430     /**
431      * Gets this analyzer's name.
432      *
433      * @return the analyzer name
434      */
435     @Override
436     public String getName() {
437         return ANALYZER_NAME;
438     }
439 
440     /**
441      * Returns the phase this analyzer runs under.
442      *
443      * @return the phase this runs under
444      */
445     @Override
446     public AnalysisPhase getAnalysisPhase() {
447         return ANALYSIS_PHASE;
448     }
449 
450     /**
451      * Returns the key used in the properties file to reference the analyzer's
452      * enabled property.
453      *
454      * @return the analyzer's enabled property setting key
455      */
456     @Override
457     protected String getAnalyzerEnabledSettingKey() {
458         return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
459     }
460 
461     /**
462      * Tests to see if a file is in the system path.
463      *
464      * @return <code>true</code> if dotnet could be found in the path; otherwise
465      * <code>false</code>
466      */
467     private boolean isDotnetPath() {
468         final String[] args = new String[2];
469         args[0] = "dotnet";
470         args[1] = "--info";
471         final ProcessBuilder pb = new ProcessBuilder(args);
472         try {
473             final Process proc = pb.start();
474             try (ProcessReader processReader = new ProcessReader(proc)) {
475                 processReader.readAll();
476                 final int exitValue = proc.exitValue();
477                 if (exitValue == 0) {
478                     return true;
479                 }
480                 final String output = processReader.getOutput();
481                 if (output.length() > 0) {
482                     return true;
483                 }
484             }
485         } catch (InterruptedException ex) {
486             Thread.currentThread().interrupt();
487             LOGGER.debug("Path search failed for dotnet", ex);
488         } catch (IOException ex) {
489             LOGGER.debug("Path search failed for dotnet", ex);
490         }
491         return false;
492     }
493 
494     /**
495      * Cycles through the collection of class name information to see if parts
496      * of the package names are contained in the provided value. If found, it
497      * will be added as the HIGHEST confidence evidence because we have more
498      * then one source corroborating the value.
499      *
500      * @param packages a collection of class name information
501      * @param value the value to check to see if it contains a package name
502      * @param dep the dependency to add new entries too
503      * @param type the type of evidence (vendor, product, or version)
504      */
505     protected static void addMatchingValues(List<String> packages, String value, Dependency dep, EvidenceType type) {
506         if (value == null || value.isEmpty() || packages == null || packages.isEmpty()) {
507             return;
508         }
509         for (String key : packages) {
510             final int pos = StringUtils.indexOfIgnoreCase(value, key);
511             if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length()
512                     && !Character.isLetterOrDigit(value.charAt(key.length())))))
513                     || (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1))
514                     && (pos + key.length() == value.length() || (key.length() < value.length()
515                     && !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) {
516                 dep.addEvidence(type, "dll", "namespace", key, Confidence.HIGHEST);
517             }
518 
519         }
520     }
521 
522     /**
523      * Used in testing only - this simply returns the path to the extracted
524      * GrokAssembly.dll.
525      *
526      * @return the path to the extracted GrokAssembly.dll
527      */
528     File getGrokAssemblyPath() {
529         return grokAssembly;
530     }
531 }