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 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
341
342
343
344
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
397
398
399
400
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
420
421
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
435
436
437
438 @Override
439 public String getName() {
440 return ANALYZER_NAME;
441 }
442
443
444
445
446
447
448 @Override
449 public AnalysisPhase getAnalysisPhase() {
450 return ANALYSIS_PHASE;
451 }
452
453
454
455
456
457
458
459 @Override
460 protected String getAnalyzerEnabledSettingKey() {
461 return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
462 }
463
464
465
466
467
468
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
499
500
501
502
503
504
505
506
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
527
528
529
530
531 File getGrokAssemblyPath() {
532 return grokAssembly;
533 }
534 }