-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
AssemblyAnalyzer.java
532 lines (502 loc) · 23.6 KB
/
AssemblyAnalyzer.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
/*
* This file is part of dependency-check-core.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2012 Jeremy Long. All Rights Reserved.
*/
package org.owasp.dependencycheck.analyzer;
import com.github.packageurl.MalformedPackageURLException;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.io.InputStream;
import org.owasp.dependencycheck.Engine;
import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
import org.owasp.dependencycheck.dependency.Confidence;
import org.owasp.dependencycheck.dependency.Dependency;
import org.owasp.dependencycheck.utils.FileFilterBuilder;
import org.owasp.dependencycheck.utils.FileUtils;
import org.owasp.dependencycheck.utils.Settings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.concurrent.ThreadSafe;
import org.apache.commons.lang3.StringUtils;
import org.owasp.dependencycheck.data.nvd.ecosystem.Ecosystem;
import org.owasp.dependencycheck.exception.InitializationException;
import org.owasp.dependencycheck.dependency.EvidenceType;
import org.owasp.dependencycheck.dependency.naming.GenericIdentifier;
import org.owasp.dependencycheck.dependency.naming.PurlIdentifier;
import org.owasp.dependencycheck.processing.GrokAssemblyProcessor;
import org.owasp.dependencycheck.utils.DependencyVersion;
import org.owasp.dependencycheck.utils.DependencyVersionUtil;
import org.owasp.dependencycheck.utils.ExtractionException;
import org.owasp.dependencycheck.utils.ExtractionUtil;
import org.owasp.dependencycheck.utils.processing.ProcessReader;
import org.owasp.dependencycheck.xml.assembly.AssemblyData;
import org.owasp.dependencycheck.xml.assembly.GrokParseException;
/**
* Analyzer for getting company, product, and version information from a .NET
* assembly.
*
* @author colezlaw
*/
@ThreadSafe
public class AssemblyAnalyzer extends AbstractFileTypeAnalyzer {
/**
* Logger
*/
private static final Logger LOGGER = LoggerFactory.getLogger(AssemblyAnalyzer.class);
/**
* The analyzer name
*/
private static final String ANALYZER_NAME = "Assembly Analyzer";
/**
* The analysis phase
*/
private static final AnalysisPhase ANALYSIS_PHASE = AnalysisPhase.INFORMATION_COLLECTION;
/**
* A descriptor for the type of dependencies processed or added by this
* analyzer.
*/
public static final String DEPENDENCY_ECOSYSTEM = Ecosystem.DOTNET;
/**
* The list of supported extensions
*/
private static final String[] SUPPORTED_EXTENSIONS = {"dll", "exe"};
/**
* The File Filter used to filter supported extensions.
*/
private static final FileFilter FILTER = FileFilterBuilder.newInstance().addExtensions(
SUPPORTED_EXTENSIONS).build();
/**
* The file path to `GrokAssembly.dll`.
*/
private File grokAssembly = null;
/**
* The base argument list to call GrokAssembly.
*/
private List<String> baseArgumentList = null;
/**
* Builds the beginnings of a List for ProcessBuilder
*
* @return the list of arguments to begin populating the ProcessBuilder
*/
protected List<String> buildArgumentList() {
// Use file.separator as a wild guess as to whether this is Windows
final List<String> args = new ArrayList<>();
if (!StringUtils.isBlank(getSettings().getString(Settings.KEYS.ANALYZER_ASSEMBLY_DOTNET_PATH))) {
args.add(getSettings().getString(Settings.KEYS.ANALYZER_ASSEMBLY_DOTNET_PATH));
} else if (isDotnetPath()) {
args.add("dotnet");
} else {
return null;
}
args.add(grokAssembly.getPath());
return args;
}
/**
* Performs the analysis on a single Dependency.
*
* @param dependency the dependency to analyze
* @param engine the engine to perform the analysis under
* @throws AnalysisException if anything goes sideways
*/
@Override
public void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
final File test = new File(dependency.getActualFilePath());
if (!test.isFile()) {
throw new AnalysisException(String.format("%s does not exist and cannot be analyzed by dependency-check",
dependency.getActualFilePath()));
}
if (grokAssembly == null) {
LOGGER.warn("GrokAssembly didn't get deployed");
return;
}
if (baseArgumentList == null) {
LOGGER.warn("Assembly Analyzer was unable to execute");
return;
}
final AssemblyData data;
final List<String> args = new ArrayList<>(baseArgumentList);
args.add(dependency.getActualFilePath());
final ProcessBuilder pb = new ProcessBuilder(args);
try {
final Process proc = pb.start();
try (GrokAssemblyProcessor processor = new GrokAssemblyProcessor();
ProcessReader processReader = new ProcessReader(proc, processor)) {
processReader.readAll();
final String errorOutput = processReader.getError();
if (!StringUtils.isBlank(errorOutput)) {
LOGGER.warn("Error from GrokAssembly: {}", errorOutput);
}
final int exitValue = proc.exitValue();
if (exitValue == 3) {
LOGGER.debug("{} is not a .NET assembly or executable and as such cannot be analyzed by dependency-check",
dependency.getActualFilePath());
return;
} else if (exitValue != 0) {
LOGGER.debug("Return code {} from GrokAssembly; dependency-check is unable to analyze the library: {}",
exitValue, dependency.getActualFilePath());
return;
}
data = processor.getAssemblyData();
}
// First, see if there was an error
final String error = data.getError();
if (error != null && !error.isEmpty()) {
throw new AnalysisException(error);
}
if (data.getWarning() != null) {
LOGGER.debug("Grok Assembly - could not get namespace on dependency `{}` - {}", dependency.getActualFilePath(), data.getWarning());
}
updateDependency(data, dependency);
} catch (GrokParseException saxe) {
LOGGER.error("----------------------------------------------------");
LOGGER.error("Failed to read the Assembly Analyzer results.");
LOGGER.error("----------------------------------------------------");
throw new AnalysisException("Couldn't parse Assembly Analyzer results (GrokAssembly)", saxe);
} catch (IOException ioe) {
throw new AnalysisException(ioe);
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new AnalysisException("GrokAssembly process interrupted", ex);
}
}
/**
* Updates the dependency information with the provided assembly data.
*
* @param data the assembly data
* @param dependency the dependency to update
*/
private void updateDependency(final AssemblyData data, Dependency dependency) {
final StringBuilder sb = new StringBuilder();
if (!StringUtils.isBlank(data.getFileDescription())) {
sb.append(data.getFileDescription());
}
if (!StringUtils.isBlank(data.getComments())) {
if (sb.length() > 0) {
sb.append("\n\n");
}
sb.append(data.getComments());
}
if (!StringUtils.isBlank(data.getLegalCopyright())) {
if (sb.length() > 0) {
sb.append("\n\n");
}
sb.append(data.getLegalCopyright());
}
if (!StringUtils.isBlank(data.getLegalTrademarks())) {
if (sb.length() > 0) {
sb.append("\n");
}
sb.append(data.getLegalTrademarks());
}
final String description = sb.toString();
if (description.length() > 0) {
dependency.setDescription(description);
addMatchingValues(data.getNamespaces(), description, dependency, EvidenceType.VENDOR);
addMatchingValues(data.getNamespaces(), description, dependency, EvidenceType.PRODUCT);
}
if (!StringUtils.isBlank(data.getProductVersion())) {
dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "ProductVersion", data.getProductVersion(), Confidence.HIGHEST);
}
if (!StringUtils.isBlank(data.getFileVersion())) {
dependency.addEvidence(EvidenceType.VERSION, "grokassembly", "FileVersion", data.getFileVersion(), Confidence.HIGH);
}
if (data.getFileVersion() != null && data.getProductVersion() != null) {
final int max = Math.min(data.getFileVersion().length(), data.getProductVersion().length());
int pos;
for (pos = 0; pos < max; pos++) {
if (data.getFileVersion().charAt(pos) != data.getProductVersion().charAt(pos)) {
break;
}
}
final DependencyVersion fileVersion = DependencyVersionUtil.parseVersion(data.getFileVersion(), true);
final DependencyVersion productVersion = DependencyVersionUtil.parseVersion(data.getProductVersion(), true);
if (pos > 0) {
final DependencyVersion matchingVersion = DependencyVersionUtil.parseVersion(data.getFileVersion().substring(0, pos), true);
if (fileVersion != null && data.getFileVersion() != null
&& fileVersion.toString().length() == data.getFileVersion().length()) {
if (matchingVersion != null && matchingVersion.getVersionParts().size() > 2) {
dependency.addEvidence(EvidenceType.VERSION, "AssemblyAnalyzer", "FilteredVersion",
matchingVersion.toString(), Confidence.HIGHEST);
dependency.setVersion(matchingVersion.toString());
}
}
}
if (dependency.getVersion() == null) {
if (data.getFileVersion() != null && data.getProductVersion() != null
&& data.getFileVersion().length() >= data.getProductVersion().length()) {
if (fileVersion != null && fileVersion.toString().length() == data.getFileVersion().length()) {
dependency.setVersion(fileVersion.toString());
} else if (productVersion != null && productVersion.toString().length() == data.getProductVersion().length()) {
dependency.setVersion(productVersion.toString());
}
} else {
if (productVersion != null && productVersion.toString().length() == data.getProductVersion().length()) {
dependency.setVersion(productVersion.toString());
} else if (fileVersion != null && fileVersion.toString().length() == data.getFileVersion().length()) {
dependency.setVersion(fileVersion.toString());
}
}
}
}
if (dependency.getVersion() == null && data.getFileVersion() != null) {
final DependencyVersion version = DependencyVersionUtil.parseVersion(data.getFileVersion(), true);
if (version != null) {
dependency.setVersion(version.toString());
}
}
if (dependency.getVersion() == null && data.getProductVersion() != null) {
final DependencyVersion version = DependencyVersionUtil.parseVersion(data.getProductVersion(), true);
if (version != null) {
dependency.setVersion(version.toString());
}
}
if (!StringUtils.isBlank(data.getCompanyName())) {
dependency.addEvidence(EvidenceType.VENDOR, "grokassembly", "CompanyName", data.getCompanyName(), Confidence.HIGHEST);
addMatchingValues(data.getNamespaces(), data.getCompanyName(), dependency, EvidenceType.VENDOR);
}
if (!StringUtils.isBlank(data.getProductName())) {
dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "ProductName", data.getProductName(), Confidence.HIGHEST);
addMatchingValues(data.getNamespaces(), data.getProductName(), dependency, EvidenceType.PRODUCT);
}
if (!StringUtils.isBlank(data.getFileDescription())) {
dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "FileDescription", data.getFileDescription(), Confidence.HIGH);
addMatchingValues(data.getNamespaces(), data.getFileDescription(), dependency, EvidenceType.PRODUCT);
}
final String internalName = data.getInternalName();
if (!StringUtils.isBlank(internalName)) {
dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "InternalName", internalName, Confidence.MEDIUM);
addMatchingValues(data.getNamespaces(), internalName, dependency, EvidenceType.PRODUCT);
addMatchingValues(data.getNamespaces(), internalName, dependency, EvidenceType.VENDOR);
if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), internalName)) {
final String ext = FileUtils.getFileExtension(internalName);
if (ext != null) {
dependency.setName(internalName.substring(0, internalName.length() - ext.length() - 1));
} else {
dependency.setName(internalName);
}
}
}
final String originalFilename = data.getOriginalFilename();
if (!StringUtils.isBlank(originalFilename)) {
dependency.addEvidence(EvidenceType.PRODUCT, "grokassembly", "OriginalFilename", originalFilename, Confidence.MEDIUM);
addMatchingValues(data.getNamespaces(), originalFilename, dependency, EvidenceType.PRODUCT);
if (dependency.getName() == null && StringUtils.containsIgnoreCase(dependency.getActualFile().getName(), originalFilename)) {
final String ext = FileUtils.getFileExtension(originalFilename);
if (ext != null) {
dependency.setName(originalFilename.substring(0, originalFilename.length() - ext.length() - 1));
} else {
dependency.setName(originalFilename);
}
}
}
if (dependency.getName() != null && dependency.getVersion() != null) {
try {
dependency.addSoftwareIdentifier(new PurlIdentifier("generic", dependency.getName(), dependency.getVersion(), Confidence.MEDIUM));
} catch (MalformedPackageURLException ex) {
LOGGER.debug("Unable to create Package URL Identifier for " + dependency.getName(), ex);
dependency.addSoftwareIdentifier(new GenericIdentifier(
String.format("%s@%s", dependency.getName(), dependency.getVersion()),
Confidence.MEDIUM));
}
}
dependency.setEcosystem(DEPENDENCY_ECOSYSTEM);
}
/**
* Initialize the analyzer. In this case, extract GrokAssembly.dll to a
* temporary location.
*
* @param engine a reference to the dependency-check engine
* @throws InitializationException thrown if anything goes wrong
*/
@Override
public void prepareFileTypeAnalyzer(Engine engine) throws InitializationException {
grokAssembly = extractGrokAssembly();
baseArgumentList = buildArgumentList();
if (baseArgumentList == null) {
setEnabled(false);
LOGGER.error("----------------------------------------------------");
LOGGER.error(".NET Assembly Analyzer could not be initialized and at least one "
+ "'exe' or 'dll' was scanned. The 'dotnet' executable could not be found on "
+ "the path; either disable the Assembly Analyzer or add the path to dotnet "
+ "core in the configuration.");
LOGGER.error("----------------------------------------------------");
return;
}
try {
final ProcessBuilder pb = new ProcessBuilder(baseArgumentList);
final Process p = pb.start();
try (ProcessReader processReader = new ProcessReader(p)) {
processReader.readAll();
final String error = processReader.getError();
if (p.exitValue() != 1 || !StringUtils.isBlank(error)) {
LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer, please see the log for more details.");
LOGGER.debug("GrokAssembly.dll is not working properly");
grokAssembly = null;
setEnabled(false);
throw new InitializationException("Could not execute .NET AssemblyAnalyzer");
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
+ "dependency-check requires dotnet 6.0 core to be installed to analyze assemblies;\n"
+ "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
setEnabled(false);
throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer", e);
} catch (IOException e) {
LOGGER.warn("An error occurred with the .NET AssemblyAnalyzer;\n"
+ "dependency-check requires dotnet 6.0 core to be installed to analyze assemblies;\n"
+ "this can be ignored unless you are scanning .NET DLLs. Please see the log for more details.");
LOGGER.debug("Could not execute GrokAssembly {}", e.getMessage());
setEnabled(false);
throw new InitializationException("An error occurred with the .NET AssemblyAnalyzer", e);
}
}
/**
* Extracts the GrokAssembly executable.
*
* @return the path to the extracted executable
* @throws InitializationException thrown if the executable could not be
* extracted
*/
private File extractGrokAssembly() throws InitializationException {
final File location;
try (InputStream in = FileUtils.getResourceAsStream("GrokAssembly.zip")) {
if (in == null) {
throw new InitializationException("Unable to extract GrokAssembly.dll - file not found");
}
location = FileUtils.createTempDirectory(getSettings().getTempDirectory());
ExtractionUtil.extractFiles(in, location);
} catch (ExtractionException ex) {
throw new InitializationException("Unable to extract GrokAssembly.dll", ex);
} catch (IOException ex) {
throw new InitializationException("Unable to create temp directory for GrokAssembly", ex);
}
return new File(location, "GrokAssembly.dll");
}
/**
* Removes resources used from the local file system.
*
* @throws Exception thrown if there is a problem closing the analyzer
*/
@Override
public void closeAnalyzer() throws Exception {
FileUtils.delete(grokAssembly.getParentFile());
}
@Override
protected FileFilter getFileFilter() {
return FILTER;
}
/**
* Gets this analyzer's name.
*
* @return the analyzer name
*/
@Override
public String getName() {
return ANALYZER_NAME;
}
/**
* Returns the phase this analyzer runs under.
*
* @return the phase this runs under
*/
@Override
public AnalysisPhase getAnalysisPhase() {
return ANALYSIS_PHASE;
}
/**
* Returns the key used in the properties file to reference the analyzer's
* enabled property.
*
* @return the analyzer's enabled property setting key
*/
@Override
protected String getAnalyzerEnabledSettingKey() {
return Settings.KEYS.ANALYZER_ASSEMBLY_ENABLED;
}
/**
* Tests to see if a file is in the system path.
*
* @return <code>true</code> if dotnet could be found in the path; otherwise
* <code>false</code>
*/
private boolean isDotnetPath() {
final String[] args = new String[2];
args[0] = "dotnet";
args[1] = "--info";
final ProcessBuilder pb = new ProcessBuilder(args);
try {
final Process proc = pb.start();
try (ProcessReader processReader = new ProcessReader(proc)) {
processReader.readAll();
final int exitValue = proc.exitValue();
if (exitValue == 0) {
return true;
}
final String output = processReader.getOutput();
if (output.length() > 0) {
return true;
}
}
} catch (InterruptedException ex) {
Thread.currentThread().interrupt();
LOGGER.debug("Path search failed for dotnet", ex);
} catch (IOException ex) {
LOGGER.debug("Path search failed for dotnet", ex);
}
return false;
}
/**
* Cycles through the collection of class name information to see if parts
* of the package names are contained in the provided value. If found, it
* will be added as the HIGHEST confidence evidence because we have more
* then one source corroborating the value.
*
* @param packages a collection of class name information
* @param value the value to check to see if it contains a package name
* @param dep the dependency to add new entries too
* @param type the type of evidence (vendor, product, or version)
*/
protected static void addMatchingValues(List<String> packages, String value, Dependency dep, EvidenceType type) {
if (value == null || value.isEmpty() || packages == null || packages.isEmpty()) {
return;
}
for (String key : packages) {
final int pos = StringUtils.indexOfIgnoreCase(value, key);
if ((pos == 0 && (key.length() == value.length() || (key.length() < value.length()
&& !Character.isLetterOrDigit(value.charAt(key.length())))))
|| (pos > 0 && !Character.isLetterOrDigit(value.charAt(pos - 1))
&& (pos + key.length() == value.length() || (key.length() < value.length()
&& !Character.isLetterOrDigit(value.charAt(pos + key.length())))))) {
dep.addEvidence(type, "dll", "namespace", key, Confidence.HIGHEST);
}
}
}
/**
* Used in testing only - this simply returns the path to the extracted
* GrokAssembly.dll.
*
* @return the path to the extracted GrokAssembly.dll
*/
File getGrokAssemblyPath() {
return grokAssembly;
}
}