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) 2022 Hans Aikema. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.data.update;
19  
20  import org.jspecify.annotations.NonNull;
21  import org.owasp.dependencycheck.Engine;
22  import org.owasp.dependencycheck.data.update.exception.UpdateException;
23  import org.owasp.dependencycheck.exception.WriteLockException;
24  import org.owasp.dependencycheck.utils.Downloader;
25  import org.owasp.dependencycheck.utils.InvalidSettingException;
26  import org.owasp.dependencycheck.utils.ResourceNotFoundException;
27  import org.owasp.dependencycheck.utils.Settings;
28  import org.owasp.dependencycheck.utils.TooManyRequestsException;
29  import org.owasp.dependencycheck.utils.WriteLock;
30  import org.slf4j.Logger;
31  import org.slf4j.LoggerFactory;
32  
33  import java.io.File;
34  import java.io.IOException;
35  import java.net.MalformedURLException;
36  import java.net.URL;
37  import java.nio.file.Files;
38  import java.time.Duration;
39  
40  public class HostedSuppressionsDataSource extends LocalDataSource {
41      /**
42       * The default URL to the Hosted Suppressions file.
43       */
44      public static final String DEFAULT_SUPPRESSIONS_URL = "https://dependency-check.github.io/DependencyCheck/suppressions/publishedSuppressions.xml";
45  
46      /**
47       * Static logger.
48       */
49      private static final Logger LOGGER = LoggerFactory.getLogger(HostedSuppressionsDataSource.class);
50  
51      /**
52       * The configured settings.
53       */
54      private Settings settings;
55  
56      /**
57       * Makes a best effort to download the current Hosted suppressions file if configured to do so.
58       *
59       * @param engine a reference to the ODC Engine
60       * @return returns false as no updates are made to the database, just web
61       * resources cached locally
62       * @throws UpdateException thrown only if the update encountered fatal configuration errors
63       */
64      @Override
65      public boolean update(Engine engine) throws UpdateException {
66          try {
67              updateUnhandled(engine);
68          } catch (UpdateException ex) {
69              // only emit a warning, DependencyCheck will continue without taking the latest hosted suppressions into account.
70              LOGGER.warn(falsePositivesDueTo("Failed to update hosted suppressions file from remote source"), ex);
71          } catch (IOException ex) {
72              // Unhandled IOExceptions are fatal configuration errors of a sort
73              throw new UpdateException("Unable to determine the local location to cache hosted suppressions", ex);
74          }
75          return false;
76      }
77  
78      public static @NonNull String falsePositivesDueTo(String reason) {
79          return reason + ", results may contain false positives already resolved by the DependencyCheck project";
80      }
81  
82      /**
83       * Updates the current Hosted Suppressions file if configured to do so; failing if it cannot be done
84       *
85       * @param engine a reference to the ODC Engine
86       * @throws IOException if there is an error determining the local location to cache hosted suppressions
87       * @throws UpdateException if the remote update failed for any reason
88       */
89      public void updateUnhandled(Engine engine) throws IOException, UpdateException {
90          this.settings = engine.getSettings();
91          final URL url = validatedUrl();
92          final File repoFile = validatedRepoFileFrom(url);
93  
94          if (isEnabled() && shouldUpdateFromRemote(repoFile)) {
95              LOGGER.debug("Begin Hosted Suppressions file update from remote source");
96              fetchHostedSuppressions(url, repoFile);
97              saveLastUpdated(repoFile);
98          }
99      }
100 
101     private @NonNull URL validatedUrl() throws InvalidSettingException {
102         final String configuredUrl = settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, DEFAULT_SUPPRESSIONS_URL);
103         try {
104             return new URL(configuredUrl);
105         } catch (MalformedURLException ex) {
106             throw new InvalidSettingException(String.format("Invalid URL for Hosted Suppressions file (%s)", configuredUrl), ex);
107         }
108     }
109 
110     public @NonNull File validatedRepoFile() throws IOException {
111         return validatedRepoFileFrom(validatedUrl());
112     }
113 
114     private @NonNull File validatedRepoFileFrom(URL url) throws IOException {
115         String fileName = new File(url.getPath()).getName();
116         if (fileName.isBlank()) {
117             throw new InvalidSettingException("Hosted Suppression URL must imply a filename; even if disabled.");
118         }
119         return new File(settings.getDataDirectory(), fileName);
120     }
121 
122     private boolean isEnabled() {
123         return settings.getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, true) && (
124                 settings.getBoolean(Settings.KEYS.ANALYZER_CPE_SUPPRESSION_ENABLED, true) ||
125                         settings.getBoolean(Settings.KEYS.ANALYZER_VULNERABILITY_SUPPRESSION_ENABLED, true));
126     }
127 
128     private boolean shouldUpdateFromRemote(File repoFile) {
129         boolean forceupdate = settings.getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, false);
130         boolean autoupdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
131         Duration validFor = Duration.ofHours(settings.getInt(Settings.KEYS.HOSTED_SUPPRESSIONS_VALID_FOR_HOURS, 2));
132         return forceupdate || (autoupdate && isStale(repoFile, validFor));
133     }
134 
135     /**
136      * Fetches the hosted suppressions file
137      *
138      * @param repoUrl the URL to the hosted suppressions file to use
139      * @param repoFile the local file where the hosted suppressions file is to
140      * be placed
141      * @throws UpdateException thrown if there is an exception during
142      * initialization
143      */
144     @SuppressWarnings("try")
145     private void fetchHostedSuppressions(URL repoUrl, File repoFile) throws UpdateException {
146         try (WriteLock ignored = new WriteLock(settings, true, repoFile.getName() + ".lock")) {
147             if (LOGGER.isDebugEnabled()) {
148                 LOGGER.debug("Hosted Suppressions URL: {}", repoUrl.toExternalForm());
149             }
150             Downloader.getInstance().fetchFile(repoUrl, repoFile);
151         } catch (IOException | TooManyRequestsException | ResourceNotFoundException | WriteLockException ex) {
152             throw new UpdateException("Failed to update the hosted suppressions file", ex);
153         }
154     }
155 
156     @Override
157     @SuppressWarnings("try")
158     public boolean purge(Engine engine) {
159         this.settings = engine.getSettings();
160         boolean result = true;
161         try {
162             final File repo = validatedRepoFile();
163             if (repo.exists()) {
164                 try (WriteLock ignored = new WriteLock(settings, true, repo.getName() + ".lock")) {
165                     result = deleteCachedFile(repo);
166                 }
167             }
168         } catch (WriteLockException | IOException ex) {
169             LOGGER.error("Unable to delete the Hosted suppression file - invalid configuration: {}", ex.toString());
170             result = false;
171         }
172         return result;
173     }
174 
175     private boolean deleteCachedFile(final File repo) {
176         boolean deleted = true;
177         try {
178             if (Files.deleteIfExists(repo.toPath())) {
179                 LOGGER.info("Hosted suppression file removed successfully");
180             }
181         } catch (IOException ex) {
182             LOGGER.error("Unable to delete '{}'; please delete the file manually", repo.getAbsolutePath(), ex);
183             deleted = false;
184         }
185         return deleted;
186     }
187 }