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     private 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 (data.getFileVersion() != null && data.getProductVersion() != null
257                         && data.getFileVersion().length() >= data.getProductVersion().length()) {
258                     if (fileVersion != null && fileVersion.toString().length() == data.getFileVersion().length()) {
259                         dependency.setVersion(fileVersion.toString());
260                     } else if (productVersion != null && productVersion.toString().length() == data.getProductVersion().length()) {
261                         dependency.setVersion(productVersion.toString());
262                     }
263                 } else {
264                     if (productVersion != null && productVersion.toString().length() == data.getProductVersion().length()) {
265                         dependency.setVersion(productVersion.toString());
266                     } else if (fileVersion != null && fileVersion.toString().length() == data.getFileVersion().length()) {
267                         dependency.setVersion(fileVersion.toString());
268                     }
269                 }
270             }
271         }
272         if (dependency.getVersion() == null && data.getFileVersion() != null) {
273             final DependencyVersion version = DependencyVersionUtil.parseVersion(data.getFileVersion(), true);
274             if (version != null) {
275                 dependency.setVersion(version.toString());
276             }
277         }
278         if (dependency.getVersion() == null && data.getProductVersion() != null) {
279             final DependencyVersion version = DependencyVersionUtil.parseVersion(data.getProductVersion(), true);
280             if (version != null) {
281                 dependency.setVersion(version.toString());
282             }
283         }
284 
285         if (!StringUtils.isBlank(data.getCompanyName())) {
286             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "CompanyName", data.getCompanyName(), Confidence.HIGHEST);
287             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "CompanyName", data.getCompanyName(), Confidence.LOW);
288             addMatchingValues(data.getNamespaces(), data.getCompanyName(), dependency, EvidenceType.VENDOR);
289         }
290         if (!StringUtils.isBlank(data.getProductName())) {
291             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "ProductName", data.getProductName(), Confidence.HIGHEST);
292             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "ProductName", data.getProductName(), Confidence.MEDIUM);
293             addMatchingValues(data.getNamespaces(), data.getProductName(), dependency, EvidenceType.PRODUCT);
294         }
295         if (!StringUtils.isBlank(data.getFileDescription())) {
296             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "FileDescription", data.getFileDescription(), Confidence.HIGH);
297             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "FileDescription", data.getFileDescription(), Confidence.LOW);
298             addMatchingValues(data.getNamespaces(), data.getFileDescription(), dependency, EvidenceType.PRODUCT);
299         }
300 
301         final String internalName = data.getInternalName();
302         if (!StringUtils.isBlank(internalName)) {
303             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "InternalName", internalName, Confidence.MEDIUM);
304             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "InternalName", internalName, Confidence.LOW);
305             addMatchingValues(data.getNamespaces(), internalName, dependency, EvidenceType.PRODUCT);
306             addMatchingValues(data.getNamespaces(), internalName, dependency, EvidenceType.VENDOR);
307             if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), internalName)) {
308                 final String ext = FileUtils.getFileExtension(internalName);
309                 if (ext != null) {
310                     dependency.setName(internalName.substring(0, internalName.length() - ext.length() - 1));
311                 } else {
312                     dependency.setName(internalName);
313                 }
314             }
315         }
316 
317         final String originalFilename = data.getOriginalFilename();
318         if (!StringUtils.isBlank(originalFilename)) {
319             dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "OriginalFilename", originalFilename, Confidence.MEDIUM);
320             dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "OriginalFilename", originalFilename, Confidence.LOW);
321             addMatchingValues(data.getNamespaces(), originalFilename, dependency, EvidenceType.PRODUCT);
322             if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), originalFilename)) {
323                 final String ext = FileUtils.getFileExtension(originalFilename);
324                 if (ext != null) {
325                     dependency.setName(originalFilename.substring(0, originalFilename.length() - ext.length() - 1));
326                 } else {
327                     dependency.setName(originalFilename);
328                 }
329             }
330         }
331         if (dependency.getName() != null && dependency.getVersion() != null) {
332             try {
333                 dependency.addSoftwareIdentifier(new PurlIdentifier("generic", dependency.getName(), dependency.getVersion(), Confidence.MEDIUM));
334             } catch (MalformedPackageURLException ex) {
335                 LOGGER.debug("Unable to create Package URL Identifier for " + dependency.getName(), ex);
336                 dependency.addSoftwareIdentifier(new GenericIdentifier(
337                         String.format("%s@%s", dependency.getName(), dependency.getVersion()),
338                         Confidence.MEDIUM));
339             }
340         }
341         dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
342     }
343 
344     /**
345      * Initialize the analyzer. In this case, extract GrokAssembly.dll to a
346      * temporary location.
347      *
348      * @param engine a reference to the dependency-check engine
349      * @throws InitializationException thrown if anything goes wrong
350      */
351     @Override
352     public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
353         grokAssembly = extractGrokAssembly();
354 
355         baseArgumentList = buildArgumentList();
356         if (baseArgumentList == null) {
357             setEnabled(false);
358             LOGGER.error("----------------------------------------------------");
359             LOGGER.error(".NET Assembly Analyzer could not be initialized and at least one "
360                     + "'exe' or 'dll' was scanned. The 'dotnet' executable could not be found on "
361                     + "the path; either disable the Assembly Analyzer or add the path to dotnet "
362                     + "core in the configuration.");
363             LOGGER.error("The dotnet 8.0 core runtime or SDK is required to analyze assemblies");
364             LOGGER.error("----------------------------------------------------");
365             return;
366         }
367         try {
368             final ProcessBuilder pb = new ProcessBuilder(baseArgumentList);
369             final Process p = pb.start();
370             try (ProcessReader processReader = new ProcessReader(p)) {
371                 processReader.readAll();
372                 final String error = processReader.getError();
373                 if (p.exitValue() != 1 || !StringUtils.isBlank(error)) {
374                     LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer, please see the log for more details.\n"
375                     + "dependency-check requires dotnet 8.0 core runtime or sdk to be installed to analyze assemblies.");
376                     LOGGER.debug("GrokAssembly.dll is not working properly");
377                     grokAssembly = null;
378                     setEnabled(false);
379                     throw new InitializationException("Could not execute .NET AssemblyAnalyzer, is the dotnet 8.0 runtime or sdk installed?");
380                 }
381             }
382         } catch (InterruptedException e) {
383             Thread.currentThread().interrupt();
384             LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
385                     + "dependency-check requires dotnet 8.0 core runtime or sdk 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", e);
390         } catch (IOException e) {
391             LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
392                     + "dependency-check requires dotnet 8.0 core to be installed to analyze assemblies;\n"
393                     + "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
394             LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
395             setEnabled(false);
396             throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer, is the dotnet 8.0 runtime or sdk installed?", e);
397         }
398     }
399 
400     /**
401      * Extracts the GrokAssembly executable.
402      *
403      * @return the path to the extracted executable
404      * @throws InitializationException thrown if the executable could not be
405      * extracted
406      */
407     private File extractGrokAssembly() throws InitializationException {
408         final File location;
409         try (InputStream in = FileUtils.getResourceAsStream("GrokAssembly.zip")) {
410             if (in == null) {
411                 throw new InitializationException("Unable to extract GrokAssembly.dll - file not found");
412             }
413             location = FileUtils.createTempDirectory(getSettings().getTempDirectory());
414             ExtractionUtil.extractFiles(in, location);
415         } catch (ExtractionException ex) {
416             throw new InitializationException("Unable to extract GrokAssembly.dll", ex);
417         } catch (IOException ex) {
418             throw new InitializationException("Unable to create temp directory for GrokAssembly", ex);
419         }
420         return new File(location, "GrokAssembly.dll");
421     }
422 
423     /**
424      * Removes resources used from the local file system.
425      *
426      * @throws Exception thrown if there is a problem closing the analyzer
427      */
428     @Override
429     public void closeAnalyzer() throws Exception {
430         FileUtils.delete(grokAssembly.getParentFile());
431     }
432 
433     @Override
434     protected FileFilter getFileFilter() {
435         return FILTER;
436     }
437 
438     /**
439      * Gets this analyzer's name.
440      *
441      * @return the analyzer name
442      */
443     @Override
444     public String getName() {
445         return ANALYZER_NAME;
446     }
447 
448     /**
449      * Returns the phase this analyzer runs under.
450      *
451      * @return the phase this runs under
452      */
453     @Override
454     public AnalysisPhase getAnalysisPhase() {
455         return ANALYSIS_PHASE;
456     }
457 
458     /**
459      * Returns the key used in the properties file to reference the analyzer's
460      * enabled property.
461      *
462      * @return the analyzer's enabled property setting key
463      */
464     @Override
465     protected String getAnalyzerEnabledSettingKey() {
466         return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
467     }
468 
469     /**
470      * Tests to see if a file is in the system path.
471      *
472      * @return <code>true</code> if dotnet could be found in the path; otherwise
473      * <code>false</code>
474      */
475     private boolean isDotnetPath() {
476         final String[] args = new String[2];
477         args[0] = "dotnet";
478         args[1] = "--info";
479         final ProcessBuilder pb = new ProcessBuilder(args);
480         try {
481             final Process proc = pb.start();
482             try (ProcessReader processReader = new ProcessReader(proc)) {
483                 processReader.readAll();
484                 final int exitValue = proc.exitValue();
485                 if (exitValue == 0) {
486                     return true;
487                 }
488                 final String output = processReader.getOutput();
489                 if (output.length() > 0) {
490                     return true;
491                 }
492             }
493         } catch (InterruptedException ex) {
494             Thread.currentThread().interrupt();
495             LOGGER.debug("Path search failed for dotnet", ex);
496         } catch (IOException ex) {
497             LOGGER.debug("Path search failed for dotnet", ex);
498         }
499         return false;
500     }
501 
502     /**
503      * Cycles through the collection of class name information to see if parts
504      * of the package names are contained in the provided value. If found, it
505      * will be added as the HIGHEST confidence evidence because we have more
506      * then one source corroborating the value.
507      *
508      * @param packages a collection of class name information
509      * @param value the value to check to see if it contains a package name
510      * @param dep the dependency to add new entries too
511      * @param type the type of evidence (vendor, product, or version)
512      */
513     protected static void addMatchingValues(List<String> packages, String value, Dependency dep, EvidenceType type) {
514         if (value == null || value.isEmpty() || packages == null || packages.isEmpty()) {
515             return;
516         }
517         for (String key : packages) {
518             final int pos = StringUtils.indexOfIgnoreCase(value, key);
519             if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length()
520                     && !Character.isLetterOrDigit(value.charAt(key.length())))))
521                     || (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1))
522                     && (pos + key.length() == value.length() || (key.length() < value.length()
523                     && !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) {
524                 dep.addEvidence(type, "dll", "namespace", key, Confidence.HIGHEST);
525             }
526 
527         }
528     }
529 
530     /**
531      * Used in testing only - this simply returns the path to the extracted
532      * GrokAssembly.dll.
533      *
534      * @return the path to the extracted GrokAssembly.dll
535      */
536     File getGrokAssemblyPath() {
537         return grokAssembly;
538     }
539 }