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  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   * Abstract base suppression analyzer that contains methods for parsing the
56   * suppression XML file.
57   *
58   * @author Jeremy Long
59   */
60  @ThreadSafe
61  public abstract class AbstractSuppressionAnalyzer extends AbstractAnalyzer {
62  
63      /**
64       * The Logger for use throughout the class.
65       */
66      private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSuppressionAnalyzer.class);
67      /**
68       * The file name of the base suppression XML file.
69       */
70      private static final String BASE_SUPPRESSION_FILE = "dependencycheck-base-suppression.xml";
71      /**
72       * The key used to store and retrieve the suppression files.
73       */
74      public static final String SUPPRESSION_OBJECT_KEY = "suppression.rules";
75  
76      /**
77       * Returns a list of file EXTENSIONS supported by this analyzer.
78       *
79       * @return a list of file EXTENSIONS supported by this analyzer.
80       */
81      @SuppressWarnings("SameReturnValue")
82      public Set<String> getSupportedExtensions() {
83          return null;
84      }
85  
86      /**
87       * The prepare method loads the suppression XML file.
88       *
89       * @param engine a reference the dependency-check engine
90       * @throws InitializationException thrown if there is an exception
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      * Determines whether a suppression rule should be retained when filtering a
129      * set of suppression rules for a concrete suppression analyzer.
130      *
131      * @param rule the suppression rule to evaluate
132      * @return <code>true</code> if the rule should be retained; otherwise
133      * <code>false</code>
134      */
135     abstract boolean filter(SuppressionRule rule);
136 
137     /**
138      * Loads all the suppression rules files configured in the {@link Settings}.
139      *
140      * @param engine a reference to the ODC engine.
141      * @throws SuppressionParseException thrown if the XML cannot be parsed.
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             // Load all the suppression file paths
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      * Loads all the base suppression rules files.
180      *
181      * @param engine a reference the dependency-check engine
182      * @throws SuppressionParseException thrown if the XML cannot be parsed.
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      * Loads the base suppression rules packaged with the application.
192      *
193      * @param parser The suppression parser to use
194      * @param engine a reference the dependency-check engine
195      * @throws SuppressionParseException thrown if the XML cannot be parsed.
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 if (suppressionFileLocation.startsWith("nested:") && suppressionFileLocation.endsWith(".jar!/")) {
204             // suppressionFileLocation -> nested:/app/app.jar/!BOOT-INF/lib/dependency-check-core-<version>.jar!/
205             // goal->                 jar:nested:/app/app.jar/!BOOT-INF/lib/dependency-check-core-<version>.jar!/dependencycheck-base-suppression.xml
206             suppressionFileLocation = "jar:" + suppressionFileLocation + BASE_SUPPRESSION_FILE;
207         } else {
208             suppressionFileLocation = "file:" + suppressionFileLocation + BASE_SUPPRESSION_FILE;
209         }
210         URL baseSuppresssionURL = null;
211         try {
212             baseSuppresssionURL = new URL(suppressionFileLocation);
213         } catch (MalformedURLException e) {
214             throw new SuppressionParseException("Unable to load the base suppression data file", e);
215         }
216         try (InputStream in = baseSuppresssionURL.openStream()) {
217             ruleList = parser.parseSuppressionRules(in);
218         } catch (SAXException | IOException ex) {
219             throw new SuppressionParseException("Unable to parse the base suppression data file", ex);
220         }
221         if (ruleList != null && !ruleList.isEmpty()) {
222             if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
223                 @SuppressWarnings("unchecked")
224                 final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
225                 rules.addAll(ruleList);
226             } else {
227                 engine.putObject(SUPPRESSION_OBJECT_KEY, ruleList);
228             }
229         }
230     }
231 
232     /**
233      * Loads all the base suppression rules from the hosted suppression file
234      * generated/updated automatically by the FP Suppression GitHub Action for
235      * approved FP suppression.<br>
236      * Uses local caching as a fall-back in case the hosted location cannot be
237      * accessed, ignore any errors in the loading of the hosted suppression file
238      * emitting only a warning that some False Positives may emerge that have
239      * already been resolved by the dependency-check project.
240      *
241      * @param engine a reference the dependency-check engine
242      * @param parser The suppression parser to use
243      */
244     private void loadHostedSuppressionBaseData(final SuppressionParser parser, final Engine engine) {
245         final File repoFile;
246         boolean repoEmpty = false;
247         final boolean enabled = getSettings().getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, true);
248         if (!enabled) {
249             return;
250         }
251         final boolean autoupdate = getSettings().getBoolean(Settings.KEYS.AUTO_UPDATE, true);
252         final boolean forceupdate = getSettings().getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, false);
253 
254         try {
255             final String configuredUrl = getSettings().getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL,
256                     HostedSuppressionsDataSource.DEFAULT_SUPPRESSIONS_URL);
257             final URL url = new URL(configuredUrl);
258             final String fileName = new File(url.getPath()).getName();
259             repoFile = new File(getSettings().getDataDirectory(), fileName);
260             if (!repoFile.isFile() || repoFile.length() <= 1L) {
261                 repoEmpty = true;
262                 LOGGER.warn("Hosted Suppressions file is empty or missing - attempting to force the update");
263                 getSettings().setBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, true);
264             }
265             if ((!autoupdate && forceupdate) || (autoupdate && repoEmpty)) {
266                 if (engine == null) {
267                     LOGGER.warn("Engine was null, this should only happen in tests - skipping forced update");
268                 } else {
269                     repoEmpty = forceUpdateHostedSuppressions(engine, repoFile);
270                 }
271             }
272             if (!repoEmpty) {
273                 loadCachedHostedSuppressionsRules(parser, repoFile, engine);
274             } else {
275                 LOGGER.warn("Empty Hosted Suppression file after update, results may contain false positives "
276                         + "already resolved by the DependencyCheck project due to failed download of the hosted suppression file");
277             }
278         } catch (IOException | InitializationException ex) {
279             LOGGER.warn("Unable to load hosted suppressions", ex);
280         }
281     }
282 
283     /**
284      * Load the hosted suppression file from the web resource
285      *
286      * @param parser The suppressionParser to use for loading
287      * @param repoFile The cached web resource
288      * @param engine a reference the dependency-check engine
289      *
290      * @throws InitializationException When errors occur trying to create a
291      * defensive copy of the web resource before loading
292      */
293     private void loadCachedHostedSuppressionsRules(final SuppressionParser parser, final File repoFile, final Engine engine)
294             throws InitializationException {
295         // take a defensive copy to avoid a risk of corrupted file by a competing parallel new download.
296         final Path defensiveCopy;
297         try (WriteLock lock = new WriteLock(getSettings(), true, repoFile.getName() + ".lock")) {
298             defensiveCopy = Files.createTempFile("dc-basesuppressions", ".xml");
299             LOGGER.debug("copying hosted suppressions file {} to {}", repoFile.toPath(), defensiveCopy);
300             Files.copy(repoFile.toPath(), defensiveCopy, StandardCopyOption.REPLACE_EXISTING);
301         } catch (WriteLockException | IOException ex) {
302             throw new InitializationException("Failed to copy the hosted suppressions file", ex);
303         }
304 
305         try (InputStream in = Files.newInputStream(defensiveCopy)) {
306             final List<SuppressionRule> ruleList;
307             ruleList = parser.parseSuppressionRules(in);
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         } catch (SAXException | IOException ex) {
318             LOGGER.warn("Unable to parse the hosted suppressions data file, results may contain false positives already resolved "
319                     + "by the DependencyCheck project", ex);
320         }
321         try {
322             Files.delete(defensiveCopy);
323         } catch (IOException ex) {
324             LOGGER.warn("Could not delete defensive copy of hosted suppressions file {}", defensiveCopy, ex);
325         }
326     }
327 
328     private static boolean forceUpdateHostedSuppressions(final Engine engine, final File repoFile) {
329         final HostedSuppressionsDataSource ds = new HostedSuppressionsDataSource();
330         boolean repoEmpty = true;
331         try {
332             ds.update(engine);
333             repoEmpty = !repoFile.isFile() || repoFile.length() <= 1L;
334         } catch (UpdateException ex) {
335             LOGGER.warn("Failed to update the Hosted Suppression file", ex);
336         }
337         return repoEmpty;
338     }
339 
340     /**
341      * Load a single suppression rules file from the path provided using the
342      * parser provided.
343      *
344      * @param parser the parser to use for loading the file
345      * @param suppressionFilePath the path to load
346      * @return the list of loaded suppression rules
347      * @throws SuppressionParseException thrown if the suppression file cannot
348      * be loaded and parsed.
349      */
350     private List<SuppressionRule> loadSuppressionFile(final SuppressionParser parser,
351             final String suppressionFilePath) throws SuppressionParseException {
352         LOGGER.debug("Loading suppression rules from '{}'", suppressionFilePath);
353         final List<SuppressionRule> list = new ArrayList<>();
354         File file = null;
355         boolean deleteTempFile = false;
356         try {
357             final Pattern uriRx = Pattern.compile("^(https?|file):.*", Pattern.CASE_INSENSITIVE);
358             if (uriRx.matcher(suppressionFilePath).matches()) {
359                 deleteTempFile = true;
360                 file = getSettings().getTempFile("suppression", "xml");
361                 final URL url = new URL(suppressionFilePath);
362                 try {
363                     Downloader.getInstance().fetchFile(url, file, false, Settings.KEYS.SUPPRESSION_FILE_USER,
364                             Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
365                 } catch (DownloadFailedException ex) {
366                     LOGGER.trace("Failed download suppression file - first attempt", ex);
367                     try {
368                         Thread.sleep(500);
369                         Downloader.getInstance().fetchFile(url, file, true, Settings.KEYS.SUPPRESSION_FILE_USER,
370                                 Settings.KEYS.SUPPRESSION_FILE_PASSWORD, Settings.KEYS.SUPPRESSION_FILE_BEARER_TOKEN);
371                     } catch (TooManyRequestsException ex1) {
372                         throw new SuppressionParseException("Unable to download supression file `" + file
373                                 + "`; received 429 - too many requests", ex1);
374                     } catch (ResourceNotFoundException ex1) {
375                         throw new SuppressionParseException("Unable to download supression file `" + file
376                                 + "`; received 404 - resource not found", ex1);
377                     } catch (InterruptedException ex1) {
378                         Thread.currentThread().interrupt();
379                         throw new SuppressionParseException("Unable to download supression file `" + file + "`", ex1);
380                     }
381                 } catch (TooManyRequestsException ex) {
382                     throw new SuppressionParseException("Unable to download supression file `" + file
383                             + "`; received 429 - too many requests", ex);
384                 } catch (ResourceNotFoundException ex) {
385                     throw new SuppressionParseException("Unable to download supression file `" + file + "`; received 404 - resource not found", ex);
386                 }
387             } else {
388                 file = new File(suppressionFilePath);
389 
390                 if (!file.exists()) {
391                     try (InputStream suppressionFromClasspath = FileUtils.getResourceAsStream(suppressionFilePath)) {
392                         deleteTempFile = true;
393                         file = getSettings().getTempFile("suppression", "xml");
394                         try {
395                             Files.copy(suppressionFromClasspath, file.toPath());
396                         } catch (IOException ex) {
397                             throwSuppressionParseException("Unable to locate suppression file in classpath", ex, suppressionFilePath);
398                         }
399                     }
400                 }
401             }
402             if (file != null) {
403                 if (!file.exists()) {
404                     final String msg = String.format("Suppression file '%s' does not exist", file.getPath());
405                     LOGGER.warn(msg);
406                     throw new SuppressionParseException(msg);
407                 }
408                 try {
409                     list.addAll(parser.parseSuppressionRules(file));
410                 } catch (SuppressionParseException ex) {
411                     LOGGER.warn("Unable to parse suppression xml file '{}'", file.getPath());
412                     LOGGER.warn(ex.getMessage());
413                     throw ex;
414                 }
415             }
416         } catch (DownloadFailedException ex) {
417             throwSuppressionParseException("Unable to fetch the configured suppression file", ex, suppressionFilePath);
418         } catch (MalformedURLException ex) {
419             throwSuppressionParseException("Configured suppression file has an invalid URL", ex, suppressionFilePath);
420         } catch (SuppressionParseException ex) {
421             throw ex;
422         } catch (IOException ex) {
423             throwSuppressionParseException("Unable to read suppression file", ex, suppressionFilePath);
424         } finally {
425             if (deleteTempFile && file != null) {
426                 FileUtils.delete(file);
427             }
428         }
429         return list;
430     }
431 
432     /**
433      * Utility method to throw parse exceptions.
434      *
435      * @param message the exception message
436      * @param exception the cause of the exception
437      * @param suppressionFilePath the path file
438      * @throws SuppressionParseException throws the generated
439      * SuppressionParseException
440      */
441     private void throwSuppressionParseException(String message, Exception exception, String suppressionFilePath) throws SuppressionParseException {
442         LOGGER.warn(String.format(message + " '%s'", suppressionFilePath));
443         LOGGER.debug("", exception);
444         throw new SuppressionParseException(message, exception);
445     }
446 
447     /**
448      * Returns the number of suppression rules currently loaded in the engine.
449      *
450      * @param engine a reference to the ODC engine
451      * @return the count of rules loaded
452      */
453     public static int getRuleCount(Engine engine) {
454         if (engine.hasObject(SUPPRESSION_OBJECT_KEY)) {
455             @SuppressWarnings("unchecked")
456             final List<SuppressionRule> rules = (List<SuppressionRule>) engine.getObject(SUPPRESSION_OBJECT_KEY);
457             return rules.size();
458         }
459         return 0;
460     }
461 }