1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.data.artifactory;
19
20 import com.fasterxml.jackson.core.JsonParser;
21 import com.fasterxml.jackson.core.JsonToken;
22 import com.fasterxml.jackson.databind.DeserializationFeature;
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.ObjectReader;
25 import org.apache.hc.core5.http.ClassicHttpResponse;
26 import org.apache.hc.core5.http.io.HttpClientResponseHandler;
27 import org.owasp.dependencycheck.data.nexus.MavenArtifact;
28 import org.owasp.dependencycheck.dependency.Dependency;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 import java.io.FileNotFoundException;
33 import java.io.IOException;
34 import java.io.InputStreamReader;
35 import java.net.URL;
36 import java.nio.charset.StandardCharsets;
37 import java.util.ArrayList;
38 import java.util.List;
39 import java.util.Optional;
40 import java.util.regex.Matcher;
41 import java.util.regex.Pattern;
42
43 import static org.owasp.dependencycheck.data.artifactory.ArtifactorySearch.X_RESULT_DETAIL_HEADER;
44
45 class ArtifactorySearchResponseHandler implements HttpClientResponseHandler<List<MavenArtifact>> {
46
47
48
49 private static final Pattern PATH_PATTERN = Pattern.compile("^/(?<groupId>.+)/(?<artifactId>[^/]+)/(?<version>[^/]+)/[^/]+$");
50
51
52
53
54 private static final Logger LOGGER = LoggerFactory.getLogger(ArtifactorySearchResponseHandler.class);
55
56
57
58
59 private final ObjectReader fileImplReader;
60
61
62
63
64 private final Dependency expectedDependency;
65
66
67
68
69 private final URL sourceUrl;
70
71
72
73
74
75
76
77 ArtifactorySearchResponseHandler(URL sourceUrl, Dependency dependency) {
78 this.fileImplReader = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).readerFor(FileImpl.class);
79 this.expectedDependency = dependency;
80 this.sourceUrl = sourceUrl;
81 }
82
83 protected boolean init(JsonParser parser) throws IOException {
84 com.fasterxml.jackson.core.JsonToken nextToken = parser.nextToken();
85 if (nextToken != com.fasterxml.jackson.core.JsonToken.START_OBJECT) {
86 throw new IOException("Expected " + com.fasterxml.jackson.core.JsonToken.START_OBJECT + ", got " + nextToken);
87 }
88
89 do {
90 nextToken = parser.nextToken();
91 if (nextToken == null) {
92 break;
93 }
94
95 if (nextToken.isStructStart()) {
96 if (nextToken == com.fasterxml.jackson.core.JsonToken.START_ARRAY && "results".equals(parser.currentName())) {
97 return true;
98 } else {
99 parser.skipChildren();
100 }
101 }
102 } while (true);
103
104 return false;
105 }
106
107
108
109
110
111
112
113 private boolean checkHashes(ChecksumsImpl checksums) {
114 final String md5sum = expectedDependency.getMd5sum();
115 final String hashMismatchFormat = "Artifact found by API is not matching the {} of the artifact (repository hash is {} while actual is {}) !";
116 boolean match = true;
117 if (!checksums.getMd5().equals(md5sum)) {
118 LOGGER.warn(hashMismatchFormat, "md5", md5sum, checksums.getMd5());
119 match = false;
120 }
121 final String sha1sum = expectedDependency.getSha1sum();
122 if (!checksums.getSha1().equals(sha1sum)) {
123 LOGGER.warn(hashMismatchFormat, "sha1", sha1sum, checksums.getSha1());
124 match = false;
125 }
126 final String sha256sum = expectedDependency.getSha256sum();
127
128
129
130
131
132
133
134 if (checksums.getSha256() != null && !checksums.getSha256().equals(sha256sum)) {
135 LOGGER.warn(hashMismatchFormat, "sha256", sha256sum, checksums.getSha256());
136 match = false;
137 }
138 return match;
139 }
140
141
142
143
144
145
146
147
148
149 @Override
150 public List<MavenArtifact> handleResponse(ClassicHttpResponse response) throws IOException {
151 final List<MavenArtifact> result = new ArrayList<>();
152
153 try (InputStreamReader streamReader = new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8);
154 JsonParser parser = fileImplReader.getFactory().createParser(streamReader)) {
155
156 if (init(parser) && parser.nextToken() == JsonToken.START_OBJECT) {
157
158 do {
159 final FileImpl file = fileImplReader.readValue(parser);
160
161 if (file.getChecksums() == null) {
162 LOGGER.warn(
163 "No checksums found in Artifactory search result for '{}'. " +
164 "Specifically, the result set contains URI '{}' but it is missing the 'checksums' property. " +
165 "Please make sure that the '{}' header is retained on any (reverse-)proxy, load-balancer or Web Application Firewall in the network path to your Artifactory server.",
166 sourceUrl, file.getUri(), X_RESULT_DETAIL_HEADER);
167 continue;
168 }
169
170 final Optional<Matcher> validationResult = validateUsability(file);
171 if (validationResult.isEmpty()) {
172 continue;
173 }
174 final Matcher pathMatcher = validationResult.get();
175
176 final String groupId = pathMatcher.group("groupId").replace('/', '.');
177 final String artifactId = pathMatcher.group("artifactId");
178 final String version = pathMatcher.group("version");
179
180 result.add(new MavenArtifact(groupId, artifactId, version, file.getDownloadUri(),
181 MavenArtifact.derivePomUrl(artifactId, version, file.getDownloadUri())));
182
183 } while (parser.nextToken() == JsonToken.START_OBJECT);
184 } else {
185 throw new FileNotFoundException("Artifact " + expectedDependency + " not found in Artifactory");
186 }
187 }
188 if (result.isEmpty()) {
189 throw new FileNotFoundException("Artifact " + expectedDependency
190 + " not found in Artifactory; discovered SHA1 hits not recognized as matching Maven artifacts");
191 }
192 return result;
193 }
194
195
196
197
198
199
200
201
202
203
204 private Optional<Matcher> validateUsability(FileImpl file) {
205 final Optional<Matcher> result;
206 if (!checkHashes(file.getChecksums())) {
207 result = Optional.empty();
208 } else {
209 final Matcher pathMatcher = PATH_PATTERN.matcher(file.getPath());
210 if (!pathMatcher.matches()) {
211 LOGGER.debug("Cannot extract the Maven information from the path retrieved in Artifactory {}", file.getPath());
212 result = Optional.empty();
213 } else {
214 result = Optional.of(pathMatcher);
215 }
216 }
217 return result;
218 }
219 }