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