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