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