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 java.io.File;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.net.MalformedURLException;
24 import java.net.URL;
25 import java.nio.file.Files;
26 import java.nio.file.Path;
27 import java.nio.file.StandardCopyOption;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Set;
31 import java.util.regex.Pattern;
32 import javax.annotation.concurrent.ThreadSafe;
33
34 import org.jspecify.annotations.NonNull;
35 import org.owasp.dependencycheck.Engine;
36 import org.owasp.dependencycheck.analyzer.exception.AnalysisException;
37 import org.owasp.dependencycheck.data.update.HostedSuppressionsDataSource;
38 import org.owasp.dependencycheck.dependency.Dependency;
39 import org.owasp.dependencycheck.exception.InitializationException;
40 import org.owasp.dependencycheck.exception.WriteLockException;
41 import org.owasp.dependencycheck.utils.WriteLock;
42 import org.owasp.dependencycheck.xml.suppression.SuppressionParseException;
43 import org.owasp.dependencycheck.xml.suppression.SuppressionParser;
44 import org.owasp.dependencycheck.xml.suppression.SuppressionRule;
45 import org.owasp.dependencycheck.utils.DownloadFailedException;
46 import org.owasp.dependencycheck.utils.Downloader;
47 import org.owasp.dependencycheck.utils.FileUtils;
48 import org.owasp.dependencycheck.utils.ResourceNotFoundException;
49 import org.owasp.dependencycheck.utils.Settings;
50 import org.owasp.dependencycheck.utils.TooManyRequestsException;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53 import org.xml.sax.SAXException;
54
55
56
57
58
59
60
61 @ThreadSafe
62 public abstract class AbstractSuppressionAnalyzer extends AbstractAnalyzer {
63
64
65
66
67 private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSuppressionAnalyzer.class);
68
69
70
71 private static final String BASE_SUPPRESSION_FILE = "dependencycheck-base-suppression.xml";
72
73
74
75 private static final String HOSTED_SUPPRESSION_SNAPSHOT_FILE = "dependencycheck-hosted-suppression-snapshot.xml";
76
77
78
79 public static final String SUPPRESSION_OBJECT_KEY = "suppression.rules";
80
81
82
83
84
85
86 @SuppressWarnings("SameReturnValue")
87 public Set<String> getSupportedExtensions() {
88 return null;
89 }
90
91
92
93
94
95
96
97 @Override
98 public synchronized void prepareAnalyzer(Engine engine) throws InitializationException {
99 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
100 return;
101 }
102 try {
103 loadSuppressionBaseData(engine);
104 } catch (SuppressionParseException ex) {
105 throw new InitializationException("Error initializing the suppression analyzer: " + ex.getLocalizedMessage(), ex, true);
106 }
107
108 try {
109 loadSuppressionData(engine);
110 } catch (SuppressionParseException ex) {
111 throw new InitializationException("Warn initializing the suppression analyzer: " + ex.getLocalizedMessage(), ex, false);
112 }
113 }
114
115 @Override
116 protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
117 if (engine == null) {
118 return;
119 }
120 @SuppressWarnings("unchecked")
121 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
122 if (rules.isEmpty()) {
123 return;
124 }
125 for (SuppressionRule rule : rules) {
126 if (filter(rule)) {
127 rule.process(dependency);
128 }
129 }
130 }
131
132
133
134
135
136
137
138
139
140 abstract boolean filter(SuppressionRule rule);
141
142
143
144
145
146
147
148 private void loadSuppressionData(Engine engine) throws SuppressionParseException {
149 final List<SuppressionRule> ruleList = new ArrayList<>();
150 final SuppressionParser parser = new SuppressionParser();
151 final String[] suppressionFilePaths = getSettings().getArray(Settings.KEYS.SUPPRESSION_FILE);
152 final List<String> failedLoadingFiles = new ArrayList<>();
153 if (suppressionFilePaths != null && suppressionFilePaths.length > 0) {
154
155 for (final String suppressionFilePath : suppressionFilePaths) {
156 try {
157 ruleList.addAll(loadSuppressionFile(parser, suppressionFilePath));
158 } catch (SuppressionParseException ex) {
159 final String msg = String.format("Failed to load %s, caused by %s. ", suppressionFilePath, ex.getMessage());
160 failedLoadingFiles.add(msg);
161 }
162 }
163 }
164
165 LOGGER.debug("{} suppression rules were loaded.", ruleList.size());
166 if (!ruleList.isEmpty()) {
167 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
168 @SuppressWarnings("unchecked")
169 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
170 rules.addAll(ruleList);
171 } else {
172 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
173 }
174 }
175 if (!failedLoadingFiles.isEmpty()) {
176 LOGGER.debug("{} suppression files failed to load.", failedLoadingFiles.size());
177 final StringBuilder sb = new StringBuilder();
178 failedLoadingFiles.forEach(sb::append);
179 throw new SuppressionParseException(sb.toString());
180 }
181 }
182
183
184
185
186
187
188
189 private void loadSuppressionBaseData(final Engine engine) throws SuppressionParseException {
190 final SuppressionParser parser = new SuppressionParser();
191 loadPackagedSuppressionBaseData(parser, engine);
192 loadHostedSuppressionBaseData(parser, engine);
193 }
194
195
196
197
198
199
200
201
202 private void loadPackagedSuppressionBaseData(final SuppressionParser parser, final Engine engine) throws SuppressionParseException {
203 List<SuppressionRule> ruleList = null;
204 URL baseSuppressionURL = getPackagedFile(BASE_SUPPRESSION_FILE);
205 try (InputStream in = baseSuppressionURL.openStream()) {
206 ruleList = parser.parseSuppressionRules(in);
207 } catch (SAXException | IOException ex) {
208 throw new SuppressionParseException("Unable to parse the base suppression data file", ex);
209 }
210 if (ruleList != null && !ruleList.isEmpty()) {
211 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
212 @SuppressWarnings("unchecked")
213 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
214 rules.addAll(ruleList);
215 } else {
216 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
217 }
218 }
219 }
220
221 private static @NonNull URL getPackagedFile(String packagedFileName) throws SuppressionParseException {
222 final URL jarLocation = AbstractSuppressionAnalyzer.class.getProtectionDomain().getCodeSource().getLocation();
223 String suppressionFileLocation = jarLocation.getFile();
224 if (suppressionFileLocation.endsWith(".jar")) {
225 suppressionFileLocation = "jar:file:" + suppressionFileLocation + "!/" + packagedFileName;
226 } else if (suppressionFileLocation.startsWith("nested:") && suppressionFileLocation.endsWith(".jar!/")) {
227
228
229 suppressionFileLocation = "jar:" + suppressionFileLocation + packagedFileName;
230 } else {
231 suppressionFileLocation = "file:" + suppressionFileLocation + packagedFileName;
232 }
233 URL baseSuppressionURL = null;
234 try {
235 baseSuppressionURL = new URL(suppressionFileLocation);
236 } catch (MalformedURLException e) {
237 throw new SuppressionParseException("Unable to load the packaged file: " + packagedFileName, e);
238 }
239 return baseSuppressionURL;
240 }
241
242
243
244
245
246
247
248
249
250
251
252
253
254 private void loadHostedSuppressionBaseData(final SuppressionParser parser, final Engine engine) {
255 final boolean enabled = getSettings().getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, true);
256 if (!enabled) {
257 return;
258 }
259
260 try {
261 final String configuredUrl = getSettings().getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL,
262 HostedSuppressionsDataSource.DEFAULT_SUPPRESSIONS_URL);
263 final URL url = new URL(configuredUrl);
264 final String fileName = new File(url.getPath()).getName();
265 if (fileName.isBlank()) {
266 throw new IOException("Hosted Suppression URL must imply a filename");
267 }
268 final File repoFile = new File(getSettings().getDataDirectory(), fileName);
269 boolean repoEmpty = !repoFile.isFile() || repoFile.length() <= 1L;
270 if (repoEmpty) {
271
272 URL hostedSuppressionSnapshotURL = getPackagedFile(HOSTED_SUPPRESSION_SNAPSHOT_FILE);
273 try (InputStream in = hostedSuppressionSnapshotURL.openStream()) {
274 Files.copy(in, repoFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
275 repoEmpty = false;
276 LOGGER.debug("Copied hosted suppression snapshot file to {}", repoFile.toPath());
277 } catch (IOException ex) {
278 LOGGER.warn("Unable to copy the hosted suppression snapshot file to {}, results may contain false positives "
279 + "already resolved by the DependencyCheck project", repoFile.toPath(), ex);
280 }
281 }
282 if (!repoEmpty) {
283 loadCachedHostedSuppressionsRules(parser, repoFile, engine);
284 } else {
285 LOGGER.warn("Empty Hosted Suppression file after update, results may contain false positives "
286 + "already resolved by the DependencyCheck project due to failed download of the hosted suppression file");
287 }
288 } catch (IOException | InitializationException ex) {
289 LOGGER.warn("Unable to load hosted suppressions", ex);
290 }
291 }
292
293
294
295
296
297
298
299
300
301
302
303 private void loadCachedHostedSuppressionsRules(final SuppressionParser parser, final File repoFile, final Engine engine)
304 throws InitializationException {
305
306 final Path defensiveCopy;
307 try (WriteLock lock = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
308 defensiveCopy = Files.createTempFile("dc-basesuppressions", ".xml");
309 LOGGER.debug("copying hosted suppressions file {} to {}", repoFile.toPath(), defensiveCopy);
310 Files.copy(repoFile.toPath(), defensiveCopy, StandardCopyOption.REPLACE_EXISTING);
311 } catch (WriteLockException | IOException ex) {
312 throw new InitializationException("Failed to copy the hosted suppressions file", ex);
313 }
314
315 try (InputStream in = Files.newInputStream(defensiveCopy)) {
316 final List<SuppressionRule> ruleList;
317 ruleList = parser.parseSuppressionRules(in);
318 if (!ruleList.isEmpty()) {
319 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
320 @SuppressWarnings("unchecked")
321 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
322 rules.addAll(ruleList);
323 } else {
324 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
325 }
326 }
327 } catch (SAXException | IOException ex) {
328 LOGGER.warn("Unable to parse the hosted suppressions data file, results may contain false positives already resolved "
329 + "by the DependencyCheck project", ex);
330 }
331 try {
332 Files.delete(defensiveCopy);
333 } catch (IOException ex) {
334 LOGGER.warn("Could not delete defensive copy of hosted suppressions file {}", defensiveCopy, ex);
335 }
336 }
337
338
339
340
341
342
343
344
345
346
347
348 private List<SuppressionRule> loadSuppressionFile(final SuppressionParser parser,
349 final String suppressionFilePath) throws SuppressionParseException {
350 LOGGER.debug("Loading suppression rules from '{}'", suppressionFilePath);
351 final List<SuppressionRule> list = new ArrayList<>();
352 File file = null;
353 boolean deleteTempFile = false;
354 try {
355 final Pattern uriRx = Pattern.compile("^(https?|file):.*", Pattern.CASE_INSENSITIVE);
356 if (uriRx.matcher(suppressionFilePath).matches()) {
357 deleteTempFile = true;
358 file = getSettings().getTempFile("suppression", "xml");
359 final URL url = new URL(suppressionFilePath);
360 try {
361 Downloader.getInstance().fetchFile(url, file, false, Settings.KEYS.SUPPRESSION_FILE_USER,
362 Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
363 } catch (DownloadFailedException ex) {
364 LOGGER.trace("Failed download suppression file - first attempt", ex);
365 try {
366 Thread.sleep(500);
367 Downloader.getInstance().fetchFile(url, file, true, Settings.KEYS.SUPPRESSION_FILE_USER,
368 Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
369 } catch (TooManyRequestsException ex1) {
370 throw new SuppressionParseException("Unable to download supression file `" + file
371 + "`; received 429 - too many requests", ex1);
372 } catch (ResourceNotFoundException ex1) {
373 throw new SuppressionParseException("Unable to download supression file `" + file
374 + "`; received 404 - resource not found", ex1);
375 } catch (InterruptedException ex1) {
376 Thread.currentThread().interrupt();
377 throw new SuppressionParseException("Unable to download supression file `" + file + "`", ex1);
378 }
379 } catch (TooManyRequestsException ex) {
380 throw new SuppressionParseException("Unable to download supression file `" + file
381 + "`; received 429 - too many requests", ex);
382 } catch (ResourceNotFoundException ex) {
383 throw new SuppressionParseException("Unable to download supression file `" + file + "`; received 404 - resource not found", ex);
384 }
385 } else {
386 file = new File(suppressionFilePath);
387
388 if (!file.exists()) {
389 try (InputStream suppressionFromClasspath = FileUtils.getResourceAsStream(suppressionFilePath)) {
390 deleteTempFile = true;
391 file = getSettings().getTempFile("suppression", "xml");
392 try {
393 Files.copy(suppressionFromClasspath, file.toPath());
394 } catch (IOException ex) {
395 throwSuppressionParseException("Unable to locate suppression file in classpath", ex, suppressionFilePath);
396 }
397 }
398 }
399 }
400 if (!file.exists()) {
401 final String msg = String.format("Suppression file '%s' does not exist", file.getPath());
402 LOGGER.warn(msg);
403 throw new SuppressionParseException(msg);
404 }
405 try {
406 list.addAll(parser.parseSuppressionRules(file));
407 } catch (SuppressionParseException ex) {
408 LOGGER.warn("Unable to parse suppression xml file '{}'", file.getPath());
409 LOGGER.warn(ex.getMessage());
410 throw ex;
411 }
412 } catch (DownloadFailedException ex) {
413 throwSuppressionParseException("Unable to fetch the configured suppression file", ex, suppressionFilePath);
414 } catch (MalformedURLException ex) {
415 throwSuppressionParseException("Configured suppression file has an invalid URL", ex, suppressionFilePath);
416 } catch (SuppressionParseException ex) {
417 throw ex;
418 } catch (IOException ex) {
419 throwSuppressionParseException("Unable to read suppression file", ex, suppressionFilePath);
420 } finally {
421 if (deleteTempFile && file != null) {
422 FileUtils.delete(file);
423 }
424 }
425 return list;
426 }
427
428
429
430
431
432
433
434
435
436
437 private void throwSuppressionParseException(String message, Exception exception, String suppressionFilePath) throws SuppressionParseException {
438 LOGGER.warn(String.format(message + " '%s'", suppressionFilePath));
439 LOGGER.debug("", exception);
440 throw new SuppressionParseException(message, exception);
441 }
442
443
444
445
446
447
448
449 public static int getRuleCount(Engine engine) {
450 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
451 @SuppressWarnings("unchecked")
452 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
453 return rules.size();
454 }
455 return 0;
456 }
457 }