1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.data.nexus;
19
20 import org.apache.hc.client5.http.HttpResponseException;
21 import org.apache.hc.client5.http.impl.classic.AbstractHttpClientResponseHandler;
22 import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
23 import org.apache.hc.core5.http.ContentType;
24 import org.apache.hc.core5.http.HttpEntity;
25 import org.apache.hc.core5.http.HttpHeaders;
26 import org.apache.hc.core5.http.message.BasicHeader;
27 import org.jetbrains.annotations.Nullable;
28 import org.owasp.dependencycheck.utils.DownloadFailedException;
29 import org.owasp.dependencycheck.utils.Downloader;
30 import org.owasp.dependencycheck.utils.ResourceNotFoundException;
31 import org.owasp.dependencycheck.utils.Settings;
32 import org.owasp.dependencycheck.utils.TooManyRequestsException;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35
36 import javax.annotation.concurrent.ThreadSafe;
37 import jakarta.json.Json;
38 import jakarta.json.JsonArray;
39 import jakarta.json.JsonObject;
40 import jakarta.json.JsonReader;
41 import java.io.BufferedReader;
42 import java.io.FileNotFoundException;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.io.InputStreamReader;
46 import java.io.StringReader;
47 import java.net.MalformedURLException;
48 import java.net.URL;
49 import java.nio.charset.StandardCharsets;
50 import java.util.ArrayList;
51 import java.util.HashSet;
52 import java.util.List;
53 import java.util.Set;
54 import java.util.stream.Collectors;
55
56
57
58
59
60
61 @ThreadSafe
62 public class NexusV3Search implements NexusSearch {
63
64
65
66
67
68
69
70 private final Set<String> acceptedClassifiers = new HashSet<>();
71
72
73
74
75 private final URL rootURL;
76
77
78
79
80 private final boolean useProxy;
81
82
83
84 private final Settings settings;
85
86
87
88 private static final Logger LOGGER = LoggerFactory.getLogger(NexusV3Search.class);
89
90
91
92
93
94
95
96
97
98 public NexusV3Search(Settings settings, boolean useProxy) throws MalformedURLException {
99 this.settings = settings;
100 this.useProxy = useProxy;
101 this.acceptedClassifiers.add(null);
102 final String searchUrl = settings.getString(Settings.KEYS.ANALYZER_NEXUS_URL);
103 LOGGER.debug("Nexus Search URL: {}", searchUrl);
104 this.rootURL = new URL(searchUrl);
105
106 }
107
108 @Override
109 public MavenArtifact searchSha1(String sha1) throws IOException {
110 if (null == sha1 || !sha1.matches("^[0-9A-Fa-f]{40}$")) {
111 throw new IllegalArgumentException("Invalid SHA1 format");
112 }
113
114 final List<MavenArtifact> collectedMatchingArtifacts = new ArrayList<>(1);
115 try (CloseableHttpClient client = Downloader.getInstance().getHttpClient(useProxy)) {
116 String continuationToken = retrievePageAndAddMatchingArtifact(client, collectedMatchingArtifacts, sha1, null);
117 while (continuationToken != null && collectedMatchingArtifacts.isEmpty()) {
118 continuationToken = retrievePageAndAddMatchingArtifact(client, collectedMatchingArtifacts, sha1, continuationToken);
119 }
120 }
121 if (collectedMatchingArtifacts.isEmpty()) {
122 throw new FileNotFoundException("Artifact not found in Nexus");
123 } else {
124 return collectedMatchingArtifacts.get(0);
125 }
126 }
127
128 private String retrievePageAndAddMatchingArtifact(CloseableHttpClient client, List<MavenArtifact> collectedMatchingArtifacts, String sha1,
129 @Nullable String continuationToken) throws IOException {
130 final URL url;
131 LOGGER.debug("Search with continuation token {}", continuationToken);
132 if (continuationToken == null) {
133 url = new URL(rootURL, String.format("v1/search/?sha1=%s",
134 sha1.toLowerCase()));
135 } else {
136 url = new URL(rootURL, String.format("v1/search/?sha1=%s&continuationToken=%s",
137 sha1.toLowerCase(), continuationToken));
138 }
139
140 LOGGER.debug("Searching Nexus url {}", url);
141
142
143
144
145 final NexusV3SearchResponseHandler handler = new NexusV3SearchResponseHandler(collectedMatchingArtifacts, sha1, acceptedClassifiers);
146 try {
147 return Downloader.getInstance().fetchAndHandle(client, url, handler, List.of(new BasicHeader(HttpHeaders.ACCEPT,
148 ContentType.APPLICATION_JSON)));
149 } catch (TooManyRequestsException | ResourceNotFoundException | DownloadFailedException e) {
150 if (LOGGER.isDebugEnabled()) {
151 int responseCode = -1;
152 String responseMessage = "";
153 if (e.getCause() instanceof HttpResponseException) {
154 final HttpResponseException cause = (HttpResponseException) e.getCause();
155 responseCode = cause.getStatusCode();
156 responseMessage = cause.getReasonPhrase();
157 }
158 LOGGER.debug("Could not connect to Nexus received response code: {} {}",
159 responseCode, responseMessage);
160 }
161 throw new IOException("Could not connect to Nexus", e);
162 }
163 }
164
165 private static final class NexusV3SearchResponseHandler extends AbstractHttpClientResponseHandler<String> {
166
167
168
169
170 private final List<MavenArtifact> matchingArtifacts;
171
172
173
174 private final String sha1;
175
176
177
178 private final Set<String> acceptedClassifiers;
179
180 private NexusV3SearchResponseHandler(List<MavenArtifact> matchingArtifacts, String sha1, Set<String> acceptedClassifiers) {
181 this.matchingArtifacts = matchingArtifacts;
182 this.sha1 = sha1;
183 this.acceptedClassifiers = acceptedClassifiers;
184 }
185
186 @Override
187 public @Nullable String handleEntity(HttpEntity entity) throws IOException {
188 try (InputStream in = entity.getContent();
189 InputStreamReader isReader = new InputStreamReader(in, StandardCharsets.UTF_8);
190 BufferedReader reader = new BufferedReader(isReader);
191 ) {
192 final String jsonString = reader.lines().collect(Collectors.joining("\n"));
193 LOGGER.debug("JSON String was >>>{}<<<", jsonString);
194 final JsonObject jsonResponse;
195 try (
196 StringReader stringReader = new StringReader(jsonString);
197 JsonReader jsonReader = Json.createReader(stringReader)
198 ) {
199 jsonResponse = jsonReader.readObject();
200 }
201 LOGGER.debug("Response: {}", jsonResponse);
202 final JsonArray components = jsonResponse.getJsonArray("items");
203 LOGGER.debug("Items: {}", components);
204 final String continuationToken = jsonResponse.getString("continuationToken", null);
205 boolean found = false;
206 for (int i = 0; i < components.size() && !found; i++) {
207 boolean jarFound = false;
208 boolean pomFound = false;
209 String downloadUrl = null;
210 String groupId = null;
211 String artifactId = null;
212 String version = null;
213 String pomUrl = null;
214
215 final JsonObject component = components.getJsonObject(i);
216
217 final String format = component.getString("format", "unknown");
218 if ("maven2".equals(format)) {
219 LOGGER.debug("Checking Maven2 artifact for {}", component);
220 final JsonArray assets = component.getJsonArray("assets");
221 for (int j = 0; !found && j < assets.size(); j++) {
222 final JsonObject asset = assets.getJsonObject(j);
223 LOGGER.debug("Checking {}", asset);
224 final JsonObject checksums = asset.getJsonObject("checksum");
225 final JsonObject maven2 = asset.getJsonObject("maven2");
226 if (maven2 != null) {
227
228 final boolean shaMatch = checksums != null && sha1.equals(checksums.getString("sha1", null));
229 final boolean hasAcceptedClassifier = acceptedClassifiers.contains(maven2.getString("classifier", null));
230 final boolean isAJar = "jar".equals(maven2.getString("extension", null));
231 LOGGER.debug("shaMatch {}", shaMatch);
232 LOGGER.debug("hasAcceptedClassifier {}", hasAcceptedClassifier);
233 LOGGER.debug("isAJar {}", isAJar);
234 if (
235 isAJar
236 && hasAcceptedClassifier
237 && shaMatch
238 ) {
239 downloadUrl = asset.getString("downloadUrl");
240 groupId = maven2.getString("groupId");
241 artifactId = maven2.getString("artifactId");
242 version = maven2.getString("version");
243
244 jarFound = true;
245 } else if ("pom".equals(maven2.getString("extension"))) {
246 LOGGER.debug("pom found {}", asset);
247 pomFound = true;
248 pomUrl = asset.getString("downloadUrl");
249 }
250 }
251 if (pomFound && jarFound) {
252 found = true;
253 }
254 }
255 if (found) {
256 matchingArtifacts.add(new MavenArtifact(groupId, artifactId, version, downloadUrl, pomUrl));
257 } else if (jarFound) {
258 final MavenArtifact ma = new MavenArtifact(groupId, artifactId, version, downloadUrl);
259 ma.setPomUrl(MavenArtifact.derivePomUrl(artifactId, version, downloadUrl));
260 matchingArtifacts.add(ma);
261 found = true;
262 }
263 }
264 }
265 return continuationToken;
266 }
267 }
268 }
269
270 @Override
271 public boolean preflightRequest() {
272 try {
273 final URL url = new URL(rootURL, "v1/status");
274 final String response = Downloader.getInstance().fetchContent(url, useProxy, StandardCharsets.UTF_8);
275 if (response == null || !response.isEmpty()) {
276 LOGGER.warn("Expected empty OK response (content-length 0), got {}", response == null ? "null" : response.length());
277 return false;
278 }
279 } catch (IOException | TooManyRequestsException | ResourceNotFoundException e) {
280 LOGGER.warn("Pre-flight request to Nexus failed: ", e);
281 return false;
282 }
283 return true;
284 }
285
286 }