1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
58
59
60
61
62 @ThreadSafe
63 public class AssemblyAnalyzer extends AbstractFileTypeAnalyzer {
64
65
66
67
68 private static final Logger LOGGER = LoggerFactory.getLogger(AssemblyAnalyzer.class);
69
70
71
72 private static final String ANALYZER_NAME = "Assembly Analyzer";
73
74
75
76 private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
77
78
79
80
81 public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.DOTNET;
82
83
84
85 private static final String[] SUPPORTED_EXTENSIONS = {"dll", "exe"};
86
87
88
89 private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(
90 SUPPORTED_EXTENSIONS).build();
91
92
93
94 private File grokAssembly = null;
95
96
97
98
99 private List<String> baseArgumentList = null;
100
101
102
103
104
105
106 protected List<String> buildArgumentList() {
107
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
122
123
124
125
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
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
193
194
195
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
339
340
341
342
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
395
396
397
398
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
415
416
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
432
433
434
435 @Override
436 public String getName() {
437 return ANALYZER_NAME;
438 }
439
440
441
442
443
444
445 @Override
446 public AnalysisPhase getAnalysisPhase() {
447 return ANALYSIS_PHASE;
448 }
449
450
451
452
453
454
455
456 @Override
457 protected String getAnalyzerEnabledSettingKey() {
458 return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
459 }
460
461
462
463
464
465
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
496
497
498
499
500
501
502
503
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
524
525
526
527
528 File getGrokAssemblyPath() {
529 return grokAssembly;
530 }
531 }