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 {
204 suppressionFileLocation = "file:" + suppressionFileLocation + BASE_SUPPRESSION_FILE;
205 }
206 URL baseSuppresssionURL = null;
207 try {
208 baseSuppresssionURL = new URL(suppressionFileLocation);
209 } catch (MalformedURLException e) {
210 throw new SuppressionParseException("Unable to load the base suppression data file", e);
211 }
212 try (InputStream in = baseSuppresssionURL.openStream()) {
213 ruleList = parser.parseSuppressionRules(in);
214 } catch (SAXException | IOException ex) {
215 throw new SuppressionParseException("Unable to parse the base suppression data file", ex);
216 }
217 if (ruleList != null && !ruleList.isEmpty()) {
218 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
219 @SuppressWarnings("unchecked")
220 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
221 rules.addAll(ruleList);
222 } else {
223 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
224 }
225 }
226 }
227
228
229
230
231
232
233
234
235
236
237
238
239
240 private void loadHostedSuppressionBaseData(final SuppressionParser parser, final Engine engine) {
241 final File repoFile;
242 boolean repoEmpty = false;
243 final boolean enabled = getSettings().getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, true);
244 if (!enabled) {
245 return;
246 }
247 final boolean autoupdate = getSettings().getBoolean(Settings.KEYS.AUTO_UPDATE, true);
248 final boolean forceupdate = getSettings().getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, false);
249
250 try {
251 final String configuredUrl = getSettings().getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL,
252 HostedSuppressionsDataSource.DEFAULT_SUPPRESSIONS_URL);
253 final URL url = new URL(configuredUrl);
254 final String fileName = new File(url.getPath()).getName();
255 repoFile = new File(getSettings().getDataDirectory(), fileName);
256 if (!repoFile.isFile() || repoFile.length() <= 1L) {
257 repoEmpty = true;
258 LOGGER.warn("Hosted Suppressions file is empty or missing - attempting to force the update");
259 getSettings().setBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, true);
260 }
261 if ((!autoupdate && forceupdate) || (autoupdate && repoEmpty)) {
262 if (engine == null) {
263 LOGGER.warn("Engine was null, this should only happen in tests - skipping forced update");
264 } else {
265 repoEmpty = forceUpdateHostedSuppressions(engine, repoFile);
266 }
267 }
268 if (!repoEmpty) {
269 loadCachedHostedSuppressionsRules(parser, repoFile, engine);
270 } else {
271 LOGGER.warn("Empty Hosted Suppression file after update, results may contain false positives "
272 + "already resolved by the DependencyCheck project due to failed download of the hosted suppression file");
273 }
274 } catch (IOException | InitializationException ex) {
275 LOGGER.warn("Unable to load hosted suppressions", ex);
276 }
277 }
278
279
280
281
282
283
284
285
286
287
288
289 private void loadCachedHostedSuppressionsRules(final SuppressionParser parser, final File repoFile, final Engine engine)
290 throws InitializationException {
291
292 final Path defensiveCopy;
293 try (WriteLock lock = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
294 defensiveCopy = Files.createTempFile("dc-basesuppressions", ".xml");
295 LOGGER.debug("copying hosted suppressions file {} to {}", repoFile.toPath(), defensiveCopy);
296 Files.copy(repoFile.toPath(), defensiveCopy, StandardCopyOption.REPLACE_EXISTING);
297 } catch (WriteLockException | IOException ex) {
298 throw new InitializationException("Failed to copy the hosted suppressions file", ex);
299 }
300
301 try (InputStream in = Files.newInputStream(defensiveCopy)) {
302 final List<SuppressionRule> ruleList;
303 ruleList = parser.parseSuppressionRules(in);
304 if (!ruleList.isEmpty()) {
305 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
306 @SuppressWarnings("unchecked")
307 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
308 rules.addAll(ruleList);
309 } else {
310 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
311 }
312 }
313 } catch (SAXException | IOException ex) {
314 LOGGER.warn("Unable to parse the hosted suppressions data file, results may contain false positives already resolved "
315 + "by the DependencyCheck project", ex);
316 }
317 try {
318 Files.delete(defensiveCopy);
319 } catch (IOException ex) {
320 LOGGER.warn("Could not delete defensive copy of hosted suppressions file {}", defensiveCopy, ex);
321 }
322 }
323
324 private static boolean forceUpdateHostedSuppressions(final Engine engine, final File repoFile) {
325 final HostedSuppressionsDataSource ds = new HostedSuppressionsDataSource();
326 boolean repoEmpty = true;
327 try {
328 ds.update(engine);
329 repoEmpty = !repoFile.isFile() || repoFile.length() <= 1L;
330 } catch (UpdateException ex) {
331 LOGGER.warn("Failed to update the Hosted Suppression file", ex);
332 }
333 return repoEmpty;
334 }
335
336
337
338
339
340
341
342
343
344
345
346 private List<SuppressionRule> loadSuppressionFile(final SuppressionParser parser,
347 final String suppressionFilePath) throws SuppressionParseException {
348 LOGGER.debug("Loading suppression rules from '{}'", suppressionFilePath);
349 final List<SuppressionRule> list = new ArrayList<>();
350 File file = null;
351 boolean deleteTempFile = false;
352 try {
353 final Pattern uriRx = Pattern.compile("^(https?|file):.*", Pattern.CASE_INSENSITIVE);
354 if (uriRx.matcher(suppressionFilePath).matches()) {
355 deleteTempFile = true;
356 file = getSettings().getTempFile("suppression", "xml");
357 final URL url = new URL(suppressionFilePath);
358 try {
359 Downloader.getInstance().fetchFile(url, file, false, Settings.KEYS.SUPPRESSION_FILE_USER,
360 Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
361 } catch (DownloadFailedException ex) {
362 LOGGER.trace("Failed download suppression file - first attempt", ex);
363 try {
364 Thread.sleep(500);
365 Downloader.getInstance().fetchFile(url, file, true, Settings.KEYS.SUPPRESSION_FILE_USER,
366 Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
367 } catch (TooManyRequestsException ex1) {
368 throw new SuppressionParseException("Unable to download supression file `" + file
369 + "`; received 429 - too many requests", ex1);
370 } catch (ResourceNotFoundException ex1) {
371 throw new SuppressionParseException("Unable to download supression file `" + file
372 + "`; received 404 - resource not found", ex1);
373 } catch (InterruptedException ex1) {
374 Thread.currentThread().interrupt();
375 throw new SuppressionParseException("Unable to download supression file `" + file + "`", ex1);
376 }
377 } catch (TooManyRequestsException ex) {
378 throw new SuppressionParseException("Unable to download supression file `" + file
379 + "`; received 429 - too many requests", ex);
380 } catch (ResourceNotFoundException ex) {
381 throw new SuppressionParseException("Unable to download supression file `" + file + "`; received 404 - resource not found", ex);
382 }
383 } else {
384 file = new File(suppressionFilePath);
385
386 if (!file.exists()) {
387 try (InputStream suppressionFromClasspath = FileUtils.getResourceAsStream(suppressionFilePath)) {
388 deleteTempFile = true;
389 file = getSettings().getTempFile("suppression", "xml");
390 try {
391 Files.copy(suppressionFromClasspath, file.toPath());
392 } catch (IOException ex) {
393 throwSuppressionParseException("Unable to locate suppression file in classpath", ex, suppressionFilePath);
394 }
395 }
396 }
397 }
398 if (file != null) {
399 if (!file.exists()) {
400 final String msg = String.format("Suppression file '%s' does not exist", file.getPath());
401 LOGGER.warn(msg);
402 throw new SuppressionParseException(msg);
403 }
404 try {
405 list.addAll(parser.parseSuppressionRules(file));
406 } catch (SuppressionParseException ex) {
407 LOGGER.warn("Unable to parse suppression xml file '{}'", file.getPath());
408 LOGGER.warn(ex.getMessage());
409 throw ex;
410 }
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 }