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