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 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
346
347
348
349
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
402
403
404
405
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
425
426
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
440
441
442
443 @Override
444 public String getName() {
445 return ANALYZER_NAME;
446 }
447
448
449
450
451
452
453 @Override
454 public AnalysisPhase getAnalysisPhase() {
455 return ANALYSIS_PHASE;
456 }
457
458
459
460
461
462
463
464 @Override
465 protected String getAnalyzerEnabledSettingKey() {
466 return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
467 }
468
469
470
471
472
473
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
504
505
506
507
508
509
510
511
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
532
533
534
535
536 File getGrokAssemblyPath() {
537 return grokAssembly;
538 }
539 }