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) 2018 - 2024 Nicolas Henneaux; Hans Aikema. All Rights Reserved.
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.nio.charset.StandardCharsets;
36  import java.util.ArrayList;
37  import java.util.List;
38  import java.util.Optional;
39  import java.util.regex.Matcher;
40  import java.util.regex.Pattern;
41  
42  class ArtifactorySearchResponseHandler implements HttpClientResponseHandler<List<MavenArtifact>> {
43      /**
44       * Pattern to match the path returned by the Artifactory AQL API.
45       */
46      private static final Pattern PATH_PATTERN = Pattern.compile("^/(?<groupId>.+)/(?<artifactId>[^/]+)/(?<version>[^/]+)/[^/]+$");
47  
48      /**
49       * Used for logging.
50       */
51      private static final Logger LOGGER = LoggerFactory.getLogger(ArtifactorySearchResponseHandler.class);
52  
53      /**
54       * Search result reader
55       */
56      private final ObjectReader fileImplReader;
57  
58      /**
59       * The dependency that is expected to be in the response from Artifactory (if found)
60       */
61      private final Dependency expectedDependency;
62  
63      /**
64       * Creates a responsehandler for the response on a single dependency-search attempt.
65       *
66       * @param dependency The dependency that is expected to be in the response when found (for validating the FileItem(s) in the response)
67       */
68      ArtifactorySearchResponseHandler(Dependency dependency) {
69          this.fileImplReader = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).readerFor(FileImpl.class);
70          this.expectedDependency = dependency;
71      }
72  
73      protected boolean init(JsonParser parser) throws IOException {
74          com.fasterxml.jackson.core.JsonToken nextToken = parser.nextToken();
75          if (nextToken != com.fasterxml.jackson.core.JsonToken.START_OBJECT) {
76              throw new IOException("Expected " + com.fasterxml.jackson.core.JsonToken.START_OBJECT + ", got " + nextToken);
77          }
78  
79          do {
80              nextToken = parser.nextToken();
81              if (nextToken == null) {
82                  break;
83              }
84  
85              if (nextToken.isStructStart()) {
86                  if (nextToken == com.fasterxml.jackson.core.JsonToken.START_ARRAY && "results".equals(parser.currentName())) {
87                      return true;
88                  } else {
89                      parser.skipChildren();
90                  }
91              }
92          } while (true);
93  
94          return false;
95      }
96  
97      /**
98       * Validates the hashes of the dependency.
99       *
100      * @param checksums the collection of checksums (md5, sha1, [sha256])
101      * @return Whether all available hashes match
102      */
103     private boolean checkHashes(ChecksumsImpl checksums) {
104         final String md5sum = expectedDependency.getMd5sum();
105         final String hashMismatchFormat = "Artifact found by API is not matching the {} of the artifact (repository hash is {} while actual is {}) !";
106         boolean match = true;
107         if (!checksums.getMd5().equals(md5sum)) {
108             LOGGER.warn(hashMismatchFormat, "md5", md5sum, checksums.getMd5());
109             match = false;
110         }
111         final String sha1sum = expectedDependency.getSha1sum();
112         if (!checksums.getSha1().equals(sha1sum)) {
113             LOGGER.warn(hashMismatchFormat, "sha1", sha1sum, checksums.getSha1());
114             match = false;
115         }
116         final String sha256sum = expectedDependency.getSha256sum();
117         /* For sha256 we need to validate that the checksum is non-null in the artifactory response.
118          * Extract from Artifactory documentation:
119          * New artifacts that are uploaded to Artifactory 5.5 and later will automatically have their SHA-256 checksum calculated.
120          * However, artifacts that were already hosted in Artifactory before the upgrade will not have their SHA-256 checksum in the database yet.
121          * To make full use of Artifactory's SHA-256 capabilities, you need to Migrate the Database to Include SHA-256 making sure that the record
122          * for each artifact includes its SHA-256 checksum.
123          */
124         if (checksums.getSha256() != null && !checksums.getSha256().equals(sha256sum)) {
125             LOGGER.warn(hashMismatchFormat, "sha256", sha256sum, checksums.getSha256());
126             match = false;
127         }
128         return match;
129     }
130 
131     /**
132      * Process the Artifactory response.
133      *
134      * @param response the HTTP response
135      * @return a list of the Maven Artifact informations that match the searched dependency hash
136      * @throws FileNotFoundException When a matching artifact is not found
137      * @throws IOException           thrown if there is an I/O error
138      */
139     @Override
140     public List<MavenArtifact> handleResponse(ClassicHttpResponse response) throws IOException {
141         final List<MavenArtifact> result = new ArrayList<>();
142 
143         try (InputStreamReader streamReader = new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8);
144              JsonParser parser = fileImplReader.getFactory().createParser(streamReader)) {
145 
146             if (init(parser) && parser.nextToken() == JsonToken.START_OBJECT) {
147                 // at least one result
148                 do {
149                     final FileImpl file = fileImplReader.readValue(parser);
150 
151                     if (file.getChecksums() == null) {
152                         LOGGER.warn("No checksums found in artifactory search result of uri {}. Please make sure that header X-Result-Detail is retained on any (reverse)-proxy, loadbalancer or WebApplicationFirewall in the network path to your Artifactory Server",
153                                 file.getUri());
154                         continue;
155                     }
156 
157                     final Optional<Matcher> validationResult = validateUsability(file);
158                     if (validationResult.isEmpty()) {
159                         continue;
160                     }
161                     final Matcher pathMatcher = validationResult.get();
162 
163                     final String groupId = pathMatcher.group("groupId").replace('/', '.');
164                     final String artifactId = pathMatcher.group("artifactId");
165                     final String version = pathMatcher.group("version");
166 
167                     result.add(new MavenArtifact(groupId, artifactId, version, file.getDownloadUri(),
168                             MavenArtifact.derivePomUrl(artifactId, version, file.getDownloadUri())));
169 
170                 } while (parser.nextToken() == JsonToken.START_OBJECT);
171             } else {
172                 throw new FileNotFoundException("Artifact " + expectedDependency + " not found in Artifactory");
173             }
174         }
175         if (result.isEmpty()) {
176             throw new FileNotFoundException("Artifact " + expectedDependency
177                     + " not found in Artifactory; discovered sha1 hits not recognized as matching maven artifacts");
178         }
179         return result;
180     }
181 
182     /**
183      * Validate the FileImpl result for usability as a dependency.
184      * <br/>
185      * Checks that the actually matches all known hashes and the path appears to match a maven repository G/A/V pattern.
186      *
187      * @param file The FileImpl from an Artifactory search response
188      * @return An Optional with the Matcher for the file path to retrieve the Maven G/A/V coordinates in case result is usable for further
189      * processing, otherwise an empty Optional.
190      */
191     private Optional<Matcher> validateUsability(FileImpl file) {
192         final Optional<Matcher> result;
193         if (!checkHashes(file.getChecksums())) {
194             result = Optional.empty();
195         } else {
196             final Matcher pathMatcher = PATH_PATTERN.matcher(file.getPath());
197             if (!pathMatcher.matches()) {
198                 LOGGER.debug("Cannot extract the Maven information from the path retrieved in Artifactory {}", file.getPath());
199                 result = Optional.empty();
200             } else {
201                 result = Optional.of(pathMatcher);
202             }
203         }
204         return result;
205     }
206 }