ArtifactorySearchResponseHandler.java
/*
* This file is part of dependency-check-core.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Copyright (c) 2018 - 2024 Nicolas Henneaux; Hans Aikema. All Rights Reserved.
*/
package org.owasp.dependencycheck.data.artifactory;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.io.HttpClientResponseHandler;
import org.owasp.dependencycheck.data.nexus.MavenArtifact;
import org.owasp.dependencycheck.dependency.Dependency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class ArtifactorySearchResponseHandler implements HttpClientResponseHandler<List<MavenArtifact>> {
/**
* Pattern to match the path returned by the Artifactory AQL API.
*/
private static final Pattern PATH_PATTERN = Pattern.compile("^/(?<groupId>.+)/(?<artifactId>[^/]+)/(?<version>[^/]+)/[^/]+$");
/**
* Used for logging.
*/
private static final Logger LOGGER = LoggerFactory.getLogger(ArtifactorySearchResponseHandler.class);
/**
* Search result reader
*/
private final ObjectReader fileImplReader;
/**
* The dependency that is expected to be in the response from Artifactory (if found)
*/
private final Dependency expectedDependency;
/**
* Creates a responsehandler for the response on a single dependency-search attempt.
*
* @param dependency The dependency that is expected to be in the response when found (for validating the FileItem(s) in the response)
*/
ArtifactorySearchResponseHandler(Dependency dependency) {
this.fileImplReader = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).readerFor(FileImpl.class);
this.expectedDependency = dependency;
}
protected boolean init(JsonParser parser) throws IOException {
com.fasterxml.jackson.core.JsonToken nextToken = parser.nextToken();
if (nextToken != com.fasterxml.jackson.core.JsonToken.START_OBJECT) {
throw new IOException("Expected " + com.fasterxml.jackson.core.JsonToken.START_OBJECT + ", got " + nextToken);
}
do {
nextToken = parser.nextToken();
if (nextToken == null) {
break;
}
if (nextToken.isStructStart()) {
if (nextToken == com.fasterxml.jackson.core.JsonToken.START_ARRAY && "results".equals(parser.currentName())) {
return true;
} else {
parser.skipChildren();
}
}
} while (true);
return false;
}
/**
* Validates the hashes of the dependency.
*
* @param checksums the collection of checksums (md5, sha1, [sha256])
* @return Whether all available hashes match
*/
private boolean checkHashes(ChecksumsImpl checksums) {
final String md5sum = expectedDependency.getMd5sum();
final String hashMismatchFormat = "Artifact found by API is not matching the {} of the artifact (repository hash is {} while actual is {}) !";
boolean match = true;
if (!checksums.getMd5().equals(md5sum)) {
LOGGER.warn(hashMismatchFormat, "md5", md5sum, checksums.getMd5());
match = false;
}
final String sha1sum = expectedDependency.getSha1sum();
if (!checksums.getSha1().equals(sha1sum)) {
LOGGER.warn(hashMismatchFormat, "sha1", sha1sum, checksums.getSha1());
match = false;
}
final String sha256sum = expectedDependency.getSha256sum();
/* For sha256 we need to validate that the checksum is non-null in the artifactory response.
* Extract from Artifactory documentation:
* New artifacts that are uploaded to Artifactory 5.5 and later will automatically have their SHA-256 checksum calculated.
* However, artifacts that were already hosted in Artifactory before the upgrade will not have their SHA-256 checksum in the database yet.
* 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
* for each artifact includes its SHA-256 checksum.
*/
if (checksums.getSha256() != null && !checksums.getSha256().equals(sha256sum)) {
LOGGER.warn(hashMismatchFormat, "sha256", sha256sum, checksums.getSha256());
match = false;
}
return match;
}
/**
* Process the Artifactory response.
*
* @param response the HTTP response
* @return a list of the Maven Artifact informations that match the searched dependency hash
* @throws FileNotFoundException When a matching artifact is not found
* @throws IOException thrown if there is an I/O error
*/
@Override
public List<MavenArtifact> handleResponse(ClassicHttpResponse response) throws IOException {
final List<MavenArtifact> result = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(response.getEntity().getContent(), StandardCharsets.UTF_8);
JsonParser parser = fileImplReader.getFactory().createParser(streamReader)) {
if (init(parser) && parser.nextToken() == JsonToken.START_OBJECT) {
// at least one result
do {
final FileImpl file = fileImplReader.readValue(parser);
if (file.getChecksums() == null) {
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",
file.getUri());
continue;
}
final Optional<Matcher> validationResult = validateUsability(file);
if (validationResult.isEmpty()) {
continue;
}
final Matcher pathMatcher = validationResult.get();
final String groupId = pathMatcher.group("groupId").replace('/', '.');
final String artifactId = pathMatcher.group("artifactId");
final String version = pathMatcher.group("version");
result.add(new MavenArtifact(groupId, artifactId, version, file.getDownloadUri(),
MavenArtifact.derivePomUrl(artifactId, version, file.getDownloadUri())));
} while (parser.nextToken() == JsonToken.START_OBJECT);
} else {
throw new FileNotFoundException("Artifact " + expectedDependency + " not found in Artifactory");
}
}
if (result.isEmpty()) {
throw new FileNotFoundException("Artifact " + expectedDependency
+ " not found in Artifactory; discovered sha1 hits not recognized as matching maven artifacts");
}
return result;
}
/**
* Validate the FileImpl result for usability as a dependency.
* <br/>
* Checks that the actually matches all known hashes and the path appears to match a maven repository G/A/V pattern.
*
* @param file The FileImpl from an Artifactory search response
* @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
* processing, otherwise an empty Optional.
*/
private Optional<Matcher> validateUsability(FileImpl file) {
final Optional<Matcher> result;
if (!checkHashes(file.getChecksums())) {
result = Optional.empty();
} else {
final Matcher pathMatcher = PATH_PATTERN.matcher(file.getPath());
if (!pathMatcher.matches()) {
LOGGER.debug("Cannot extract the Maven information from the path retrieved in Artifactory {}", file.getPath());
result = Optional.empty();
} else {
result = Optional.of(pathMatcher);
}
}
return result;
}
}