View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2013 Jeremy Long. All Rights Reserved.
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.jspecify.annotations.NonNull;
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.dependency.Dependency;
39  import org.owasp.dependencycheck.exception.InitializationException;
40  import org.owasp.dependencycheck.exception.WriteLockException;
41  import org.owasp.dependencycheck.utils.WriteLock;
42  import org.owasp.dependencycheck.xml.suppression.SuppressionParseException;
43  import org.owasp.dependencycheck.xml.suppression.SuppressionParser;
44  import org.owasp.dependencycheck.xml.suppression.SuppressionRule;
45  import org.owasp.dependencycheck.utils.DownloadFailedException;
46  import org.owasp.dependencycheck.utils.Downloader;
47  import org.owasp.dependencycheck.utils.FileUtils;
48  import org.owasp.dependencycheck.utils.ResourceNotFoundException;
49  import org.owasp.dependencycheck.utils.Settings;
50  import org.owasp.dependencycheck.utils.TooManyRequestsException;
51  import org.slf4j.Logger;
52  import org.slf4j.LoggerFactory;
53  import org.xml.sax.SAXException;
54  
55  /**
56   * Abstract base suppression analyzer that contains methods for parsing the
57   * suppression XML file.
58   *
59   * @author Jeremy Long
60   */
61  @ThreadSafe
62  public abstract class AbstractSuppressionAnalyzer extends AbstractAnalyzer {
63  
64      /**
65       * The Logger for use throughout the class.
66       */
67      private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSuppressionAnalyzer.class);
68      /**
69       * The file name of the base suppression XML file.
70       */
71      private static final String BASE_SUPPRESSION_FILE = "dependencycheck-base-suppression.xml";
72      /**
73       * The file name of the snapshot of the hosted suppression XML file.
74       */
75      private static final String HOSTED_SUPPRESSION_SNAPSHOT_FILE = "dependencycheck-hosted-suppression-snapshot.xml";
76      /**
77       * The key used to store and retrieve the suppression files.
78       */
79      public static final String SUPPRESSION_OBJECT_KEY = "suppression.rules";
80  
81      /**
82       * Returns a list of file EXTENSIONS supported by this analyzer.
83       *
84       * @return a list of file EXTENSIONS supported by this analyzer.
85       */
86      @SuppressWarnings("SameReturnValue")
87      public Set<String> getSupportedExtensions() {
88          return null;
89      }
90  
91      /**
92       * The prepare method loads the suppression XML file.
93       *
94       * @param engine a reference the dependency-check engine
95       * @throws InitializationException thrown if there is an exception
96       */
97      @Override
98      public synchronized void prepareAnalyzer(Engine engine) throws InitializationException {
99          if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
100             return;
101         }
102         try {
103             loadSuppressionBaseData(engine);
104         } catch (SuppressionParseException ex) {
105             throw new InitializationException("Error initializing the suppression analyzer: " + ex.getLocalizedMessage(), ex, true);
106         }
107 
108         try {
109             loadSuppressionData(engine);
110         } catch (SuppressionParseException ex) {
111             throw new InitializationException("Warn initializing the suppression analyzer: " + ex.getLocalizedMessage(), ex, false);
112         }
113     }
114 
115     @Override
116     protected void analyzeDependency(Dependency dependency, Engine engine) throws AnalysisException {
117         if (engine == null) {
118             return;
119         }
120         @SuppressWarnings("unchecked")
121         final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
122         if (rules.isEmpty()) {
123             return;
124         }
125         for (SuppressionRule rule : rules) {
126             if (filter(rule)) {
127                 rule.process(dependency);
128             }
129         }
130     }
131 
132     /**
133      * Determines whether a suppression rule should be retained when filtering a
134      * set of suppression rules for a concrete suppression analyzer.
135      *
136      * @param rule the suppression rule to evaluate
137      * @return <code>true</code> if the rule should be retained; otherwise
138      * <code>false</code>
139      */
140     abstract boolean filter(SuppressionRule rule);
141 
142     /**
143      * Loads all the suppression rules files configured in the {@link Settings}.
144      *
145      * @param engine a reference to the ODC engine.
146      * @throws SuppressionParseException thrown if the XML cannot be parsed.
147      */
148     private void loadSuppressionData(Engine engine) throws SuppressionParseException {
149         final List<SuppressionRule> ruleList = new ArrayList<>();
150         final SuppressionParser parser = new SuppressionParser();
151         final String[] suppressionFilePaths = getSettings().getArray(Settings.KEYS.SUPPRESSION_FILE);
152         final List<String> failedLoadingFiles = new ArrayList<>();
153         if (suppressionFilePaths != null && suppressionFilePaths.length > 0) {
154             // Load all the suppression file paths
155             for (final String suppressionFilePath : suppressionFilePaths) {
156                 try {
157                     ruleList.addAll(loadSuppressionFile(parser, suppressionFilePath));
158                 } catch (SuppressionParseException ex) {
159                     final String msg = String.format("Failed to load %s, caused by %s. ", suppressionFilePath, ex.getMessage());
160                     failedLoadingFiles.add(msg);
161                 }
162             }
163         }
164 
165         LOGGER.debug("{} suppression rules were loaded.", ruleList.size());
166         if (!ruleList.isEmpty()) {
167             if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
168                 @SuppressWarnings("unchecked")
169                 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
170                 rules.addAll(ruleList);
171             } else {
172                 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
173             }
174         }
175         if (!failedLoadingFiles.isEmpty()) {
176             LOGGER.debug("{} suppression files failed to load.", failedLoadingFiles.size());
177             final StringBuilder sb = new StringBuilder();
178             failedLoadingFiles.forEach(sb::append);
179             throw new SuppressionParseException(sb.toString());
180         }
181     }
182 
183     /**
184      * Loads all the base suppression rules files.
185      *
186      * @param engine a reference the dependency-check engine
187      * @throws SuppressionParseException thrown if the XML cannot be parsed.
188      */
189     private void loadSuppressionBaseData(final Engine engine) throws SuppressionParseException {
190         final SuppressionParser parser = new SuppressionParser();
191         loadPackagedSuppressionBaseData(parser, engine);
192         loadHostedSuppressionBaseData(parser, engine);
193     }
194 
195     /**
196      * Loads the suppression rules packaged with the application.
197      *
198      * @param parser The suppression parser to use
199      * @param engine a reference the dependency-check engine
200      * @throws SuppressionParseException thrown if the XML cannot be parsed.
201      */
202     private void loadPackagedSuppressionBaseData(final SuppressionParser parser, final Engine engine) throws SuppressionParseException {
203         List<SuppressionRule> ruleList = null;
204         URL baseSuppressionURL = getPackagedFile(BASE_SUPPRESSION_FILE);
205         try (InputStream in = baseSuppressionURL.openStream()) {
206             ruleList = parser.parseSuppressionRules(in);
207         } catch (SAXException | IOException ex) {
208             throw new SuppressionParseException("Unable to parse the base suppression data file", ex);
209         }
210         if (ruleList != null && !ruleList.isEmpty()) {
211             if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
212                 @SuppressWarnings("unchecked")
213                 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
214                 rules.addAll(ruleList);
215             } else {
216                 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
217             }
218         }
219     }
220 
221     private static @NonNull URL getPackagedFile(String packagedFileName) throws SuppressionParseException {
222         final URL jarLocation = AbstractSuppressionAnalyzer.class.getProtectionDomain().getCodeSource().getLocation();
223         String suppressionFileLocation = jarLocation.getFile();
224         if (suppressionFileLocation.endsWith(".jar")) {
225             suppressionFileLocation = "jar:file:" + suppressionFileLocation + "!/" + packagedFileName;
226         } else if (suppressionFileLocation.startsWith("nested:") && suppressionFileLocation.endsWith(".jar!/")) {
227             // suppressionFileLocation -> nested:/app/app.jar/!BOOT-INF/lib/dependency-check-core-<version>.jar!/
228             // goal->                 jar:nested:/app/app.jar/!BOOT-INF/lib/dependency-check-core-<version>.jar!/dependencycheck-base-suppression.xml
229             suppressionFileLocation = "jar:" + suppressionFileLocation + packagedFileName;
230         } else {
231             suppressionFileLocation = "file:" + suppressionFileLocation + packagedFileName;
232         }
233         URL baseSuppressionURL = null;
234         try {
235             baseSuppressionURL = new URL(suppressionFileLocation);
236         } catch (MalformedURLException e) {
237             throw new SuppressionParseException("Unable to load the packaged file: " + packagedFileName, e);
238         }
239         return baseSuppressionURL;
240     }
241 
242     /**
243      * Loads all the base suppression rules from the hosted suppression file
244      * generated/updated automatically by the FP Suppression GitHub Action for
245      * approved FP suppression.<br>
246      * Uses local caching as a fall-back in case the hosted location cannot be
247      * accessed, ignore any errors in the loading of the hosted suppression file
248      * emitting only a warning that some False Positives may emerge that have
249      * already been resolved by the dependency-check project.
250      *
251      * @param engine a reference the dependency-check engine
252      * @param parser The suppression parser to use
253      */
254     private void loadHostedSuppressionBaseData(final SuppressionParser parser, final Engine engine) {
255         final boolean enabled = getSettings().getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, true);
256         if (!enabled) {
257             return;
258         }
259 
260         try {
261             final String configuredUrl = getSettings().getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL,
262                     HostedSuppressionsDataSource.DEFAULT_SUPPRESSIONS_URL);
263             final URL url = new URL(configuredUrl);
264             final String fileName = new File(url.getPath()).getName();
265             if (fileName.isBlank()) {
266                 throw new IOException("Hosted Suppression URL must imply a filename");
267             }
268             final File repoFile = new File(getSettings().getDataDirectory(), fileName);
269             boolean repoEmpty = !repoFile.isFile() || repoFile.length() <= 1L;
270             if (repoEmpty) {
271                 // utilize the snapshot hosted suppression file
272                 URL hostedSuppressionSnapshotURL = getPackagedFile(HOSTED_SUPPRESSION_SNAPSHOT_FILE);
273                 try (InputStream in = hostedSuppressionSnapshotURL.openStream()) {
274                     Files.copy(in, repoFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
275                     repoEmpty = false;
276                     LOGGER.debug("Copied hosted suppression snapshot file to {}", repoFile.toPath());
277                 } catch (IOException ex) {
278                     LOGGER.warn("Unable to copy the hosted suppression snapshot file to {}, results may contain false positives "
279                             + "already resolved by the DependencyCheck project", repoFile.toPath(), ex);
280                 }
281             }
282             if (!repoEmpty) {
283                 loadCachedHostedSuppressionsRules(parser, repoFile, engine);
284             } else {
285                 LOGGER.warn("Empty Hosted Suppression file after update, results may contain false positives "
286                         + "already resolved by the DependencyCheck project due to failed download of the hosted suppression file");
287             }
288         } catch (IOException | InitializationException ex) {
289             LOGGER.warn("Unable to load hosted suppressions", ex);
290         }
291     }
292 
293     /**
294      * Load the hosted suppression file from the web resource
295      *
296      * @param parser The suppressionParser to use for loading
297      * @param repoFile The cached web resource
298      * @param engine a reference the dependency-check engine
299      *
300      * @throws InitializationException When errors occur trying to create a
301      * defensive copy of the web resource before loading
302      */
303     private void loadCachedHostedSuppressionsRules(final SuppressionParser parser, final File repoFile, final Engine engine)
304             throws InitializationException {
305         // take a defensive copy to avoid a risk of corrupted file by a competing parallel new download.
306         final Path defensiveCopy;
307         try (WriteLock lock = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
308             defensiveCopy = Files.createTempFile("dc-basesuppressions", ".xml");
309             LOGGER.debug("copying hosted suppressions file {} to {}", repoFile.toPath(), defensiveCopy);
310             Files.copy(repoFile.toPath(), defensiveCopy, StandardCopyOption.REPLACE_EXISTING);
311         } catch (WriteLockException | IOException ex) {
312             throw new InitializationException("Failed to copy the hosted suppressions file", ex);
313         }
314 
315         try (InputStream in = Files.newInputStream(defensiveCopy)) {
316             final List<SuppressionRule> ruleList;
317             ruleList = parser.parseSuppressionRules(in);
318             if (!ruleList.isEmpty()) {
319                 if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
320                     @SuppressWarnings("unchecked")
321                     final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
322                     rules.addAll(ruleList);
323                 } else {
324                     engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
325                 }
326             }
327         } catch (SAXException | IOException ex) {
328             LOGGER.warn("Unable to parse the hosted suppressions data file, results may contain false positives already resolved "
329                     + "by the DependencyCheck project", ex);
330         }
331         try {
332             Files.delete(defensiveCopy);
333         } catch (IOException ex) {
334             LOGGER.warn("Could not delete defensive copy of hosted suppressions file {}", defensiveCopy, ex);
335         }
336     }
337 
338     /**
339      * Load a single suppression rules file from the path provided using the
340      * parser provided.
341      *
342      * @param parser the parser to use for loading the file
343      * @param suppressionFilePath the path to load
344      * @return the list of loaded suppression rules
345      * @throws SuppressionParseException thrown if the suppression file cannot
346      * be loaded and parsed.
347      */
348     private List<SuppressionRule> loadSuppressionFile(final SuppressionParser parser,
349             final String suppressionFilePath) throws SuppressionParseException {
350         LOGGER.debug("Loading suppression rules from '{}'", suppressionFilePath);
351         final List<SuppressionRule> list = new ArrayList<>();
352         File file = null;
353         boolean deleteTempFile = false;
354         try {
355             final Pattern uriRx = Pattern.compile("^(https?|file):.*", Pattern.CASE_INSENSITIVE);
356             if (uriRx.matcher(suppressionFilePath).matches()) {
357                 deleteTempFile = true;
358                 file = getSettings().getTempFile("suppression", "xml");
359                 final URL url = new URL(suppressionFilePath);
360                 try {
361                     Downloader.getInstance().fetchFile(url, file, false, Settings.KEYS.SUPPRESSION_FILE_USER,
362                             Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
363                 } catch (DownloadFailedException ex) {
364                     LOGGER.trace("Failed download suppression file - first attempt", ex);
365                     try {
366                         Thread.sleep(500);
367                         Downloader.getInstance().fetchFile(url, file, true, Settings.KEYS.SUPPRESSION_FILE_USER,
368                                 Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
369                     } catch (TooManyRequestsException ex1) {
370                         throw new SuppressionParseException("Unable to download supression file `" + file
371                                 + "`; received 429 - too many requests", ex1);
372                     } catch (ResourceNotFoundException ex1) {
373                         throw new SuppressionParseException("Unable to download supression file `" + file
374                                 + "`; received 404 - resource not found", ex1);
375                     } catch (InterruptedException ex1) {
376                         Thread.currentThread().interrupt();
377                         throw new SuppressionParseException("Unable to download supression file `" + file + "`", ex1);
378                     }
379                 } catch (TooManyRequestsException ex) {
380                     throw new SuppressionParseException("Unable to download supression file `" + file
381                             + "`; received 429 - too many requests", ex);
382                 } catch (ResourceNotFoundException ex) {
383                     throw new SuppressionParseException("Unable to download supression file `" + file + "`; received 404 - resource not found", ex);
384                 }
385             } else {
386                 file = new File(suppressionFilePath);
387 
388                 if (!file.exists()) {
389                     try (InputStream suppressionFromClasspath = FileUtils.getResourceAsStream(suppressionFilePath)) {
390                         deleteTempFile = true;
391                         file = getSettings().getTempFile("suppression", "xml");
392                         try {
393                             Files.copy(suppressionFromClasspath, file.toPath());
394                         } catch (IOException ex) {
395                             throwSuppressionParseException("Unable to locate suppression file in classpath", ex, suppressionFilePath);
396                         }
397                     }
398                 }
399             }
400             if (!file.exists()) {
401                 final String msg = String.format("Suppression file '%s' does not exist", file.getPath());
402                 LOGGER.warn(msg);
403                 throw new SuppressionParseException(msg);
404             }
405             try {
406                 list.addAll(parser.parseSuppressionRules(file));
407             } catch (SuppressionParseException ex) {
408                 LOGGER.warn("Unable to parse suppression xml file '{}'", file.getPath());
409                 LOGGER.warn(ex.getMessage());
410                 throw ex;
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      * Utility method to throw parse exceptions.
430      *
431      * @param message the exception message
432      * @param exception the cause of the exception
433      * @param suppressionFilePath the path file
434      * @throws SuppressionParseException throws the generated
435      * SuppressionParseException
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      * Returns the number of suppression rules currently loaded in the engine.
445      *
446      * @param engine a reference to the ODC engine
447      * @return the count of rules loaded
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 }