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