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