1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
43
44 public static final String DEFAULT_SUPPRESSIONS_URL = "https://dependency-check.github.io/DependencyCheck/suppressions/publishedSuppressions.xml";
45
46
47
48
49 private static final Logger LOGGER = LoggerFactory.getLogger(HostedSuppressionsDataSource.class);
50
51
52
53
54 private Settings settings;
55
56
57
58
59
60
61
62
63
64 @Override
65 public boolean update(Engine engine) throws UpdateException {
66 try {
67 updateUnhandled(engine);
68 } catch (UpdateException ex) {
69
70 LOGGER.warn(falsePositivesDueTo("Failed to update hosted suppressions file from remote source"), ex);
71 } catch (IOException ex) {
72
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
84
85
86
87
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
137
138
139
140
141
142
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 }