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 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   * Abstract base suppression analyzer that contains methods for parsing the
61   * suppression XML file.
62   *
63   * @author Jeremy Long
64   */
65  @ThreadSafe
66  public abstract class AbstractSuppressionAnalyzer extends AbstractAnalyzer {
67  
68      /**
69       * The Logger for use throughout the class.
70       */
71      private static final Logger LOGGER = LoggerFactory.getLogger(AbstractSuppressionAnalyzer.class);
72      /**
73       * The file name of the base suppression XML file.
74       */
75      private static final String BASE_SUPPRESSION_FILE = "dependencycheck-base-suppression.xml";
76      /**
77       * The file name of the snapshot of the hosted suppression XML file.
78       */
79      private static final String HOSTED_SUPPRESSION_SNAPSHOT_FILE = "dependencycheck-hosted-suppression-snapshot.xml";
80      /**
81       * The key used to store and retrieve the suppression files.
82       */
83      public static final String SUPPRESSION_OBJECT_KEY = "suppression.rules";
84  
85      /**
86       * The prepare method loads the suppression XML file.
87       *
88       * @param engine a reference the dependency-check engine
89       * @throws InitializationException thrown if there is an exception
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      * Determines whether a suppression rule should be retained when filtering a
128      * set of suppression rules for a concrete suppression analyzer.
129      *
130      * @param rule the suppression rule to evaluate
131      * @return <code>true</code> if the rule should be retained; otherwise
132      * <code>false</code>
133      */
134     abstract boolean filter(SuppressionRule rule);
135 
136     /**
137      * Loads all the suppression rules files configured in the {@link Settings}.
138      *
139      * @param engine a reference to the ODC engine.
140      * @throws SuppressionParseException thrown if the XML cannot be parsed.
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             // Load all the suppression file paths
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      * Loads all the base suppression rules files.
171      *
172      * @param engine a reference the dependency-check engine
173      * @throws SuppressionParseException thrown if the XML cannot be parsed.
174      */
175     private void loadSuppressionBaseData(final Engine engine) throws SuppressionParseException {
176         loadPackagedBaseSuppressionData(engine);
177         loadHostedSuppressionBaseData(engine);
178     }
179 
180     /**
181      * Loads the suppression rules packaged with the application.
182      *
183      * @param engine a reference the dependency-check engine
184      * @throws SuppressionParseException thrown if the XML cannot be parsed.
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             // suppressionFileLocation -> nested:/app/app.jar/!BOOT-INF/lib/dependency-check-core-<version>.jar!/
206             // goal->                 jar:nested:/app/app.jar/!BOOT-INF/lib/dependency-check-core-<version>.jar!/dependencycheck-base-suppression.xml
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      * Loads all the base suppression rules from the hosted suppression file
220      * generated/updated automatically by the FP Suppression GitHub Action for
221      * approved FP suppression.<br>
222      * Uses local caching as a fall-back in case the hosted location cannot be
223      * accessed, ignore any errors in the loading of the hosted suppression file
224      * emitting only a warning that some False Positives may emerge that have
225      * already been resolved by the dependency-check project.
226      *
227      * @param engine a reference the dependency-check engine
228      */
229     @VisibleForTesting
230     void loadHostedSuppressionBaseData(final Engine engine) {
231         try {
232             // Try remote update if enabled and stale or forced by user
233             File repoFile = tryRemoteHostedSuppressionsFetchIfConfigured(engine);
234 
235             // If still empty after update attempt; utilize the snapshot hosted suppression file
236             //
237             // Note that this local fallback will run regardless of whether hosted suppressions are "enabled" or the
238             // value of autoUpdate, forceupdate etc since this is an offline operation similar to regular "base" suppressions.
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      * If configured to do so, try fetching hosted suppressions from the configured remote source.
257      * @return The local cached repoFile the suppressions are to be loaded from. Note that on return this may still not be created.
258      * @throws IOException only if settings are invalid to handle hosted suppressions either remotely or locally
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      * Load the hosted suppression file from the web resource
272      *
273      * @param repoFile The cached web resource
274      * @param engine a reference the dependency-check engine
275      *
276      * @throws InitializationException When errors occur trying to create a
277      * defensive copy of the web resource before loading
278      */
279     private void loadCachedHostedSuppressionsRules(final File repoFile, final Engine engine)
280             throws InitializationException {
281         // take a defensive copy to avoid a risk of corrupted file by a competing parallel new download.
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      * Load a single suppression rules file from the path provided using the
321      * parser provided.
322      *
323      * @param parser the parser to use for loading the file
324      * @param suppressionFilePath the path to load
325      * @return the list of loaded suppression rules
326      * @throws SuppressionParseException thrown if the suppression file cannot
327      * be loaded and parsed.
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      * Utility method to throw parse exceptions.
411      *
412      * @param message the exception message
413      * @param exception the cause of the exception
414      * @param suppressionFilePath the path file
415      * @throws SuppressionParseException throws the generated
416      * SuppressionParseException
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      * Returns the number of suppression rules currently loaded in the engine.
426      *
427      * @param engine a reference to the ODC engine
428      * @return the count of rules loaded
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 }