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.owasp.dependencycheck.Engine;
21  import org.owasp.dependencycheck.data.update.exception.UpdateException;
22  import org.owasp.dependencycheck.exception.WriteLockException;
23  import org.owasp.dependencycheck.utils.Downloader;
24  import org.owasp.dependencycheck.utils.ResourceNotFoundException;
25  import org.owasp.dependencycheck.utils.Settings;
26  import org.owasp.dependencycheck.utils.TooManyRequestsException;
27  import org.owasp.dependencycheck.utils.WriteLock;
28  import org.slf4j.Logger;
29  import org.slf4j.LoggerFactory;
30  
31  import java.io.File;
32  import java.io.IOException;
33  import java.net.MalformedURLException;
34  import java.net.URL;
35  import java.nio.file.Files;
36  
37  public class HostedSuppressionsDataSource extends LocalDataSource {
38  
39      /**
40       * Static logger.
41       */
42      private static final Logger LOGGER = LoggerFactory.getLogger(HostedSuppressionsDataSource.class);
43  
44      /**
45       * The configured settings.
46       */
47      private Settings settings;
48      /**
49       * The default URL to the Hosted Suppressions file.
50       */
51      public static final String DEFAULT_SUPPRESSIONS_URL = "https://jeremylong.github.io/DependencyCheck/suppressions/publishedSuppressions.xml";
52  
53      /**
54       * Downloads the current Hosted suppressions file.
55       *
56       * @param engine a reference to the ODC Engine
57       * @return returns false as no updates are made to the database, just web
58       * resources cached locally
59       * @throws UpdateException thrown if the update encountered fatal errors
60       */
61      @Override
62      public boolean update(Engine engine) throws UpdateException {
63          this.settings = engine.getSettings();
64          final String configuredUrl = settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL, DEFAULT_SUPPRESSIONS_URL);
65          final boolean autoupdate = settings.getBoolean(Settings.KEYS.AUTO_UPDATE, true);
66          final boolean forceupdate = settings.getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_FORCEUPDATE, false);
67          final boolean cpeSuppressionEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_CPE_SUPPRESSION_ENABLED, true);
68          final boolean vulnSuppressionEnabled = settings.getBoolean(Settings.KEYS.ANALYZER_VULNERABILITY_SUPPRESSION_ENABLED, true);
69          boolean enabled = settings.getBoolean(Settings.KEYS.HOSTED_SUPPRESSIONS_ENABLED, true);
70          enabled = enabled && (cpeSuppressionEnabled || vulnSuppressionEnabled);
71          try {
72              final URL url = new URL(configuredUrl);
73              final File filepath = new File(url.getPath());
74              final File repoFile = new File(settings.getDataDirectory(), filepath.getName());
75              final boolean proceed = enabled && (forceupdate || (autoupdate && shouldUpdate(repoFile)));
76              if (proceed) {
77                  LOGGER.debug("Begin Hosted Suppressions file update");
78                  fetchHostedSuppressions(settings, url, repoFile);
79                  saveLastUpdated(repoFile, System.currentTimeMillis() / 1000);
80              }
81          } catch (UpdateException ex) {
82              // only emit a warning, DependencyCheck will continue without taking the latest hosted suppressions into account.
83              LOGGER.warn("Failed to update hosted suppressions file, results may contain false positives already resolved by the "
84                      + "DependencyCheck project", ex);
85          } catch (MalformedURLException ex) {
86              throw new UpdateException(String.format("Invalid URL for Hosted Suppressions file (%s)", configuredUrl), ex);
87          } catch (IOException ex) {
88              throw new UpdateException("Unable to get the data directory", ex);
89          }
90          return false;
91      }
92  
93      /**
94       * Determines if the we should update the Hosted Suppressions file.
95       *
96       * @param repo the Hosted Suppressions file.
97       * @return <code>true</code> if an update to the Hosted Suppressions file
98       * should be performed; otherwise <code>false</code>
99       * @throws NumberFormatException thrown if an invalid value is contained in
100      * the database properties
101      */
102     protected boolean shouldUpdate(File repo) throws NumberFormatException {
103         boolean proceed = true;
104         if (repo != null && repo.isFile()) {
105             final int validForHours = settings.getInt(Settings.KEYS.HOSTED_SUPPRESSIONS_VALID_FOR_HOURS, 2);
106             final long lastUpdatedOn = getLastUpdated(repo);
107             final long now = System.currentTimeMillis();
108             LOGGER.debug("Last updated: {}", lastUpdatedOn);
109             LOGGER.debug("Now: {}", now);
110             final long msValid = validForHours * 60L * 60L * 1000L;
111             proceed = (now - lastUpdatedOn) > msValid;
112             if (!proceed) {
113                 LOGGER.info("Skipping Hosted Suppressions file update since last update was within {} hours.", validForHours);
114             }
115         }
116         return proceed;
117     }
118 
119     /**
120      * Fetches the hosted suppressions file
121      *
122      * @param settings a reference to the dependency-check settings
123      * @param repoUrl the URL to the hosted suppressions file to use
124      * @param repoFile the local file where the hosted suppressions file is to
125      * be placed
126      * @throws UpdateException thrown if there is an exception during
127      * initialization
128      */
129     @SuppressWarnings("try")
130     private void fetchHostedSuppressions(Settings settings, URL repoUrl, File repoFile) throws UpdateException {
131         try (WriteLock ignored = new WriteLock(settings, true, repoFile.getName() + ".lock")) {
132             if (LOGGER.isDebugEnabled()) {
133                 LOGGER.debug("Hosted Suppressions URL: {}", repoUrl.toExternalForm());
134             }
135             Downloader.getInstance().fetchFile(repoUrl, repoFile);
136         } catch (IOException | TooManyRequestsException | ResourceNotFoundException | WriteLockException ex) {
137             throw new UpdateException("Failed to update the hosted suppressions file", ex);
138         }
139     }
140 
141     @Override
142     @SuppressWarnings("try")
143     public boolean purge(Engine engine) {
144         this.settings = engine.getSettings();
145         boolean result = true;
146         try {
147             final URL repoUrl = new URL(settings.getString(Settings.KEYS.HOSTED_SUPPRESSIONS_URL,
148                     DEFAULT_SUPPRESSIONS_URL));
149             final String filename = new File(repoUrl.getPath()).getName();
150             final File repo = new File(settings.getDataDirectory(), filename);
151             if (repo.exists()) {
152                 try (WriteLock ignored = new WriteLock(settings, true, filename + ".lock")) {
153                     result = deleteCachedFile(repo);
154                 }
155             }
156         } catch (WriteLockException | IOException ex) {
157             LOGGER.error("Unable to delete the Hosted suppression file - invalid configuration");
158             result = false;
159         }
160         return result;
161     }
162 
163     private boolean deleteCachedFile(final File repo) {
164         boolean deleted = true;
165         try {
166             Files.delete(repo.toPath());
167             LOGGER.info("Hosted suppression file removed successfully");
168         } catch (IOException ex) {
169             LOGGER.error("Unable to delete '{}'; please delete the file manually", repo.getAbsolutePath(), ex);
170             deleted = false;
171         }
172         return deleted;
173     }
174 }