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