From 3c7ad3c1c85947933126fdf2fe7126f5a43c4d9e Mon Sep 17 00:00:00 2001 From: Ruben Romero Montes Date: Thu, 3 Aug 2023 20:29:24 +0200 Subject: [PATCH] feat: update data model for multi-source Signed-off-by: Ruben Romero Montes --- README.md | 69 +- api-spec/v3/openapi.yaml | 40 +- pom.xml | 3 +- src/main/docker/Dockerfile.multi-stage | 4 +- .../redhat/exhort/integration/Constants.java | 1 + .../ProviderAggregationStrategy.java | 73 --- .../integration/backend/BackendUtils.java | 80 ++- .../backend/ExhortIntegration.java | 23 +- .../backend/sbom/spdx/SpdxParser.java | 3 +- .../ProviderAggregationStrategy.java | 37 ++ .../providers/ProviderAggregator.java | 188 ++++++ .../VulnerabilityProvider.java | 4 +- .../ossindex/OssIndexAggregator.java} | 36 +- .../ossindex/OssIndexIntegration.java | 28 +- .../ossindex/OssIndexRequestBuilder.java | 12 +- .../snyk/SnykAggregator.java} | 74 +-- .../{ => providers}/snyk/SnykIntegration.java | 24 +- .../snyk/SnykRequestBuilder.java | 2 +- .../integration/report/ReportIntegration.java | 2 - .../integration/report/ReportTemplate.java | 4 +- .../integration/report/ReportTransformer.java | 190 +----- .../TrustedContentBodyMapper.java | 155 ++++- .../TrustedContentIntegration.java | 79 +-- .../exhort/model/CvssScoreComparable.java | 12 +- .../redhat/exhort/model/DirectDependency.java | 11 +- .../com/redhat/exhort/model/GraphRequest.java | 58 +- .../resources/freemarker/templates/report.ftl | 53 +- .../exhort/integration/AnalysisTest.java | 68 +- .../VulnerabilityProviderTest.java | 2 + .../integration/backend/BackendUtilsTest.java | 212 +++++++ .../integration/backend/IssueTestUtils.java | 34 + .../backend/ProviderAggregatorTest.java | 206 ++++++ .../report/ReportTransformerTest.java | 183 ------ .../reports/report_all_no_snyk_token.html | 38 +- .../reports/report_all_no_snyk_token.json | 376 ++++++----- .../__files/reports/report_all_token.html | 148 +---- .../__files/reports/report_all_token.json | 593 ++++++++++-------- .../__files/reports/report_error.html | 1 + .../__files/reports/report_forbidden.html | 1 + .../__files/reports/report_unauthorized.html | 1 + .../trustedcontent/oss_vulns_vex_request.json | 4 +- .../trustedcontent/short_vex_report.json | 23 +- src/test/resources/application.properties | 2 +- 43 files changed, 1684 insertions(+), 1473 deletions(-) delete mode 100644 src/main/java/com/redhat/exhort/integration/ProviderAggregationStrategy.java create mode 100644 src/main/java/com/redhat/exhort/integration/providers/ProviderAggregationStrategy.java create mode 100644 src/main/java/com/redhat/exhort/integration/providers/ProviderAggregator.java rename src/main/java/com/redhat/exhort/integration/{ => providers}/VulnerabilityProvider.java (97%) rename src/main/java/com/redhat/exhort/integration/{ossindex/OssIndexAggregationStrategy.java => providers/ossindex/OssIndexAggregator.java} (61%) rename src/main/java/com/redhat/exhort/integration/{ => providers}/ossindex/OssIndexIntegration.java (85%) rename src/main/java/com/redhat/exhort/integration/{ => providers}/ossindex/OssIndexRequestBuilder.java (91%) rename src/main/java/com/redhat/exhort/integration/{snyk/SnykAggregationStrategy.java => providers/snyk/SnykAggregator.java} (62%) rename src/main/java/com/redhat/exhort/integration/{ => providers}/snyk/SnykIntegration.java (91%) rename src/main/java/com/redhat/exhort/integration/{ => providers}/snyk/SnykRequestBuilder.java (98%) create mode 100644 src/test/java/com/redhat/exhort/integration/backend/BackendUtilsTest.java create mode 100644 src/test/java/com/redhat/exhort/integration/backend/IssueTestUtils.java create mode 100644 src/test/java/com/redhat/exhort/integration/backend/ProviderAggregatorTest.java delete mode 100644 src/test/java/com/redhat/exhort/integration/report/ReportTransformerTest.java diff --git a/README.md b/README.md index 516c7274..1849bb80 100644 --- a/README.md +++ b/README.md @@ -79,19 +79,29 @@ in order to retrieve just a Summary. Use the `verbose=false` Query parameter to $ http :8080/api/v3/analysis Content-Type:"application/vnd.cyclonedx+json" Accept:"application/json" @'target/sbom.json' verbose==false { - "dependencies": [], - "summary": { - "dependencies": { - "scanned": 11, - "transitive": 217 - }, - "vulnerabilities": { - "critical": 1, - "direct": 6, - "high": 4, - "low": 5, - "medium": 10, - "total": 20 + "snyk": { + "status": { + "name": "snyk", + "code": 200, + "message": "OK", + "ok": true + } + "dependencies": [ + ... + ], + "summary": { + "dependencies": { + "scanned": 11, + "transitive": 217 + }, + "vulnerabilities": { + "critical": 1, + "direct": 6, + "high": 4, + "low": 5, + "medium": 10, + "total": 20 + } } } } @@ -154,23 +164,28 @@ Content-Type: application/json Content-Transfer-Encoding: binary { - "summary": { - "dependencies": { - ... - }, - "vulnerabilities": { + "snyk": { + "status": { ... } - }, - "dependencies": [ - { - "ref": { - "name": "log4j:log4j", - "version": "1.2.17" + "summary": { + "dependencies": { + ... + }, + "vulnerabilities": { + ... + } }, - ... - } - ] + "dependencies": [ + { + "ref": { + "name": "log4j:log4j", + "version": "1.2.17" + }, + ... + } + ] + } } ------=_Part_2_2047647971.1682593849895 Content-Type: text/html diff --git a/api-spec/v3/openapi.yaml b/api-spec/v3/openapi.yaml index a67091e2..b2a1a7ec 100644 --- a/api-spec/v3/openapi.yaml +++ b/api-spec/v3/openapi.yaml @@ -46,7 +46,7 @@ paths: schema: type: object responses: - '200': + 200: description: Full dependency analysis from all the available providers content: application/json: @@ -54,15 +54,15 @@ paths: $ref: '#/components/schemas/AnalysisReport' text/html: schema: - type: object + type: string multipart/mixed: schema: type: object properties: json_report: - $ref: '#/components/schemas/AnalysisReport' - html_report: type: object + html_report: + type: string '422': description: Invalid request content: @@ -132,13 +132,17 @@ components: schemas: AnalysisReport: type: object - properties: - summary: - $ref: '#/components/schemas/Summary' - dependencies: - type: array - items: - $ref: '#/components/schemas/DependencyReport' + additionalProperties: + type: object + properties: + status: + $ref: '#/components/schemas/ProviderStatus' + summary: + $ref: '#/components/schemas/Summary' + dependencies: + type: array + items: + $ref: '#/components/schemas/DependencyReport' PackageRef: type: object description: PackageURL used to identify a dependency artifact @@ -173,7 +177,6 @@ components: issues: - id: SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426 title: Denial of Service (DoS) - source: snyk cvss: attackVector: Network attackComplexity: High @@ -196,7 +199,6 @@ components: issues: - id: SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426 title: Denial of Service (DoS) - source: snyk cvss: attackVector: Network attackComplexity: High @@ -233,8 +235,6 @@ components: type: string title: type: string - source: - type: string cvss: $ref: '#/components/schemas/CvssVector' cvssScore: @@ -252,7 +252,6 @@ components: example: id: SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426 title: Denial of Service (DoS) - source: snyk cvss: attackVector: Network attackComplexity: High @@ -291,7 +290,6 @@ components: issues: - id: SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426 title: Denial of Service (DoS) - source: snyk cvss: attackVector: Network attackComplexity: High @@ -329,10 +327,6 @@ components: $ref: '#/components/schemas/DependenciesSummary' vulnerabilities: $ref: '#/components/schemas/VulnerabilitiesSummary' - providerStatuses: - type: array - items: - $ref: '#/components/schemas/ProviderStatus' DependenciesSummary: type: object properties: @@ -360,9 +354,9 @@ components: properties: ok: type: boolean - provider: + name: type: string - status: + code: type: integer message: type: string diff --git a/pom.xml b/pom.xml index 863a58b7..3c60b2a1 100644 --- a/pom.xml +++ b/pom.xml @@ -334,9 +334,10 @@ false false false + true false false - analysis_200_response=AnalysisResponse + analysis_200_response=MultipartResponse,analysis_report_value=ProviderReport PackageRef=com.redhat.exhort.api.PackageRef diff --git a/src/main/docker/Dockerfile.multi-stage b/src/main/docker/Dockerfile.multi-stage index 22965861..bdf497b6 100644 --- a/src/main/docker/Dockerfile.multi-stage +++ b/src/main/docker/Dockerfile.multi-stage @@ -1,8 +1,10 @@ ## Stage 1 : build with maven builder image with native capabilities -FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:22.3-java17 AS build +FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-17 AS build COPY --chown=quarkus:quarkus mvnw /code/mvnw COPY --chown=quarkus:quarkus .mvn /code/.mvn COPY --chown=quarkus:quarkus pom.xml /code/ +COPY --chown=quarkus:quarkus api-spec /code/api-spec + USER quarkus WORKDIR /code RUN ./mvnw -B org.apache.maven.plugins:maven-dependency-plugin:3.1.2:go-offline diff --git a/src/main/java/com/redhat/exhort/integration/Constants.java b/src/main/java/com/redhat/exhort/integration/Constants.java index c1a2653b..1cedb8ad 100644 --- a/src/main/java/com/redhat/exhort/integration/Constants.java +++ b/src/main/java/com/redhat/exhort/integration/Constants.java @@ -57,6 +57,7 @@ private Constants() {} public static final String PKG_MANAGER_PROPERTY = "pkgManager"; public static final String REQUEST_CONTENT_PROPERTY = "requestContent"; public static final String REPORT_PROPERTY = "report"; + public static final String DEPENDENCY_TREE_PROPERTY = "dependencyTree"; public static final String PROVIDER_PRIVATE_DATA_PROPERTY = "providerPrivateData"; public static final String RESPONSE_STATUS_PROPERTY = "responseStatus"; diff --git a/src/main/java/com/redhat/exhort/integration/ProviderAggregationStrategy.java b/src/main/java/com/redhat/exhort/integration/ProviderAggregationStrategy.java deleted file mode 100644 index ba65376e..00000000 --- a/src/main/java/com/redhat/exhort/integration/ProviderAggregationStrategy.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2023 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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. - */ - -package com.redhat.exhort.integration; - -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.redhat.exhort.api.Issue; -import com.redhat.exhort.api.PackageRef; -import com.redhat.exhort.api.ProviderStatus; -import com.redhat.exhort.api.Remediation; -import com.redhat.exhort.model.GraphRequest; - -import io.quarkus.runtime.annotations.RegisterForReflection; - -@RegisterForReflection -public class ProviderAggregationStrategy { - - public GraphRequest aggregate(GraphRequest oldRequest, GraphRequest newRequest) { - if (oldRequest == null) { - return newRequest; - } - Map> issues = new HashMap<>(oldRequest.issues()); - newRequest - .issues() - .entrySet() - .forEach( - e -> { - List newIssues = issues.get(e.getKey()); - if (newIssues == null) { - newIssues = List.copyOf(e.getValue()); - issues.put(e.getKey(), newIssues); - } else { - newIssues.addAll(e.getValue()); - } - }); - - Map remediations = new HashMap<>(oldRequest.remediations()); - remediations.putAll(newRequest.remediations()); - - Map recommendations = new HashMap<>(oldRequest.recommendations()); - recommendations.putAll(newRequest.recommendations()); - - Set providerStatuses = new HashSet<>(oldRequest.providerStatuses()); - providerStatuses.addAll(newRequest.providerStatuses()); - - return new GraphRequest.Builder(oldRequest) - .issues(issues) - .remediations(remediations) - .recommendations(recommendations) - .providerStatuses(List.copyOf(providerStatuses)) - .build(); - } -} diff --git a/src/main/java/com/redhat/exhort/integration/backend/BackendUtils.java b/src/main/java/com/redhat/exhort/integration/backend/BackendUtils.java index 07ee844a..d64d1005 100644 --- a/src/main/java/com/redhat/exhort/integration/backend/BackendUtils.java +++ b/src/main/java/com/redhat/exhort/integration/backend/BackendUtils.java @@ -18,14 +18,24 @@ package com.redhat.exhort.integration.backend; +import java.util.Collections; import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import org.apache.camel.Body; import org.apache.camel.Exchange; import org.apache.camel.Header; import org.apache.camel.http.base.HttpOperationFailedException; import org.jboss.resteasy.reactive.common.util.MediaTypeHelper; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.api.AnalysisReportValue; +import com.redhat.exhort.api.DependenciesSummary; +import com.redhat.exhort.api.DependencyReport; import com.redhat.exhort.api.ProviderStatus; +import com.redhat.exhort.api.Summary; +import com.redhat.exhort.api.VulnerabilitiesSummary; import com.redhat.exhort.integration.Constants; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -56,26 +66,26 @@ public String getResponseMediaType(@Header(Constants.ACCEPT_HEADER) String accep } public static void processResponseError(Exchange exchange, String provider) { - ProviderStatus status = new ProviderStatus().ok(false).provider(provider); + ProviderStatus status = new ProviderStatus().ok(false).name(provider); Exception exception = (Exception) exchange.getProperty(Exchange.EXCEPTION_CAUGHT); Throwable cause = exception.getCause(); if (cause != null) { if (cause instanceof HttpOperationFailedException) { HttpOperationFailedException httpException = (HttpOperationFailedException) cause; - status.message(prettifyHttpError(httpException)).status(httpException.getStatusCode()); + status.message(prettifyHttpError(httpException)).code(httpException.getStatusCode()); } else { status .message(cause.getMessage()) - .status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + .code(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); } } else { status .message(exception.getMessage()) - .status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); + .code(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); } - exchange.getMessage().setBody(status); + exchange.getMessage().setBody(newEmptyReportValue(status)); } public static void processTokenFallBack(Exchange exchange, String provider) { @@ -99,6 +109,31 @@ public static void processTokenFallBack(Exchange exchange, String provider) { exchange.getMessage().setBody(body); } + public static AnalysisReport removeEmptyDependencies(@Body AnalysisReport reports) { + AnalysisReport result = new AnalysisReport(); + reports + .entrySet() + .forEach( + entry -> { + ProviderStatus status = entry.getValue().getStatus(); + if (entry.getValue().getDependencies() == null) { + entry.getValue().dependencies(Collections.emptyList()); + } + List filteredDeps = + entry.getValue().getDependencies().stream() + .map(BackendUtils::removeEmptyTransitive) + .filter(Predicate.not(BackendUtils::filterDependency)) + .collect(Collectors.toList()); + result.put( + entry.getKey(), + new AnalysisReportValue() + .status(status) + .summary(entry.getValue().getSummary()) + .dependencies(filteredDeps)); + }); + return result; + } + private static String prettifyHttpError(HttpOperationFailedException httpException) { String text = httpException.getStatusText(); switch (httpException.getStatusCode()) { @@ -112,4 +147,39 @@ private static String prettifyHttpError(HttpOperationFailedException httpExcepti return text; } } + + private static boolean filterDependency(DependencyReport report) { + if (report.getRecommendation() != null) { + return false; + } + if (report.getHighestVulnerability() == null) { + return true; + } + boolean hasIssues = report.getIssues() != null && !report.getIssues().isEmpty(); + boolean hasTransitiveIssues = + report.getTransitive() != null && !report.getTransitive().isEmpty(); + + return !hasIssues && !hasTransitiveIssues; + } + + private static DependencyReport removeEmptyTransitive(DependencyReport report) { + if (report.getTransitive() == null) { + return report; + } + report.setTransitive( + report.getTransitive().stream() + .filter(t -> t.getIssues() != null && !t.getIssues().isEmpty()) + .collect(Collectors.toList())); + return report; + } + + private static AnalysisReportValue newEmptyReportValue(ProviderStatus status) { + return new AnalysisReportValue() + .status(status) + .dependencies(Collections.emptyList()) + .summary( + new Summary() + .dependencies(new DependenciesSummary()) + .vulnerabilities(new VulnerabilitiesSummary())); + } } diff --git a/src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java b/src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java index 7e72466c..e1d6928e 100644 --- a/src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java @@ -18,6 +18,8 @@ package com.redhat.exhort.integration.backend; +import static com.redhat.exhort.integration.Constants.DEPENDENCY_TREE_PROPERTY; +import static com.redhat.exhort.integration.Constants.PROVIDERS_PARAM; import static com.redhat.exhort.integration.Constants.REQUEST_CONTENT_PROPERTY; import java.io.InputStream; @@ -31,10 +33,10 @@ import org.apache.camel.component.micrometer.routepolicy.MicrometerRoutePolicyFactory; import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.integration.ProviderAggregationStrategy; -import com.redhat.exhort.integration.VulnerabilityProvider; import com.redhat.exhort.integration.backend.sbom.SbomParser; import com.redhat.exhort.integration.backend.sbom.SbomParserFactory; +import com.redhat.exhort.integration.providers.ProviderAggregationStrategy; +import com.redhat.exhort.integration.providers.VulnerabilityProvider; import com.redhat.exhort.model.DependencyTree; import com.redhat.exhort.model.GraphRequest; @@ -88,22 +90,27 @@ public void configure() { .to("direct:analysis") .get("/token") .routeId("restTokenValidation") - .to("direct:validateToken"); + .to("direct:validateToken") + .get("/test").to("direct:testFreemarker"); from(direct("analysis")) - .routeId("dependencyAnalysis") - .setProperty(Constants.PROVIDERS_PARAM, method(vulnerabilityProvider, "getProvidersFromQueryParam")) + .routeId("analysis") + .setProperty(PROVIDERS_PARAM, method(vulnerabilityProvider, "getProvidersFromQueryParam")) .setProperty(REQUEST_CONTENT_PROPERTY, method(BackendUtils.class, "getResponseMediaType")) + .setProperty(Constants.VERBOSE_MODE_HEADER, header(Constants.VERBOSE_MODE_HEADER)) .process(this::processAnalysisRequest) + .setProperty(DEPENDENCY_TREE_PROPERTY, simple("${body.tree}")) .to(direct("findVulnerabilities")) - .to(direct("recommendAllTrustedContent")) + .to(direct("findRemediations")) + .to(direct("recommendTrustedContent")) + .transform().method(BackendUtils.class, "removeEmptyDependencies") .to(direct("report")) .process(this::cleanUpHeaders); from(direct("findVulnerabilities")) .routeId("findVulnerabilities") .recipientList(method(vulnerabilityProvider, "getProviderEndpoints")) - .aggregationStrategy(AggregationStrategies.bean(ProviderAggregationStrategy.class, "aggregate")) + .aggregationStrategy(AggregationStrategies.beanAllowNull(ProviderAggregationStrategy.class, "aggregate")) .parallelProcessing(); from(direct("validateToken")) @@ -135,7 +142,7 @@ private void processAnalysisRequest(Exchange exchange) { } SbomParser parser = SbomParserFactory.newInstance(ct.getBaseType()); DependencyTree tree = parser.parse(exchange.getIn().getBody(InputStream.class)); - List providers = exchange.getProperty(Constants.PROVIDERS_PARAM, List.class); + List providers = exchange.getProperty(PROVIDERS_PARAM, List.class); exchange .getIn() .setBody( diff --git a/src/main/java/com/redhat/exhort/integration/backend/sbom/spdx/SpdxParser.java b/src/main/java/com/redhat/exhort/integration/backend/sbom/spdx/SpdxParser.java index 52acf055..14a33bcd 100644 --- a/src/main/java/com/redhat/exhort/integration/backend/sbom/spdx/SpdxParser.java +++ b/src/main/java/com/redhat/exhort/integration/backend/sbom/spdx/SpdxParser.java @@ -120,7 +120,8 @@ private Map buildDeps(SpdxWrapper wrapper) { .map(wrapper::getPackageById) .map(wrapper::toPackageRef) .collect(Collectors.toSet()); - DirectDependency dep = new DirectDependency(ref, transitive); + DirectDependency dep = + DirectDependency.builder().ref(ref).transitive(transitive).build(); deps.put(ref, dep); }); return deps; diff --git a/src/main/java/com/redhat/exhort/integration/providers/ProviderAggregationStrategy.java b/src/main/java/com/redhat/exhort/integration/providers/ProviderAggregationStrategy.java new file mode 100644 index 00000000..181d27fb --- /dev/null +++ b/src/main/java/com/redhat/exhort/integration/providers/ProviderAggregationStrategy.java @@ -0,0 +1,37 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package com.redhat.exhort.integration.providers; + +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.api.AnalysisReportValue; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +@RegisterForReflection +public class ProviderAggregationStrategy { + + public AnalysisReport aggregate(AnalysisReport response, AnalysisReportValue reportValue) { + + if (response == null) { + response = new AnalysisReport(); + } + response.put(reportValue.getStatus().getName(), reportValue); + return response; + } +} diff --git a/src/main/java/com/redhat/exhort/integration/providers/ProviderAggregator.java b/src/main/java/com/redhat/exhort/integration/providers/ProviderAggregator.java new file mode 100644 index 00000000..8e87a9c2 --- /dev/null +++ b/src/main/java/com/redhat/exhort/integration/providers/ProviderAggregator.java @@ -0,0 +1,188 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package com.redhat.exhort.integration.providers; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; + +import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; + +import com.redhat.exhort.api.AnalysisReportValue; +import com.redhat.exhort.api.DependenciesSummary; +import com.redhat.exhort.api.DependencyReport; +import com.redhat.exhort.api.Issue; +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.api.ProviderStatus; +import com.redhat.exhort.api.Summary; +import com.redhat.exhort.api.TransitiveDependencyReport; +import com.redhat.exhort.api.VulnerabilitiesSummary; +import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.model.CvssScoreComparable.DependencyScoreComparator; +import com.redhat.exhort.model.CvssScoreComparable.TransitiveScoreComparator; +import com.redhat.exhort.model.DependencyTree; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import jakarta.ws.rs.core.Response; + +@RegisterForReflection +public abstract class ProviderAggregator { + + protected abstract String getProviderName(); + + protected ProviderStatus defaultOkStatus(String provider) { + return new ProviderStatus() + .name(provider) + .ok(Boolean.TRUE) + .message("OK") + .code(Response.Status.OK.getStatusCode()); + } + + protected DependencyReport toDependencyReport(PackageRef ref, List issues) { + return new DependencyReport() + .ref(ref) + .issues( + issues.stream() + .sorted(Comparator.comparing(Issue::getCvssScore).reversed()) + .collect(Collectors.toList())); + } + + public AnalysisReportValue buildReport( + @Body Map> issuesData, + @ExchangeProperty(Constants.DEPENDENCY_TREE_PROPERTY) DependencyTree tree) { + AnalysisReportValue report = new AnalysisReportValue(); + DependenciesSummary deps = + new DependenciesSummary().scanned(tree.directCount()).transitive(tree.transitiveCount()); + VulnerabilityCounter counter = new VulnerabilityCounter(); + report.status(defaultOkStatus(getProviderName())).dependencies(new ArrayList<>()); + tree.dependencies().entrySet().stream() + .forEach( + e -> { + String ref = e.getKey().name(); + List issues = issuesData.get(ref); + DependencyReport directReport = new DependencyReport().ref(e.getKey()); + if (issues == null) { + issues = Collections.emptyList(); + } + directReport.issues( + issues.stream() + .sorted(Comparator.comparing(Issue::getCvssScore).reversed()) + .collect(Collectors.toList())); + directReport.setHighestVulnerability(issues.stream().findFirst().orElse(null)); + directReport + .getIssues() + .forEach( + i -> { + incrementCounter(i, counter); + counter.direct.incrementAndGet(); + }); + + List transitiveReports = + e.getValue().transitive().stream() + .map( + t -> { + List transitiveIssues = Collections.emptyList(); + String tRef = t.name(); + if (issuesData.get(tRef) != null) { + transitiveIssues = + issuesData.get(tRef).stream() + .sorted(Comparator.comparing(Issue::getCvssScore).reversed()) + .collect(Collectors.toList()); + transitiveIssues.forEach(i -> incrementCounter(i, counter)); + } + Optional highestTransitive = + transitiveIssues.stream().findFirst(); + if (highestTransitive.isPresent()) { + if (directReport.getHighestVulnerability() == null + || directReport.getHighestVulnerability().getCvssScore() + < highestTransitive.get().getCvssScore()) { + directReport.setHighestVulnerability(highestTransitive.get()); + } + } + return new TransitiveDependencyReport() + .ref(t) + .issues(transitiveIssues) + .highestVulnerability(highestTransitive.orElse(null)); + }) + .collect(Collectors.toList()); + transitiveReports.sort(new TransitiveScoreComparator()); + directReport.setTransitive(transitiveReports); + report.addDependenciesItem(directReport); + }); + report.dependencies( + report.getDependencies().stream() + .sorted(new DependencyScoreComparator()) + .collect(Collectors.toList())); + return report.summary(new Summary().dependencies(deps).vulnerabilities(counter.getSummary())); + } + + private void incrementCounter(Issue i, VulnerabilityCounter counter) { + switch (i.getSeverity()) { + case CRITICAL: + counter.critical.incrementAndGet(); + break; + case HIGH: + counter.high.incrementAndGet(); + break; + case MEDIUM: + counter.medium.incrementAndGet(); + break; + case LOW: + counter.low.incrementAndGet(); + break; + } + counter.total.incrementAndGet(); + } + + private static final record VulnerabilityCounter( + AtomicInteger total, + AtomicInteger direct, + AtomicInteger critical, + AtomicInteger high, + AtomicInteger medium, + AtomicInteger low) { + + VulnerabilityCounter() { + this( + new AtomicInteger(), + new AtomicInteger(), + new AtomicInteger(), + new AtomicInteger(), + new AtomicInteger(), + new AtomicInteger()); + } + + VulnerabilitiesSummary getSummary() { + return new VulnerabilitiesSummary() + .total(total.get()) + .direct(direct.get()) + .critical(critical.get()) + .high(high.get()) + .medium(medium.get()) + .low(low.get()); + } + } +} diff --git a/src/main/java/com/redhat/exhort/integration/VulnerabilityProvider.java b/src/main/java/com/redhat/exhort/integration/providers/VulnerabilityProvider.java similarity index 97% rename from src/main/java/com/redhat/exhort/integration/VulnerabilityProvider.java rename to src/main/java/com/redhat/exhort/integration/providers/VulnerabilityProvider.java index cc1141b8..2374e5cf 100644 --- a/src/main/java/com/redhat/exhort/integration/VulnerabilityProvider.java +++ b/src/main/java/com/redhat/exhort/integration/providers/VulnerabilityProvider.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package com.redhat.exhort.integration; +package com.redhat.exhort.integration.providers; import java.net.URISyntaxException; import java.util.ArrayList; @@ -32,6 +32,8 @@ import org.apache.camel.util.URISupport; import org.eclipse.microprofile.config.inject.ConfigProperty; +import com.redhat.exhort.integration.Constants; + import io.quarkus.runtime.annotations.RegisterForReflection; import jakarta.annotation.PostConstruct; diff --git a/src/main/java/com/redhat/exhort/integration/ossindex/OssIndexAggregationStrategy.java b/src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexAggregator.java similarity index 61% rename from src/main/java/com/redhat/exhort/integration/ossindex/OssIndexAggregationStrategy.java rename to src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexAggregator.java index 940a8673..e9c612e5 100644 --- a/src/main/java/com/redhat/exhort/integration/ossindex/OssIndexAggregationStrategy.java +++ b/src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexAggregator.java @@ -16,28 +16,23 @@ * limitations under the License. */ -package com.redhat.exhort.integration.ossindex; +package com.redhat.exhort.integration.providers.ossindex; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.redhat.exhort.api.Issue; import com.redhat.exhort.api.PackageRef; -import com.redhat.exhort.api.ProviderStatus; import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.integration.providers.ProviderAggregator; import com.redhat.exhort.model.GraphRequest; import io.quarkus.runtime.annotations.RegisterForReflection; -import jakarta.ws.rs.core.Response; - @RegisterForReflection -public class OssIndexAggregationStrategy { +public class OssIndexAggregator extends ProviderAggregator { private static final int BULK_SIZE = 128; @@ -71,27 +66,8 @@ public Map> aggregateSplit( return newExchange; } - @SuppressWarnings("unchecked") - public GraphRequest aggregate(GraphRequest graphReq, Object newExchange) - throws JsonMappingException, JsonProcessingException { - GraphRequest.Builder builder = new GraphRequest.Builder(graphReq); - if (newExchange instanceof ProviderStatus) { - return builder.providerStatuses(List.of((ProviderStatus) newExchange)).build(); - } - - Map> issues; - if (newExchange instanceof List) { - issues = Collections.emptyMap(); - } else { - issues = (Map>) newExchange; - } - - ProviderStatus status = - new ProviderStatus() - .ok(true) - .provider(Constants.OSS_INDEX_PROVIDER) - .status(Response.Status.OK.getStatusCode()) - .message(Response.Status.OK.name()); - return builder.issues(issues).providerStatuses(List.of(status)).build(); + @Override + protected final String getProviderName() { + return Constants.OSS_INDEX_PROVIDER; } } diff --git a/src/main/java/com/redhat/exhort/integration/ossindex/OssIndexIntegration.java b/src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexIntegration.java similarity index 85% rename from src/main/java/com/redhat/exhort/integration/ossindex/OssIndexIntegration.java rename to src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexIntegration.java index 7b8818a7..9e20fe33 100644 --- a/src/main/java/com/redhat/exhort/integration/ossindex/OssIndexIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexIntegration.java @@ -16,9 +16,10 @@ * limitations under the License. */ -package com.redhat.exhort.integration.ossindex; +package com.redhat.exhort.integration.providers.ossindex; import java.util.Base64; +import java.util.Collections; import java.util.List; import java.util.Objects; @@ -29,8 +30,8 @@ import org.eclipse.microprofile.config.inject.ConfigProperty; import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.integration.VulnerabilityProvider; import com.redhat.exhort.integration.backend.BackendUtils; +import com.redhat.exhort.integration.providers.VulnerabilityProvider; import com.redhat.exhort.model.DependencyTree; import jakarta.enterprise.context.ApplicationScoped; @@ -52,28 +53,35 @@ public void configure() { // fmt:off from(direct("ossIndexScan")) .routeId("ossIndexScan") - .enrich(direct("ossIndexRequest"), AggregationStrategies.bean(OssIndexAggregationStrategy.class, "aggregate")); - - from(direct("ossIndexRequest")) - .routeId("ossIndexRequest") .circuitBreaker() .faultToleranceConfiguration() .timeoutEnabled(true) .timeoutDuration(timeout) .end() - .to(direct("ossSplitReq")) + .to(direct("ossIndexRequest")) .onFallback() .process(e -> BackendUtils.processResponseError(e, Constants.OSS_INDEX_PROVIDER)); + from(direct("ossIndexRequest")) + .routeId("ossIndexRequest") + .choice() + .when(method(OssIndexRequestBuilder.class, "hasDependencies")) + .to(direct("ossSplitReq")) + .otherwise() + .to("log:foo?showBody=true") + .setBody(constant(Collections.emptyMap())) + .end() + .transform().method(OssIndexAggregator.class, "buildReport"); + from(direct("ossSplitReq")) .routeId("ossSplitReq") - .transform(method(OssIndexAggregationStrategy.class, "split")) - .split(body(), AggregationStrategies.bean(OssIndexAggregationStrategy.class, "aggregateSplit")) + .transform().method(OssIndexAggregator.class, "split") + .split(body(), AggregationStrategies.bean(OssIndexAggregator.class, "aggregateSplit")) .parallelProcessing() .transform().method(OssIndexRequestBuilder.class, "buildRequest") .process(this::processComponentRequest) .to(vertxHttp("{{api.ossindex.host}}")) - .transform(method(OssIndexRequestBuilder.class, "responseToIssues")); + .transform().method(OssIndexRequestBuilder.class, "responseToIssues"); from(direct("ossValidateCredentials")) .routeId("ossValidateCredentials") diff --git a/src/main/java/com/redhat/exhort/integration/ossindex/OssIndexRequestBuilder.java b/src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexRequestBuilder.java similarity index 91% rename from src/main/java/com/redhat/exhort/integration/ossindex/OssIndexRequestBuilder.java rename to src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexRequestBuilder.java index 9cf5d5cb..fbec3cf8 100644 --- a/src/main/java/com/redhat/exhort/integration/ossindex/OssIndexRequestBuilder.java +++ b/src/main/java/com/redhat/exhort/integration/providers/ossindex/OssIndexRequestBuilder.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package com.redhat.exhort.integration.ossindex; +package com.redhat.exhort.integration.providers.ossindex; import java.io.IOException; import java.util.ArrayList; @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Objects; +import org.apache.camel.Body; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,8 +38,8 @@ import com.redhat.exhort.api.PackageRef; import com.redhat.exhort.api.SeverityUtils; import com.redhat.exhort.config.ObjectMapperProducer; -import com.redhat.exhort.integration.Constants; import com.redhat.exhort.model.CvssParser; +import com.redhat.exhort.model.GraphRequest; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -60,6 +61,10 @@ public String buildRequest(List packages) throws JsonProcessingExcep return mapper.writeValueAsString(root); } + public boolean hasDependencies(@Body GraphRequest req) { + return req != null && req.tree() != null && !req.tree().getAll().isEmpty(); + } + public Map> responseToIssues(byte[] response) throws IOException { ArrayNode json = (ArrayNode) mapper.readTree(response); return getIssues(json); @@ -94,7 +99,6 @@ private Issue toIssue(JsonNode data) { .cves(List.of(data.get("cve").asText())) .cvss(CvssParser.fromVectorString(data.get("cvssVector").asText())) .cvssScore(score) - .severity(SeverityUtils.fromScore(score)) - .source(Constants.OSS_INDEX_PROVIDER); + .severity(SeverityUtils.fromScore(score)); } } diff --git a/src/main/java/com/redhat/exhort/integration/snyk/SnykAggregationStrategy.java b/src/main/java/com/redhat/exhort/integration/providers/snyk/SnykAggregator.java similarity index 62% rename from src/main/java/com/redhat/exhort/integration/snyk/SnykAggregationStrategy.java rename to src/main/java/com/redhat/exhort/integration/providers/snyk/SnykAggregator.java index c3593503..fbcf9809 100644 --- a/src/main/java/com/redhat/exhort/integration/snyk/SnykAggregationStrategy.java +++ b/src/main/java/com/redhat/exhort/integration/providers/snyk/SnykAggregator.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package com.redhat.exhort.integration.snyk; +package com.redhat.exhort.integration.providers.snyk; import java.io.IOException; import java.util.ArrayList; @@ -24,76 +24,29 @@ import java.util.List; import java.util.Map; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.redhat.exhort.api.Issue; -import com.redhat.exhort.api.ProviderStatus; import com.redhat.exhort.api.SeverityUtils; import com.redhat.exhort.config.ObjectMapperProducer; import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.integration.providers.ProviderAggregator; import com.redhat.exhort.model.CvssParser; -import com.redhat.exhort.model.GraphRequest; import io.quarkus.runtime.annotations.RegisterForReflection; -import jakarta.ws.rs.core.Response; - @RegisterForReflection -public class SnykAggregationStrategy { +public class SnykAggregator extends ProviderAggregator { private static final String SNYK_PRIVATE_VULNERABILITY_ID = "SNYK-PRIVATE-VULNERABILITY"; private static final String SNYK_PRIVATE_VULNERABILITY_TITLE = "Sign up for a free Snyk account to learn aboutn the vulnerabilities found"; - private static final Logger LOGGER = LoggerFactory.getLogger(SnykAggregationStrategy.class); private final ObjectMapper mapper = ObjectMapperProducer.newInstance(); - @SuppressWarnings("unchecked") - public GraphRequest aggregate( - GraphRequest graphReq, - Map currentHeaders, - Map currentProperties, - Object newExchange, - Map newHeaders, - Map newProperties) - throws JsonMappingException, JsonProcessingException { - - List privateProviders = - (List) currentProperties.get(Constants.PROVIDER_PRIVATE_DATA_PROPERTY); - boolean filterUnique = - privateProviders != null && privateProviders.contains(Constants.SNYK_PROVIDER); - GraphRequest.Builder builder = new GraphRequest.Builder(graphReq); - if (newExchange instanceof ProviderStatus) { - return builder.providerStatuses(List.of((ProviderStatus) newExchange)).build(); - } - ProviderStatus status; - try { - JsonNode snykResponse = mapper.readTree((byte[]) newExchange); - Map> issuesData = getIssues(snykResponse, filterUnique); - builder.issues(issuesData); - status = - new ProviderStatus() - .ok(true) - .provider(Constants.SNYK_PROVIDER) - .status(Response.Status.OK.getStatusCode()) - .message(Response.Status.OK.name()); - } catch (IOException e) { - LOGGER.error("Unable to read Json from Snyk Response", e); - status = - new ProviderStatus() - .ok(false) - .provider(Constants.SNYK_PROVIDER) - .status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()) - .message(e.getMessage()); - } - return builder.providerStatuses(List.of(status)).build(); - } - private Map> getIssues(JsonNode snykResponse, boolean filterUnique) { Map> reports = new HashMap<>(); snykResponse @@ -124,7 +77,6 @@ private Issue toIssue(String id, JsonNode data, boolean filterUnique) { } return new Issue() .id(id) - .source(Constants.SNYK_PROVIDER) .title(data.get("title").asText()) .severity(SeverityUtils.fromValue(data.get("severity").asText())) .cvss(CvssParser.fromVectorString(cvssV3)) @@ -136,10 +88,24 @@ private Issue toIssue(String id, JsonNode data, boolean filterUnique) { private Issue toFilteredIssue(JsonNode data) { return new Issue() .id(SNYK_PRIVATE_VULNERABILITY_ID) - .source(Constants.SNYK_PROVIDER) .title(SNYK_PRIVATE_VULNERABILITY_TITLE) .severity(SeverityUtils.fromValue(data.get("severity").asText())) .cvssScore(data.get("cvssScore").floatValue()) .unique(Boolean.TRUE); } + + protected final String getProviderName() { + return Constants.SNYK_PROVIDER; + } + + public Map> transformResponse( + @Body byte[] providerResponse, + @ExchangeProperty(Constants.PROVIDER_PRIVATE_DATA_PROPERTY) String privateProviders) + throws IOException { + boolean filterUnique = + privateProviders != null && privateProviders.contains(Constants.SNYK_PROVIDER); + + JsonNode snykResponse = mapper.readTree((byte[]) providerResponse); + return getIssues(snykResponse, filterUnique); + } } diff --git a/src/main/java/com/redhat/exhort/integration/snyk/SnykIntegration.java b/src/main/java/com/redhat/exhort/integration/providers/snyk/SnykIntegration.java similarity index 91% rename from src/main/java/com/redhat/exhort/integration/snyk/SnykIntegration.java rename to src/main/java/com/redhat/exhort/integration/providers/snyk/SnykIntegration.java index 4fef3d62..02fc8f9f 100644 --- a/src/main/java/com/redhat/exhort/integration/snyk/SnykIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/providers/snyk/SnykIntegration.java @@ -16,17 +16,16 @@ * limitations under the License. */ -package com.redhat.exhort.integration.snyk; +package com.redhat.exhort.integration.providers.snyk; import org.apache.camel.Exchange; import org.apache.camel.Message; -import org.apache.camel.builder.AggregationStrategies; import org.apache.camel.builder.endpoint.EndpointRouteBuilder; import org.eclipse.microprofile.config.inject.ConfigProperty; import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.integration.VulnerabilityProvider; import com.redhat.exhort.integration.backend.BackendUtils; +import com.redhat.exhort.integration.providers.VulnerabilityProvider; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; @@ -51,21 +50,24 @@ public void configure() { from(direct("snykDepGraph")) .routeId("snykDepGraph") .process(this::setAuthToken) - .enrich(direct("snykRequest"), AggregationStrategies.bean(SnykAggregationStrategy.class, "aggregate")); - - from(direct("snykRequest")) - .routeId("snykRequest") - .transform().method(SnykRequestBuilder.class, "fromDiGraph") - .process(this::processDepGraphRequest) - .circuitBreaker() + .circuitBreaker() .faultToleranceConfiguration() .timeoutEnabled(true) .timeoutDuration(timeout) .end() - .to(vertxHttp("{{api.snyk.host}}")) + .to(direct("snykRequest")) .onFallback() .process(e -> BackendUtils.processResponseError(e, Constants.SNYK_PROVIDER)); + from(direct("snykRequest")) + .routeId("snykRequest") + .transform().method(SnykRequestBuilder.class, "fromDiGraph") + .process(this::processDepGraphRequest) + .to(vertxHttp("{{api.snyk.host}}")) + .transform().method(SnykAggregator.class, "transformResponse") + .transform().method(SnykAggregator.class, "buildReport") + ; + from(direct("snykValidateToken")) .routeId("snykValidateToken") .process(this::processTokenRequest) diff --git a/src/main/java/com/redhat/exhort/integration/snyk/SnykRequestBuilder.java b/src/main/java/com/redhat/exhort/integration/providers/snyk/SnykRequestBuilder.java similarity index 98% rename from src/main/java/com/redhat/exhort/integration/snyk/SnykRequestBuilder.java rename to src/main/java/com/redhat/exhort/integration/providers/snyk/SnykRequestBuilder.java index 7361572a..6d507806 100644 --- a/src/main/java/com/redhat/exhort/integration/snyk/SnykRequestBuilder.java +++ b/src/main/java/com/redhat/exhort/integration/providers/snyk/SnykRequestBuilder.java @@ -16,7 +16,7 @@ * limitations under the License. */ -package com.redhat.exhort.integration.snyk; +package com.redhat.exhort.integration.providers.snyk; import java.util.Collections; import java.util.Set; diff --git a/src/main/java/com/redhat/exhort/integration/report/ReportIntegration.java b/src/main/java/com/redhat/exhort/integration/report/ReportIntegration.java index 2f088fb9..b61de284 100644 --- a/src/main/java/com/redhat/exhort/integration/report/ReportIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/report/ReportIntegration.java @@ -49,7 +49,6 @@ public void configure() { from(direct("htmlReport")) .routeId("htmlReport") .setHeader(Exchange.CONTENT_TYPE, constant(MediaType.TEXT_HTML)) - .bean(ReportTransformer.class, "transform") .setProperty(Constants.REPORT_PROPERTY, body()) .setBody(method(reportTemplate, "setVariables")) .to(freemarker("report.ftl")); @@ -66,7 +65,6 @@ public void configure() { from(direct("jsonReport")) .routeId("jsonReport") - .bean(ReportTransformer.class, "transform") .bean(ReportTransformer.class, "filterVerboseResult") .marshal().json(); //fmt:on diff --git a/src/main/java/com/redhat/exhort/integration/report/ReportTemplate.java b/src/main/java/com/redhat/exhort/integration/report/ReportTemplate.java index 29dffdbc..4f20dd3b 100644 --- a/src/main/java/com/redhat/exhort/integration/report/ReportTemplate.java +++ b/src/main/java/com/redhat/exhort/integration/report/ReportTemplate.java @@ -92,11 +92,11 @@ public String format(String id) { @RegisterForReflection public static record IssueVisibilityHelper(List providerData) { - public boolean showIssue(Issue issue) { + public boolean showIssue(String provider, Issue issue) { if (!issue.getUnique() || providerData == null) { return true; } - return !providerData.contains(issue.getSource()); + return !providerData.contains(provider); } } } diff --git a/src/main/java/com/redhat/exhort/integration/report/ReportTransformer.java b/src/main/java/com/redhat/exhort/integration/report/ReportTransformer.java index 1118746e..5c69fe86 100644 --- a/src/main/java/com/redhat/exhort/integration/report/ReportTransformer.java +++ b/src/main/java/com/redhat/exhort/integration/report/ReportTransformer.java @@ -18,35 +18,15 @@ package com.redhat.exhort.integration.report; -import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; import org.apache.camel.Body; import org.apache.camel.Exchange; -import org.apache.camel.Header; +import org.apache.camel.ExchangeProperty; import org.apache.camel.attachment.AttachmentMessage; import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.api.DependenciesSummary; -import com.redhat.exhort.api.DependencyReport; -import com.redhat.exhort.api.Issue; -import com.redhat.exhort.api.Remediation; -import com.redhat.exhort.api.Summary; -import com.redhat.exhort.api.TransitiveDependencyReport; -import com.redhat.exhort.api.VulnerabilitiesSummary; import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.model.CvssScoreComparable.DependencyScoreComparator; -import com.redhat.exhort.model.DirectDependency; -import com.redhat.exhort.model.GraphRequest; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -56,99 +36,11 @@ @RegisterForReflection public class ReportTransformer { - public AnalysisReport transform(@Body GraphRequest request) { - List depsReport = new ArrayList<>(); - Collection direct = request.tree().dependencies().values(); - VulnerabilityCounter counter = new VulnerabilityCounter(); - direct.forEach( - d -> { - List issues = request.issues().get(d.ref().name()); - if (issues == null) { - issues = Collections.emptyList(); - } - List transitiveReport = - getTransitiveDependenciesReport(d, request); - updateVulnerabilitySummary(issues, transitiveReport, counter); - Optional highestVulnerability = issues.stream().findFirst(); - Optional highestTransitive = - transitiveReport.stream() - .map(TransitiveDependencyReport::getHighestVulnerability) - .max(Comparator.comparing(Issue::getCvssScore)); - if (!highestTransitive.isEmpty()) { - if (highestVulnerability.isEmpty() - || highestVulnerability.get().getCvssScore() - < highestTransitive.get().getCvssScore()) { - highestVulnerability = highestTransitive; - } - } - depsReport.add( - new DependencyReport() - .ref(d.ref()) - .highestVulnerability(highestVulnerability.orElse(null)) - .issues( - issues.stream() - .sorted(Comparator.comparing(Issue::getCvssScore).reversed()) - .collect(Collectors.toList())) - .transitive(transitiveReport) - .remediations(getRemediations(issues, request.remediations())) - .recommendation(request.recommendations().get(d.ref().toGav()))); - }); - List result = - depsReport.stream() - .filter( - r -> - (r.getIssues() != null && !r.getIssues().isEmpty()) - || !r.getTransitive().isEmpty()) - .sorted(new DependencyScoreComparator().reversed()) - .collect(Collectors.toList()); - DependenciesSummary deps = - new DependenciesSummary() - .scanned(request.tree().directCount()) - .transitive(request.tree().transitiveCount()); - counter.direct.set(result.size()); - Summary summary = - new Summary() - .dependencies(deps) - .vulnerabilities(counter.getSummary()) - .providerStatuses(request.providerStatuses()); - return new AnalysisReport().summary(summary).dependencies(result); - } - - private void updateVulnerabilitySummary( - List issues, - List transitiveReport, - VulnerabilityCounter counter) { - issues.forEach(i -> incrementCounter(i, counter)); - transitiveReport.forEach( - tr -> { - tr.getIssues().forEach(i -> incrementCounter(i, counter)); - }); - } - - private void incrementCounter(Issue i, VulnerabilityCounter counter) { - switch (i.getSeverity()) { - case CRITICAL: - counter.critical.incrementAndGet(); - break; - case HIGH: - counter.high.incrementAndGet(); - break; - case MEDIUM: - counter.medium.incrementAndGet(); - break; - case LOW: - counter.low.incrementAndGet(); - break; - } - counter.total.incrementAndGet(); - } - public AnalysisReport filterVerboseResult( - @Body AnalysisReport report, @Header(Constants.VERBOSE_MODE_HEADER) Boolean verbose) { + @Body AnalysisReport report, + @ExchangeProperty(Constants.VERBOSE_MODE_HEADER) Boolean verbose) { if (Boolean.FALSE.equals(verbose)) { - return new AnalysisReport() - .summary(report.getSummary()) - .dependencies(Collections.emptyList()); + report.values().forEach(r -> r.dependencies(Collections.emptyList())); } return report; } @@ -160,78 +52,4 @@ public void attachHtmlReport(Exchange exchange) { "report.html", new DataHandler(exchange.getIn().getBody(String.class), MediaType.TEXT_HTML)); } - - private List getTransitiveDependenciesReport( - DirectDependency start, GraphRequest request) { - - List result = new ArrayList<>(); - start.transitive().stream() - .forEach( - d -> { - List issues = request.issues().get(d.name()); - if (issues != null && !issues.isEmpty()) { - Optional highestVulnerability = - issues.stream().max(Comparator.comparing(Issue::getCvssScore)); - result.add( - new TransitiveDependencyReport() - .ref(d) - .highestVulnerability(highestVulnerability.orElse(null)) - .issues( - issues.stream() - .sorted(Comparator.comparing(Issue::getCvssScore).reversed()) - .collect(Collectors.toList())) - .remediations(getRemediations(issues, request.remediations()))); - } - }); - return result; - } - - private Map getRemediations( - List issues, Map remediations) { - Map result = new HashMap<>(); - if (issues == null) { - return result; - } - issues.stream() - .map(i -> i.getCves()) - .filter(Objects::nonNull) - .flatMap(List::stream) - .forEach( - cve -> { - Remediation r = remediations.get(cve); - if (r != null) { - result.put(cve, r); - } - }); - return result; - } - - static final record VulnerabilityCounter( - AtomicInteger total, - AtomicInteger direct, - AtomicInteger critical, - AtomicInteger high, - AtomicInteger medium, - AtomicInteger low) { - - VulnerabilityCounter() { - this( - new AtomicInteger(), - new AtomicInteger(), - new AtomicInteger(), - new AtomicInteger(), - new AtomicInteger(), - new AtomicInteger()); - } - - VulnerabilitiesSummary getSummary() { - return new VulnerabilitiesSummary() - .total(total.get()) - .direct(direct.get()) - .critical(critical.get()) - .high(high.get()) - .medium(medium.get()) - .low(low.get()); - } - } } diff --git a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentBodyMapper.java b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentBodyMapper.java index 1bce3406..a1c475ec 100644 --- a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentBodyMapper.java +++ b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentBodyMapper.java @@ -18,20 +18,27 @@ package com.redhat.exhort.integration.trustedcontent; -import java.util.Collection; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import org.apache.camel.Body; +import org.apache.camel.ExchangeProperty; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.api.AnalysisReportValue; +import com.redhat.exhort.api.DependencyReport; import com.redhat.exhort.api.Issue; import com.redhat.exhort.api.PackageRef; import com.redhat.exhort.api.Remediation; +import com.redhat.exhort.api.TransitiveDependencyReport; import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.model.GraphRequest; +import com.redhat.exhort.model.DependencyTree; import com.redhat.exhort.model.trustedcontent.MavenPackage; import com.redhat.exhort.model.trustedcontent.VexRequest; import com.redhat.exhort.model.trustedcontent.VexResult; @@ -41,14 +48,30 @@ @RegisterForReflection public class TrustedContentBodyMapper { - public static VexRequest buildVexRequest(@Body GraphRequest graph) { - return new VexRequest( - graph.issues().values().stream() - .flatMap(Collection::stream) - .map(Issue::getCves) - .filter(Objects::nonNull) - .flatMap(List::stream) - .collect(Collectors.toUnmodifiableList())); + public static VexRequest buildVexRequest(@Body AnalysisReport reports) { + Set cves = new HashSet<>(); + reports.values().stream() + .map(AnalysisReportValue::getDependencies) + .flatMap(List::stream) + .forEach( + d -> { + if (d.getIssues() != null) { + d.getIssues().stream().forEach(i -> cves.addAll(i.getCves())); + } + if (d.getTransitive() != null) { + cves.addAll( + d.getTransitive().stream() + .map(TransitiveDependencyReport::getIssues) + .flatMap(List::stream) + .filter(Objects::nonNull) + .map(Issue::getCves) + .filter(Objects::nonNull) + .flatMap(List::stream) + .collect(Collectors.toSet())); + } + }); + + return new VexRequest(List.copyOf(cves)); } public Map createRemediations(VexRequest request, List response) { @@ -70,44 +93,108 @@ public Map createRemediations(VexRequest request, List remediations) { + public AnalysisReport filterRemediations( + AnalysisReport req, Map remediations) { if (remediations == null || remediations.isEmpty()) { return req; } - Map merged = new HashMap<>(); - if (req.remediations() != null) { - merged.putAll(req.remediations()); - } - remediations.entrySet().stream() - .filter(Objects::nonNull) - .filter( - r -> { - PackageRef o = r.getValue().getMavenPackage(); - return req.tree().getAll().contains(o); - }) - .forEach(e -> merged.put(e.getKey(), e.getValue())); - return new GraphRequest.Builder(req).remediations(merged).build(); + req.values() + .forEach( + report -> + report.getDependencies().stream() + .forEach( + d -> { + Map depRemediations = new HashMap<>(); + if (d.getIssues() != null) { + d.getIssues() + .forEach( + i -> { + List cves = i.getCves(); + if (cves != null) { + cves.forEach( + cve -> { + Remediation r = remediations.get(cve); + if (r != null + && r.getMavenPackage() + .name() + .equals(d.getRef().name())) { + depRemediations.put(cve, r); + } + }); + } + }); + if (d.getTransitive() != null) { + d.getTransitive() + .forEach( + transitive -> { + Map transRemediations = + new HashMap<>(); + if (transitive.getIssues() != null) { + transitive + .getIssues() + .forEach( + i -> { + List cves = i.getCves(); + if (cves != null) { + cves.forEach( + cve -> { + Remediation r = remediations.get(cve); + if (r != null) + if (r.getMavenPackage() + .name() + .equals( + transitive.getRef().name())) { + transRemediations.put(cve, r); + } else if (r.getMavenPackage() + .name() + .equals(d.getRef().name())) { + depRemediations.put(cve, r); + } + }); + } + }); + } + transitive.setRemediations(transRemediations); + }); + } + } + d.setRemediations(depRemediations); + })); + return req; } // Only look for recommendations for direct dependencies - public List buildGavRequest(@Body GraphRequest request) { - return request.tree().dependencies().keySet().stream() + public List buildGavRequest( + @ExchangeProperty(Constants.DEPENDENCY_TREE_PROPERTY) DependencyTree tree) { + return tree.dependencies().keySet().stream() .map(PackageRef::toGav) .sorted() .collect(Collectors.toUnmodifiableList()); } - public GraphRequest addRecommendations( - GraphRequest req, Map recommendations) { + public AnalysisReport addRecommendations( + AnalysisReport reports, Map recommendations) { if (recommendations == null || recommendations.isEmpty()) { - return req; + return reports; } - Map merged = new HashMap<>(recommendations); - if (req.recommendations() != null) { - merged.putAll(req.recommendations()); - } - return new GraphRequest.Builder(req).recommendations(merged).build(); + reports + .values() + .forEach( + report -> + recommendations + .entrySet() + .forEach( + r -> { + Optional dep = + report.getDependencies().stream() + .filter(d -> d.getRef().toGav().equals(r.getKey())) + .findFirst(); + if (dep.isPresent()) { + dep.get().setRecommendation(r.getValue()); + } + })); + return reports; } public Map createGavRecommendations( diff --git a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java index 721b8e09..2afd3090 100644 --- a/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/trustedcontent/TrustedContentIntegration.java @@ -18,43 +18,38 @@ package com.redhat.exhort.integration.trustedcontent; -import java.util.ArrayList; import java.util.Collections; -import java.util.List; import org.apache.camel.Exchange; import org.apache.camel.Message; import org.apache.camel.builder.AggregationStrategies; import org.apache.camel.builder.endpoint.EndpointRouteBuilder; import org.apache.camel.component.jackson.ListJacksonDataFormat; -import org.apache.camel.http.base.HttpOperationFailedException; import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; -import com.redhat.exhort.api.ProviderStatus; import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.integration.ProviderAggregationStrategy; -import com.redhat.exhort.model.GraphRequest; import com.redhat.exhort.model.trustedcontent.MavenPackage; import com.redhat.exhort.model.trustedcontent.VexResult; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.HttpMethod; import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; @ApplicationScoped public class TrustedContentIntegration extends EndpointRouteBuilder { + private static final Logger LOGGER = LoggerFactory.getLogger(TrustedContentIntegration.class); + @ConfigProperty(name = "api.trustedContent.timeout", defaultValue = "10s") String timeout; @Override public void configure() { // fmt:off - from(direct("recommendAllTrustedContent")) - .routeId("recommendAllTrustedContent") - .multicast(AggregationStrategies.bean(ProviderAggregationStrategy.class, "aggregate")) - .parallelProcessing() + from(direct("findRemediations")) + .routeId("findRemediations") .circuitBreaker() .faultToleranceConfiguration() .timeoutEnabled(true) @@ -62,26 +57,13 @@ public void configure() { .end() .enrich(direct("trustedContentVex"), AggregationStrategies.bean(TrustedContentBodyMapper.class, "filterRemediations")) - .enrich(direct("trustedContentGav"), - AggregationStrategies.bean(TrustedContentBodyMapper.class, "addRecommendations")) .onFallback() - .process(this::processFallBack); - - from(direct("recommendVexContent")) - .routeId("addRecommendedVexContent") - .circuitBreaker() - .faultToleranceConfiguration() - .timeoutEnabled(true) - .timeoutDuration(timeout) - .end() - .enrich(direct("trustedContentVex"), - AggregationStrategies.bean(TrustedContentBodyMapper.class, "filterRemediations")) - .onFallback() - .process(this::processFallBack); + .process(this::processFallback); from(direct("trustedContentVex")) - .routeId("prepareVexRequest") + .routeId("trustedContentVex") .process(this::processVexRequest) + .transform().method(TrustedContentBodyMapper.class, "buildVexRequest") .choice().when(method(TrustedContentBodyMapper.class, "cvesSize").isGreaterThan(0)) .enrich(direct("vexRequest"), AggregationStrategies.bean(TrustedContentBodyMapper.class, "createRemediations")) @@ -94,7 +76,7 @@ public void configure() { .unmarshal(new ListJacksonDataFormat(VexResult.class)); from(direct("trustedContentGav")) - .routeId("prepareGavRequest") + .routeId("trustedContentGav") .bean(TrustedContentBodyMapper.class, "buildGavRequest") .choice().when(method(TrustedContentBodyMapper.class, "gavsSize").isGreaterThan(0)) .enrich(direct("gavRequest"), @@ -108,13 +90,21 @@ public void configure() { .to(vertxHttp("{{api.trustedContent.gav.host}}")) .unmarshal(new ListJacksonDataFormat(MavenPackage.class)); + from(direct("recommendTrustedContent")) + .routeId("recommendTrustedContent") + .circuitBreaker() + .faultToleranceConfiguration() + .timeoutEnabled(true) + .timeoutDuration(timeout) + .end() + .enrich(direct("trustedContentGav"), AggregationStrategies.bean(TrustedContentBodyMapper.class, "addRecommendations")) + .onFallback() + .process(this::processFallback); // fmt:on } private void processVexRequest(Exchange exchange) { - Message message = exchange.getMessage(); - processRequest(message, Constants.TRUSTED_CONTENT_VEX_PATH); - message.setBody(TrustedContentBodyMapper.buildVexRequest(message.getBody(GraphRequest.class))); + processRequest(exchange.getMessage(), Constants.TRUSTED_CONTENT_VEX_PATH); } private void processGavRequest(Exchange exchange) { @@ -133,30 +123,9 @@ private void processRequest(Message message, String path) { message.setHeader("Accept", MediaType.APPLICATION_JSON); } - private void processFallBack(Exchange exchange) { - GraphRequest req = exchange.getMessage().getBody(GraphRequest.class); - ProviderStatus status = new ProviderStatus().ok(false).provider(Constants.TRUSTED_CONTENT_NAME); + private void processFallback(Exchange exchange) { + // Ignore error and don't process Trusted Content requests Exception exception = (Exception) exchange.getProperty(Exchange.EXCEPTION_CAUGHT); - Throwable cause = exception.getCause(); - - if (cause != null) { - if (cause instanceof HttpOperationFailedException) { - HttpOperationFailedException httpException = (HttpOperationFailedException) cause; - status.message(httpException.getMessage()).status(httpException.getStatusCode()); - - } else { - status - .message(cause.getMessage()) - .status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); - } - } else { - status - .message(exception.getMessage()) - .status(Response.Status.INTERNAL_SERVER_ERROR.getStatusCode()); - } - List statuses = new ArrayList<>(req.providerStatuses()); - statuses.add(status); - GraphRequest newReq = new GraphRequest.Builder(req).providerStatuses(statuses).build(); - exchange.getMessage().setBody(newReq); + LOGGER.warn("Unable to process Trusted Content request", exception); } } diff --git a/src/main/java/com/redhat/exhort/model/CvssScoreComparable.java b/src/main/java/com/redhat/exhort/model/CvssScoreComparable.java index 10b755a6..10c34953 100644 --- a/src/main/java/com/redhat/exhort/model/CvssScoreComparable.java +++ b/src/main/java/com/redhat/exhort/model/CvssScoreComparable.java @@ -36,13 +36,13 @@ public int compare(DependencyReport d1, DependencyReport d2) { return 0; } if (d1.getHighestVulnerability() != null && d2.getHighestVulnerability() == null) { - return 1; + return -1; } if (d1.getHighestVulnerability() == null && d2.getHighestVulnerability() != null) { - return -1; + return 1; } return Float.compare( - d1.getHighestVulnerability().getCvssScore(), d2.getHighestVulnerability().getCvssScore()); + d2.getHighestVulnerability().getCvssScore(), d1.getHighestVulnerability().getCvssScore()); } } @@ -54,13 +54,13 @@ public int compare(TransitiveDependencyReport d1, TransitiveDependencyReport d2) return 0; } if (d1.getHighestVulnerability() != null && d2.getHighestVulnerability() == null) { - return 1; + return -1; } if (d1.getHighestVulnerability() == null && d2.getHighestVulnerability() != null) { - return -1; + return 1; } return Float.compare( - d1.getHighestVulnerability().getCvssScore(), d2.getHighestVulnerability().getCvssScore()); + d2.getHighestVulnerability().getCvssScore(), d1.getHighestVulnerability().getCvssScore()); } } } diff --git a/src/main/java/com/redhat/exhort/model/DirectDependency.java b/src/main/java/com/redhat/exhort/model/DirectDependency.java index e778a667..5b5a9702 100644 --- a/src/main/java/com/redhat/exhort/model/DirectDependency.java +++ b/src/main/java/com/redhat/exhort/model/DirectDependency.java @@ -27,7 +27,8 @@ import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection -public record DirectDependency(PackageRef ref, Set transitive) { +public record DirectDependency( + PackageRef ref, Set transitive, PackageRef recommendation) { public DirectDependency { Objects.requireNonNull(ref); @@ -46,6 +47,7 @@ public static class Builder { public PackageRef ref; public Set transitive; + public PackageRef recommendation; public Builder ref(PackageRef ref) { this.ref = ref; @@ -57,8 +59,13 @@ public Builder transitive(Set transitive) { return this; } + public Builder recommendation(PackageRef recommendation) { + this.recommendation = recommendation; + return this; + } + public DirectDependency build() { - return new DirectDependency(ref, transitive); + return new DirectDependency(ref, transitive, recommendation); } } } diff --git a/src/main/java/com/redhat/exhort/model/GraphRequest.java b/src/main/java/com/redhat/exhort/model/GraphRequest.java index fd23d52c..89f56091 100644 --- a/src/main/java/com/redhat/exhort/model/GraphRequest.java +++ b/src/main/java/com/redhat/exhort/model/GraphRequest.java @@ -18,7 +18,6 @@ package com.redhat.exhort.model; -import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -27,10 +26,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import com.redhat.exhort.api.Issue; import com.redhat.exhort.api.PackageRef; -import com.redhat.exhort.api.ProviderStatus; -import com.redhat.exhort.api.Remediation; import com.redhat.exhort.integration.Constants; import io.quarkus.runtime.annotations.RegisterForReflection; @@ -40,10 +36,7 @@ public record GraphRequest( String pkgManager, List providers, DependencyTree tree, - Map> issues, - Map remediations, - Map recommendations, - List providerStatuses) { + Map recommendations) { public GraphRequest { Objects.requireNonNull(pkgManager); @@ -61,26 +54,11 @@ public record GraphRequest( throw new IllegalArgumentException("Unsupported providers: " + invalidProviders); } - if (issues != null) { - issues = Collections.unmodifiableMap(issues); - } else { - issues = Collections.emptyMap(); - } - if (remediations != null) { - remediations = Collections.unmodifiableMap(remediations); - } else { - remediations = Collections.emptyMap(); - } if (recommendations != null) { recommendations = Collections.unmodifiableMap(recommendations); } else { recommendations = Collections.emptyMap(); } - if (providerStatuses != null) { - providerStatuses = Collections.unmodifiableList(providerStatuses); - } else { - providerStatuses = Collections.emptyList(); - } } public static class Builder { @@ -88,10 +66,7 @@ public static class Builder { String pkgManager; List providers; DependencyTree tree; - Map> issues; - Map remediations; Map recommendations; - List providerStatuses; public Builder(String pkgManager, List providers) { this.pkgManager = pkgManager; @@ -103,21 +78,9 @@ public Builder(GraphRequest copy) { this.providers = copy.providers; this.tree = copy.tree; - if (copy.issues != null) { - this.issues = new HashMap<>(copy.issues); - } - - if (copy.remediations != null) { - this.remediations = new HashMap<>(copy.remediations); - } - if (copy.recommendations != null) { this.recommendations = new HashMap<>(copy.recommendations); } - - if (copy.providerStatuses != null) { - this.providerStatuses = new ArrayList<>(copy.providerStatuses); - } } public Builder tree(DependencyTree tree) { @@ -125,30 +88,13 @@ public Builder tree(DependencyTree tree) { return this; } - public Builder issues(Map> issues) { - this.issues = issues; - return this; - } - ; - - public Builder remediations(Map remediations) { - this.remediations = remediations; - return this; - } - public Builder recommendations(Map recommendations) { this.recommendations = recommendations; return this; } - public Builder providerStatuses(List providerStatuses) { - this.providerStatuses = providerStatuses; - return this; - } - public GraphRequest build() { - return new GraphRequest( - pkgManager, providers, tree, issues, remediations, recommendations, providerStatuses); + return new GraphRequest(pkgManager, providers, tree, recommendations); } } } diff --git a/src/main/resources/freemarker/templates/report.ftl b/src/main/resources/freemarker/templates/report.ftl index cdf86d8e..ed317e7c 100644 --- a/src/main/resources/freemarker/templates/report.ftl +++ b/src/main/resources/freemarker/templates/report.ftl @@ -34,10 +34,10 @@ <#function packageLink package> <#return body.packagePath + package.name()?replace(":", "/") + "/" + package.version()> -<#function issueLink issue> - <#if issue.getSource()=="snyk"> +<#function issueLink providerName, issue> + <#if providerName=="snyk"> <#return body.snykIssueLinkFormatter.format(issue.getId()) > - <#elseif issue.getSource()=="oss-index"> + <#elseif providerName=="oss-index"> <#return body.ossIndexIssueLinkFormatter.format(issue.getId()) > @@ -57,6 +57,9 @@ +<#assign provider = body.report.snyk> +<#assign providerName = provider.getStatus().getName()> +
@@ -87,7 +90,7 @@ - Total Vulnerabilities: ${body.report.getSummary().getVulnerabilities().getTotal()} + Total Vulnerabilities: ${provider.getSummary().getVulnerabilities().getTotal()!0}

@@ -98,15 +101,14 @@ - Vulnerable Dependencies: ${body.report.getSummary().getVulnerabilities().getDirect()} + Vulnerable Dependencies: ${provider.getSummary().getVulnerabilities().getDirect()!0}

-<#list body.report.getSummary().getProviderStatuses() as providerStatus> -<#if (providerStatus.getStatus() >= 500)> +<#if (provider.getStatus().getCode() >= 500)>

- ${providerStatus.getProvider()}: - ${providerStatus.getProvider()?cap_first}: ${providerStatus.getMessage()!"Unknown error"} + ${providerName}: + ${providerName?cap_first}: ${provider.getStatus().getMessage()!"Unknown error"}


-<#elseif (providerStatus.getStatus() >= 400)> +<#elseif (provider.getStatus().getCode() >= 400)>

- ${providerStatus.getProvider()}: - ${providerStatus.getProvider()?cap_first}: ${providerStatus.getMessage()!"Unknown error"} + ${providerName}: + ${providerName?cap_first}: ${provider.getStatus().getMessage()!"Unknown error"}


- -<#if body.report.getDependencies()?size == 0> +<#if provider.getDependencies()?size == 0>
@@ -172,7 +173,7 @@ <#assign numOfPkg = 0> - <#list body.report.getDependencies() as dependency> + <#list provider.getDependencies() as dependency>
@@ -188,7 +189,11 @@ ${dependency.getRef().name()} + <#if dependency.getIssues()??> ${dependency.getIssues()?size} + <#else> + 0 + ${body.dependencyHelper.transitiveIssuesCount(dependency)} <#if dependency.getHighestVulnerability()?? > <#assign barNum = dependency.getHighestVulnerability().getCvssScore() *10> @@ -217,8 +222,8 @@
- <#if body.issueVisibilityHelper.showIssue(dependency.getHighestVulnerability())> - + ${dependency.getHighestVulnerability().getId()} @@ -277,7 +282,7 @@ <#list dependency.getIssues() as vulnerability> <#assign severity = vulnerability.getSeverity()> - <#if body.issueVisibilityHelper.showIssue(vulnerability)> + <#if body.issueVisibilityHelper.showIssue(providerName, vulnerability)> <#if severity == "critical" || severity == "high"> @@ -291,7 +296,7 @@ - <#if body.issueVisibilityHelper.showIssue(vulnerability)> + <#if body.issueVisibilityHelper.showIssue(providerName, vulnerability)> ${vulnerability.getCvss().getExploitCodeMaturity()!"No known exploit"} <#else> <#else> - <#if body.issueVisibilityHelper.showIssue(vulnerability)> - + ${vulnerability.getId()} @@ -400,7 +405,7 @@ - <#if body.issueVisibilityHelper.showIssue(vulnerability)> + <#if body.issueVisibilityHelper.showIssue(providerName, vulnerability)> <#if severity == "critical" || severity == "high"> @@ -462,8 +467,8 @@ ${remediation.version()} <#else> - <#if body.issueVisibilityHelper.showIssue(vulnerability)> - + ${vulnerability.getId()} diff --git a/src/test/java/com/redhat/exhort/integration/AnalysisTest.java b/src/test/java/com/redhat/exhort/integration/AnalysisTest.java index 565b86a0..783e12b8 100644 --- a/src/test/java/com/redhat/exhort/integration/AnalysisTest.java +++ b/src/test/java/com/redhat/exhort/integration/AnalysisTest.java @@ -37,7 +37,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.stream.Stream; import org.cyclonedx.CycloneDxMediaType; @@ -48,6 +47,7 @@ import org.junit.jupiter.params.provider.ValueSource; import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.api.AnalysisReportValue; import com.redhat.exhort.api.DependencyReport; import com.redhat.exhort.api.PackageRef; import com.redhat.exhort.api.ProviderStatus; @@ -145,26 +145,22 @@ public void testEmptySbom( providers.forEach( p -> { - Optional status = - report.getSummary().getProviderStatuses().stream() - .filter(s -> s.getProvider().equals(p)) - .findFirst(); - assertEquals(Response.Status.OK.getStatusCode(), status.get().getStatus()); - assertTrue(status.get().getOk()); - assertEquals(Response.Status.OK.getReasonPhrase(), status.get().getMessage()); + AnalysisReportValue reportValue = report.get(p); + assertEquals(Response.Status.OK.getStatusCode(), reportValue.getStatus().getCode()); + assertTrue(reportValue.getStatus().getOk()); + assertEquals(Response.Status.OK.getReasonPhrase(), reportValue.getStatus().getMessage()); + assertEquals(0, reportValue.getSummary().getDependencies().getScanned()); + assertEquals(0, reportValue.getSummary().getDependencies().getTransitive()); + assertEquals(0, reportValue.getSummary().getVulnerabilities().getTotal()); + assertEquals(0, reportValue.getSummary().getVulnerabilities().getDirect()); + assertEquals(0, reportValue.getSummary().getVulnerabilities().getCritical()); + assertEquals(0, reportValue.getSummary().getVulnerabilities().getHigh()); + assertEquals(0, reportValue.getSummary().getVulnerabilities().getMedium()); + assertEquals(0, reportValue.getSummary().getVulnerabilities().getLow()); + + assertTrue(reportValue.getDependencies().isEmpty()); }); - assertEquals(0, report.getSummary().getDependencies().getScanned()); - assertEquals(0, report.getSummary().getDependencies().getTransitive()); - assertEquals(0, report.getSummary().getVulnerabilities().getTotal()); - assertEquals(0, report.getSummary().getVulnerabilities().getDirect()); - assertEquals(0, report.getSummary().getVulnerabilities().getCritical()); - assertEquals(0, report.getSummary().getVulnerabilities().getHigh()); - assertEquals(0, report.getSummary().getVulnerabilities().getMedium()); - assertEquals(0, report.getSummary().getVulnerabilities().getLow()); - - assertTrue(report.getDependencies().isEmpty()); - verifyProviders(providers, authHeaders, true); verifyNoInteractionsWithTC(); @@ -295,11 +291,11 @@ public void testUnauthorizedRequest() { .body() .as(AnalysisReport.class); - assertEquals(1, report.getSummary().getProviderStatuses().size()); - ProviderStatus status = report.getSummary().getProviderStatuses().get(0); + assertEquals(1, report.size()); + ProviderStatus status = report.get(Constants.SNYK_PROVIDER).getStatus(); assertFalse(status.getOk()); - assertEquals(Constants.SNYK_PROVIDER, status.getProvider()); - assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), status.getStatus()); + assertEquals(Constants.SNYK_PROVIDER, status.getName()); + assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), status.getCode()); verifySnykRequest(INVALID_TOKEN); verifyNoInteractionsWithTC(); @@ -326,11 +322,11 @@ public void testForbiddenRequest() { .body() .as(AnalysisReport.class); - assertEquals(1, report.getSummary().getProviderStatuses().size()); - ProviderStatus status = report.getSummary().getProviderStatuses().get(0); + assertEquals(1, report.size()); + ProviderStatus status = report.get(Constants.SNYK_PROVIDER).getStatus(); assertFalse(status.getOk()); - assertEquals(Constants.SNYK_PROVIDER, status.getProvider()); - assertEquals(Response.Status.FORBIDDEN.getStatusCode(), status.getStatus()); + assertEquals(Constants.SNYK_PROVIDER, status.getName()); + assertEquals(Response.Status.FORBIDDEN.getStatusCode(), status.getCode()); verifySnykRequest(UNAUTH_TOKEN); verifyNoInteractionsWithTC(); @@ -357,8 +353,8 @@ public void testSBOMJsonWithToken() { .body() .as(AnalysisReport.class); - assertSummary(report.getSummary()); - assertDependenciesReport(report.getDependencies()); + assertSummary(report.get(Constants.SNYK_PROVIDER).getSummary()); + assertDependenciesReport(report.get(Constants.SNYK_PROVIDER).getDependencies()); verifyTCRequests(); verifySnykRequest(OK_TOKEN); @@ -385,8 +381,8 @@ public void testNonVerboseJson() { .body() .as(AnalysisReport.class); - assertSummary(report.getSummary()); - assertTrue(report.getDependencies().isEmpty()); + assertSummary(report.get(Constants.SNYK_PROVIDER).getSummary()); + assertTrue(report.get(Constants.SNYK_PROVIDER).getDependencies().isEmpty()); verifyTCRequests(); verifySnykRequest(null); @@ -414,8 +410,8 @@ public void testNonVerboseWithToken() { .body() .as(AnalysisReport.class); - assertSummary(report.getSummary()); - assertTrue(report.getDependencies().isEmpty()); + assertSummary(report.get(Constants.SNYK_PROVIDER).getSummary()); + assertTrue(report.get(Constants.SNYK_PROVIDER).getDependencies().isEmpty()); verifySnykRequest(OK_TOKEN); verifyTCRequests(); @@ -612,7 +608,7 @@ private void assertSummary(Summary summary) { assertEquals(7, summary.getDependencies().getTransitive()); assertEquals(4, summary.getVulnerabilities().getTotal()); - assertEquals(2, summary.getVulnerabilities().getDirect()); + assertEquals(0, summary.getVulnerabilities().getDirect()); assertEquals(0, summary.getVulnerabilities().getCritical()); assertEquals(1, summary.getVulnerabilities().getHigh()); assertEquals(3, summary.getVulnerabilities().getMedium()); @@ -662,9 +658,9 @@ private void assertDependenciesReport(List dependencies) { .name(jackson.purl().getName()) .version("2.13.1.Final-redhat-00002") .build(), - tReport.getRemediations().get("CVE-2020-36518").getMavenPackage()); + tReport.getRemediations().get("CVE-2022-42003").getMavenPackage()); - assertNull(tReport.getRemediations().get("CVE-2022-42003")); + assertNull(tReport.getRemediations().get("CVE-2020-36518")); } private DependencyReport getReport(String pkgName, List dependencies) { diff --git a/src/test/java/com/redhat/exhort/integration/VulnerabilityProviderTest.java b/src/test/java/com/redhat/exhort/integration/VulnerabilityProviderTest.java index 1cb5ed08..63dd603c 100644 --- a/src/test/java/com/redhat/exhort/integration/VulnerabilityProviderTest.java +++ b/src/test/java/com/redhat/exhort/integration/VulnerabilityProviderTest.java @@ -24,6 +24,8 @@ import org.junit.jupiter.api.Test; +import com.redhat.exhort.integration.providers.VulnerabilityProvider; + import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.TestProfile; diff --git a/src/test/java/com/redhat/exhort/integration/backend/BackendUtilsTest.java b/src/test/java/com/redhat/exhort/integration/backend/BackendUtilsTest.java new file mode 100644 index 00000000..b9b0c99a --- /dev/null +++ b/src/test/java/com/redhat/exhort/integration/backend/BackendUtilsTest.java @@ -0,0 +1,212 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package com.redhat.exhort.integration.backend; + +import static com.redhat.exhort.integration.backend.IssueTestUtils.buildIssue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.api.AnalysisReportValue; +import com.redhat.exhort.api.DependencyReport; +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.api.ProviderStatus; +import com.redhat.exhort.api.TransitiveDependencyReport; + +public class BackendUtilsTest { + + @Test + public void testRemoveEmptyDependencies_empty() { + AnalysisReport result = BackendUtils.removeEmptyDependencies(new AnalysisReport()); + assertEquals(new AnalysisReport(), result); + } + + @Test + public void testRemoveEmptyDependencies_empty_reports() { + + AnalysisReport reports = new AnalysisReport(); + reports.put("one", new AnalysisReportValue()); + reports.put("two", new AnalysisReportValue()); + AnalysisReport result = BackendUtils.removeEmptyDependencies(reports); + assertEquals(reports, result); + } + + @Test + public void testRemoveDependencies_no_issues_no_recommendations() { + AnalysisReport result = BackendUtils.removeEmptyDependencies(buildReport()); + assertNotNull(result); + AnalysisReportValue report = result.get(PROVIDER_NAME); + assertNotNull(report); + assertTrue(report.getDependencies().isEmpty()); + } + + @Test + public void testRemoveDependencies_with_recommendations() { + AnalysisReport reports = buildReport(); + addRecommendations(reports); + AnalysisReport result = BackendUtils.removeEmptyDependencies(reports); + List dependencies = result.get(PROVIDER_NAME).getDependencies(); + assertEquals(2, dependencies.size()); + assertEquals( + "pkg:maven/com.example/dep-a@1.0.0", dependencies.get(0).getRef().purl().getCoordinates()); + assertEquals( + "pkg:maven/com.example/dep-a@1.0.0-redhat-001", + dependencies.get(0).getRecommendation().purl().getCoordinates()); + assertEquals( + "pkg:maven/com.example/dep-b@1.0.0", dependencies.get(1).getRef().purl().getCoordinates()); + assertEquals( + "pkg:maven/com.example/dep-b@1.0.0-redhat-001", + dependencies.get(1).getRecommendation().purl().getCoordinates()); + } + + @Test + public void testRemoveDependencies_with_issues() { + AnalysisReport reports = buildReport(); + addIssues(reports); + AnalysisReport result = BackendUtils.removeEmptyDependencies(reports); + List dependencies = result.get(PROVIDER_NAME).getDependencies(); + assertEquals(2, dependencies.size()); + assertEquals( + "pkg:maven/com.example/dep-a@1.0.0", dependencies.get(0).getRef().purl().getCoordinates()); + assertEquals(2, dependencies.get(0).getIssues().size()); + assertEquals(Float.valueOf(10), dependencies.get(0).getHighestVulnerability().getCvssScore()); + assertEquals( + "pkg:maven/com.example/dep-c@1.0.0", dependencies.get(1).getRef().purl().getCoordinates()); + assertEquals(Float.valueOf(8), dependencies.get(1).getHighestVulnerability().getCvssScore()); + assertNull(dependencies.get(1).getIssues()); + assertEquals(1, dependencies.get(1).getTransitive().get(0).getIssues().size()); + assertEquals( + Float.valueOf(8), + dependencies.get(1).getTransitive().get(0).getHighestVulnerability().getCvssScore()); + } + + @Test + public void testRemoveDependencies() { + AnalysisReport reports = buildReport(); + addIssues(reports); + addRecommendations(reports); + AnalysisReport result = BackendUtils.removeEmptyDependencies(reports); + + List dependencies = result.get(PROVIDER_NAME).getDependencies(); + assertEquals(3, dependencies.size()); + assertEquals( + "pkg:maven/com.example/dep-a@1.0.0", dependencies.get(0).getRef().purl().getCoordinates()); + assertEquals(2, dependencies.get(0).getIssues().size()); + assertEquals(Float.valueOf(10), dependencies.get(0).getHighestVulnerability().getCvssScore()); + assertEquals( + "pkg:maven/com.example/dep-c@1.0.0", dependencies.get(2).getRef().purl().getCoordinates()); + assertEquals(Float.valueOf(8), dependencies.get(2).getHighestVulnerability().getCvssScore()); + assertNull(dependencies.get(1).getIssues()); + assertEquals(1, dependencies.get(2).getTransitive().get(0).getIssues().size()); + assertEquals( + Float.valueOf(8), + dependencies.get(2).getTransitive().get(0).getHighestVulnerability().getCvssScore()); + + assertEquals( + "pkg:maven/com.example/dep-a@1.0.0", dependencies.get(0).getRef().purl().getCoordinates()); + assertEquals( + "pkg:maven/com.example/dep-a@1.0.0-redhat-001", + dependencies.get(0).getRecommendation().purl().getCoordinates()); + assertEquals( + "pkg:maven/com.example/dep-b@1.0.0", dependencies.get(1).getRef().purl().getCoordinates()); + assertEquals( + "pkg:maven/com.example/dep-b@1.0.0-redhat-001", + dependencies.get(1).getRecommendation().purl().getCoordinates()); + } + + private static AnalysisReportValue emptyReport(String providerName) { + return new AnalysisReportValue() + .status(new ProviderStatus().name(providerName)) + .dependencies(Collections.emptyList()); + } + + private static void addIssues(AnalysisReport report) { + // Add issues to direct dependency dep-a + report + .get(PROVIDER_NAME) + .getDependencies() + .get(0) + .issues(List.of(buildIssue(10), buildIssue(5))) + .highestVulnerability(buildIssue(10)); + // Add issues to transitive dependency dep-c-a + report + .get(PROVIDER_NAME) + .getDependencies() + .get(2) + .highestVulnerability(buildIssue(8)) + .getTransitive() + .get(0) + .issues(List.of(buildIssue(8))) + .highestVulnerability(buildIssue(8)); + } + + private static void addRecommendations(AnalysisReport report) { + // Add recommendation dependency dep-a + report + .get(PROVIDER_NAME) + .getDependencies() + .get(0) + .recommendation(new PackageRef("pkg:maven/com.example/dep-a@1.0.0-redhat-001")); + // Add recommendation to dependency dep-b + report + .get(PROVIDER_NAME) + .getDependencies() + .get(1) + .recommendation(new PackageRef("pkg:maven/com.example/dep-b@1.0.0-redhat-001")); + } + + private static final String PROVIDER_NAME = "example"; + + private static final AnalysisReport buildReport() { + AnalysisReport result = new AnalysisReport(); + result.put( + PROVIDER_NAME, + emptyReport(PROVIDER_NAME) + .dependencies( + List.of( + new DependencyReport() + .ref(new PackageRef("pkg:maven/com.example/dep-a@1.0.0")) + .transitive( + List.of( + new TransitiveDependencyReport() + .ref(new PackageRef("pkg:maven/com.example/dep-a-a@1.0.0")), + new TransitiveDependencyReport() + .ref(new PackageRef("pkg:maven/com.example/dep-a-b@1.0.0")))), + new DependencyReport().ref(new PackageRef("pkg:maven/com.example/dep-b@1.0.0")), + new DependencyReport() + .ref(new PackageRef("pkg:maven/com.example/dep-c@1.0.0")) + .transitive( + List.of( + new TransitiveDependencyReport() + .ref(new PackageRef("pkg:maven/com.example/dep-c-a@1.0.0")), + new TransitiveDependencyReport() + .ref(new PackageRef("pkg:maven/com.example/dep-c-b@1.0.0")), + new TransitiveDependencyReport() + .ref( + new PackageRef("pkg:maven/com.example/dep-c-c@1.0.0"))))))); + return result; + } +} diff --git a/src/test/java/com/redhat/exhort/integration/backend/IssueTestUtils.java b/src/test/java/com/redhat/exhort/integration/backend/IssueTestUtils.java new file mode 100644 index 00000000..bf65c5c0 --- /dev/null +++ b/src/test/java/com/redhat/exhort/integration/backend/IssueTestUtils.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package com.redhat.exhort.integration.backend; + +import com.redhat.exhort.api.Issue; +import com.redhat.exhort.api.SeverityUtils; + +public class IssueTestUtils { + + public static Issue buildIssue(int i) { + return new Issue() + .id("ID-" + i) + .title("Vulnerability " + i) + .cvssScore(Float.valueOf(i)) + .addCvesItem("CVE-" + i) + .severity(SeverityUtils.fromScore(i)); + } +} diff --git a/src/test/java/com/redhat/exhort/integration/backend/ProviderAggregatorTest.java b/src/test/java/com/redhat/exhort/integration/backend/ProviderAggregatorTest.java new file mode 100644 index 00000000..5a5b20c3 --- /dev/null +++ b/src/test/java/com/redhat/exhort/integration/backend/ProviderAggregatorTest.java @@ -0,0 +1,206 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * 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. + */ + +package com.redhat.exhort.integration.backend; + +import static com.redhat.exhort.integration.backend.IssueTestUtils.buildIssue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.jupiter.api.Test; + +import com.redhat.exhort.api.AnalysisReportValue; +import com.redhat.exhort.api.Issue; +import com.redhat.exhort.api.PackageRef; +import com.redhat.exhort.integration.Constants; +import com.redhat.exhort.integration.providers.ProviderAggregator; +import com.redhat.exhort.model.DependencyTree; +import com.redhat.exhort.model.DirectDependency; + +import jakarta.ws.rs.core.Response.Status; + +public class ProviderAggregatorTest { + + @Test + public void testBuildReport_empty() { + AnalysisReportValue report = + new SimpleProviderAggregator() + .buildReport( + Collections.emptyMap(), + DependencyTree.builder() + .root(DependencyTree.getDefaultRoot(Constants.MAVEN_PKG_MANAGER)) + .build()); + assertEmptyReport(report); + + report = + new SimpleProviderAggregator() + .buildReport( + Collections.emptyMap(), + DependencyTree.builder() + .root(DependencyTree.getDefaultRoot(Constants.MAVEN_PKG_MANAGER)) + .dependencies(Collections.emptyMap()) + .build()); + assertEmptyReport(report); + } + + @Test + public void testBuildReport() { + DependencyTree tree = buildTree(); + AnalysisReportValue report = new SimpleProviderAggregator().buildReport(buildIssues(), tree); + assertNotNull(report); + + assertEquals(3, report.getSummary().getDependencies().getScanned()); + assertEquals(4, report.getSummary().getDependencies().getTransitive()); + assertEquals(3, report.getDependencies().size()); + + assertEquals(4, report.getSummary().getVulnerabilities().getTotal()); + assertEquals(1, report.getSummary().getVulnerabilities().getHigh()); + assertEquals(1, report.getSummary().getVulnerabilities().getCritical()); + assertEquals(1, report.getSummary().getVulnerabilities().getDirect()); + assertEquals(1, report.getSummary().getVulnerabilities().getMedium()); + assertEquals(1, report.getSummary().getVulnerabilities().getLow()); + + assertOkStatus(report); + + // this dependency has one transitive dependency with 3 issues + assertEquals( + Float.valueOf(10), + report.getDependencies().get(0).getHighestVulnerability().getCvssScore()); + + assertEquals( + Float.valueOf(10), + report + .getDependencies() + .get(0) + .getTransitive() + .get(0) + .getHighestVulnerability() + .getCvssScore()); + assertEquals( + Float.valueOf(10), + report.getDependencies().get(0).getTransitive().get(0).getIssues().get(0).getCvssScore()); + assertEquals( + Float.valueOf(8), + report.getDependencies().get(0).getTransitive().get(0).getIssues().get(1).getCvssScore()); + assertEquals( + Float.valueOf(5), + report.getDependencies().get(0).getTransitive().get(0).getIssues().get(2).getCvssScore()); + + assertEquals(Collections.emptyList(), report.getDependencies().get(0).getIssues()); + assertEquals( + Collections.emptyList(), + report.getDependencies().get(0).getTransitive().get(1).getIssues()); + assertNull(report.getDependencies().get(0).getTransitive().get(1).getHighestVulnerability()); + + // this has only one issue in the direct dependency + assertEquals( + Float.valueOf(3), report.getDependencies().get(1).getHighestVulnerability().getCvssScore()); + assertEquals( + Float.valueOf(3), report.getDependencies().get(1).getIssues().get(0).getCvssScore()); + + // this dependency does not have issues + assertNull(report.getDependencies().get(2).getHighestVulnerability()); + } + + private void assertEmptyReport(AnalysisReportValue report) { + assertNotNull(report); + + assertEquals(0, report.getSummary().getDependencies().getScanned()); + assertEquals(0, report.getSummary().getDependencies().getTransitive()); + + assertNoVulnerabilities(report); + assertEquals(Collections.emptyList(), report.getDependencies()); + } + + private void assertNoVulnerabilities(AnalysisReportValue report) { + assertEquals(0, report.getSummary().getVulnerabilities().getTotal()); + assertEquals(0, report.getSummary().getVulnerabilities().getHigh()); + assertEquals(0, report.getSummary().getVulnerabilities().getCritical()); + assertEquals(0, report.getSummary().getVulnerabilities().getDirect()); + assertEquals(0, report.getSummary().getVulnerabilities().getMedium()); + assertEquals(0, report.getSummary().getVulnerabilities().getLow()); + + assertOkStatus(report); + } + + private void assertOkStatus(AnalysisReportValue report) { + assertEquals(Boolean.TRUE, report.getStatus().getOk()); + assertEquals(Status.OK.getStatusCode(), report.getStatus().getCode()); + assertEquals(Status.OK.getReasonPhrase(), report.getStatus().getMessage()); + assertEquals("simple", report.getStatus().getName()); + } + + private static final Map> DEFAULT_TREE = + Map.of( + "com.example:dep-a:jar:1.0.0", + List.of("com.example:dep-a-a:jar:1.0.0", "com.example:dep-a-b:jar:1.0.0"), + "com.example:dep-b:jar:1.0.0", + List.of("com.example:dep-b-a:jar:1.0.0", "com.example:dep-b-b:jar:1.0.0"), + "com.example:dep-c:jar:1.0.0", + Collections.emptyList()); + + private static DependencyTree buildTree() { + Map deps = new HashMap<>(); + DEFAULT_TREE.entrySet().stream() + .sorted(Comparator.comparing(Entry::getKey)) + .forEach( + e -> { + PackageRef ref = PackageRef.parse(e.getKey(), Constants.MAVEN_PKG_MANAGER); + Set transitive = + e.getValue().stream() + .sorted() + .map(s -> PackageRef.parse(s, Constants.MAVEN_PKG_MANAGER)) + .collect(Collectors.toSet()); + deps.put(ref, DirectDependency.builder().ref(ref).transitive(transitive).build()); + }); + + DependencyTree tree = + DependencyTree.builder() + .root(DependencyTree.getDefaultRoot(Constants.MAVEN_PKG_MANAGER)) + .dependencies(deps) + .build(); + return tree; + } + + private static Map> buildIssues() { + Map> issues = new HashMap<>(); + issues.put("com.example:dep-a-a", List.of(buildIssue(5), buildIssue(10), buildIssue(8))); + issues.put("com.example:dep-c", List.of(buildIssue(3))); + + return issues; + } + + static final class SimpleProviderAggregator extends ProviderAggregator { + + @Override + protected String getProviderName() { + return "simple"; + } + } +} diff --git a/src/test/java/com/redhat/exhort/integration/report/ReportTransformerTest.java b/src/test/java/com/redhat/exhort/integration/report/ReportTransformerTest.java deleted file mode 100644 index 700c6a66..00000000 --- a/src/test/java/com/redhat/exhort/integration/report/ReportTransformerTest.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * Copyright 2023 Red Hat, Inc. and/or its affiliates - * and other contributors as indicated by the @author tags. - * - * 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. - */ - -package com.redhat.exhort.integration.report; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.junit.jupiter.api.Test; - -import com.redhat.exhort.api.AnalysisReport; -import com.redhat.exhort.api.Issue; -import com.redhat.exhort.api.PackageRef; -import com.redhat.exhort.api.Severity; -import com.redhat.exhort.integration.Constants; -import com.redhat.exhort.model.DependencyTree; -import com.redhat.exhort.model.DirectDependency; -import com.redhat.exhort.model.GraphRequest; - -public class ReportTransformerTest { - - @Test - public void testFilterDepsWithoutIssues() { - Map> issues = Map.of("aa", List.of(buildIssue(1, 5f))); - GraphRequest req = - new GraphRequest.Builder(Constants.NPM_PKG_MANAGER, List.of(Constants.SNYK_PROVIDER)) - .tree(buildTree()) - .issues(issues) - .build(); - - AnalysisReport report = new ReportTransformer().transform(req); - - assertNotNull(report); - assertEquals(1, report.getDependencies().size()); - - assertEquals("aa", report.getDependencies().get(0).getRef().name()); - } - - @Test - public void testFilterTransitiveWithoutIssues() { - Map> issues = - Map.of( - "aa", List.of(buildIssue(1, 5f)), - "aaa", List.of(buildIssue(2, 5f)), - "aba", List.of(buildIssue(3, 5f))); - GraphRequest req = - new GraphRequest.Builder(Constants.NPM_PKG_MANAGER, List.of(Constants.SNYK_PROVIDER)) - .tree(buildTree()) - .issues(issues) - .build(); - - AnalysisReport report = new ReportTransformer().transform(req); - - assertNotNull(report); - assertEquals(2, report.getDependencies().size()); - - assertTrue(report.getDependencies().stream().anyMatch(d -> d.getRef().name().equals("aa"))); - assertTrue(report.getDependencies().stream().anyMatch(d -> d.getRef().name().equals("aa"))); - - assertEquals(1, report.getDependencies().get(0).getTransitive().size()); - assertEquals(1, report.getDependencies().get(1).getTransitive().size()); - } - - @Test - public void testFilterRecommendations() { - Map recommendations = - Map.of( - "aa:1", - PackageRef.builder() - .name("aa") - .version("1.redhat-0001") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build()); - GraphRequest req = - new GraphRequest.Builder(Constants.NPM_PKG_MANAGER, List.of(Constants.SNYK_PROVIDER)) - .tree(buildTree()) - .recommendations(recommendations) - .build(); - - AnalysisReport report = new ReportTransformer().transform(req); - - assertNotNull(report); - assertTrue(report.getDependencies().isEmpty()); - } - - private DependencyTree buildTree() { - Map direct = - Map.of( - PackageRef.builder() - .name("aa") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build(), - DirectDependency.builder() - .ref( - PackageRef.builder() - .name("aa") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build()) - .transitive( - Set.of( - PackageRef.builder() - .name("aaa") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build(), - PackageRef.builder() - .name("aab") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build())) - .build(), - PackageRef.builder() - .name("ab") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build(), - DirectDependency.builder() - .ref( - PackageRef.builder() - .name("ab") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build()) - .transitive( - Set.of( - PackageRef.builder() - .name("aba") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build(), - PackageRef.builder() - .name("abb") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build(), - PackageRef.builder() - .name("abc") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build())) - .build()); - return DependencyTree.builder() - .root( - PackageRef.builder() - .name("a") - .version("1") - .pkgManager(Constants.NPM_PKG_MANAGER) - .build()) - .dependencies(direct) - .build(); - } - - private Issue buildIssue(int id, Float score) { - return new Issue() - .id(String.format("ISSUE-00%d", id)) - .title(String.format("ISSUE Example 00%d", id)) - .source(Constants.SNYK_PROVIDER) - .severity(Severity.values()[id % Severity.values().length]) - .cvssScore(score); - } -} diff --git a/src/test/resources/__files/reports/report_all_no_snyk_token.html b/src/test/resources/__files/reports/report_all_no_snyk_token.html index e3b5d663..f3e73955 100644 --- a/src/test/resources/__files/reports/report_all_no_snyk_token.html +++ b/src/test/resources/__files/reports/report_all_no_snyk_token.html @@ -130,6 +130,7 @@ +
@@ -171,7 +172,7 @@ - Vulnerable Dependencies: 2 + Vulnerable Dependencies: 0

@@ -312,12 +313,10 @@ CVE-2020-36518 - - - - + + SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244 + @@ -383,10 +382,12 @@ CVE-2022-42003 - - SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426 - + + + + @@ -438,11 +439,6 @@ -
- - - 1 Transitive -
@@ -507,12 +503,10 @@ CVE-2022-41946 - - - - + + SNYK-JAVA-ORGPOSTGRESQL-3146847 + diff --git a/src/test/resources/__files/reports/report_all_no_snyk_token.json b/src/test/resources/__files/reports/report_all_no_snyk_token.json index a1e23dc2..f20f5fa7 100644 --- a/src/test/resources/__files/reports/report_all_no_snyk_token.json +++ b/src/test/resources/__files/reports/report_all_no_snyk_token.json @@ -1,38 +1,101 @@ { - "summary": { - "dependencies": { - "scanned": 2, - "transitive": 7 + "snyk": { + "status": { + "ok": true, + "name": "snyk", + "code": 200, + "message": "OK" }, - "vulnerabilities": { - "total": 4, - "direct": 2, - "critical": 0, - "high": 1, - "medium": 3, - "low": 0 + "summary": { + "dependencies": { + "scanned": 2, + "transitive": 7 + }, + "vulnerabilities": { + "direct": 0, + "total": 4, + "critical": 0, + "high": 1, + "medium": 3, + "low": 0 + } }, - "providerStatuses": [ + "dependencies": [ { - "ok": true, - "provider": "snyk", - "status": 200, - "message": "OK" - } - ] - }, - "dependencies": [ - { - "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final", - "issues": [], - "transitive": [ - { - "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", - "issues": [ - { + "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final", + "issues": [], + "transitive": [ + { + "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "issues": [ + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2020-36518" + ], + "unique": false + }, + { + "id": "SNYK-PRIVATE-VULNERABILITY", + "title": "Sign up for a free Snyk account to learn aboutn the vulnerabilities found", + "cvss": null, + "cvssScore": 5.9, + "severity": "MEDIUM", + "cves": null, + "unique": true + }, + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "High", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": "Proof of concept code", + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" + }, + "cvssScore": 5.9, + "severity": "MEDIUM", + "cves": [ + "CVE-2022-42003" + ], + "unique": false + } + ], + "remediations": { + "CVE-2022-42003": { + "issueRef": "CVE-2022-42003", + "mavenPackage": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1.Final-redhat-00002", + "productStatus": "known_affected" + } + }, + "highestVulnerability": { "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", "title": "Denial of Service (DoS)", - "source": "snyk", "cvss": { "attackVector": "Network", "attackComplexity": "Low", @@ -53,116 +116,72 @@ "CVE-2020-36518" ], "unique": false - }, - { - "id": "SNYK-PRIVATE-VULNERABILITY", - "title": "Sign up for a free Snyk account to learn aboutn the vulnerabilities found", - "source": "snyk", - "cvss": null, - "cvssScore": 5.9, - "severity": "MEDIUM", - "cves": null, - "unique": true - }, - { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "High", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": "Proof of concept code", - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" - }, - "cvssScore": 5.9, - "severity": "MEDIUM", - "cves": [ - "CVE-2022-42003" - ], - "unique": false - } - ], - "remediations": { - "CVE-2020-36518": { - "issueRef": "CVE-2020-36518", - "mavenPackage": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1.Final-redhat-00002", - "productStatus": "known_affected" } - }, - "highestVulnerability": { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "Low", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" - }, - "cvssScore": 7.5, - "severity": "HIGH", - "cves": [ - "CVE-2020-36518" - ], - "unique": false } - } - ], - "recommendation": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.redhat-00001", - "remediations": {}, - "highestVulnerability": { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "Low", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" - }, - "cvssScore": 7.5, - "severity": "HIGH", - "cves": [ - "CVE-2020-36518" ], - "unique": false - } - }, - { - "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final", - "issues": [], - "transitive": [ - { - "ref": "pkg:maven/org.postgresql/postgresql@42.5.0", - "issues": [ - { + "recommendation": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.redhat-00001", + "remediations": {}, + "highestVulnerability": { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2020-36518" + ], + "unique": false + } + }, + { + "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final", + "issues": [], + "transitive": [ + { + "ref": "pkg:maven/org.postgresql/postgresql@42.5.0", + "issues": [ + { + "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", + "title": "Information Exposure", + "cvss": { + "attackVector": "Local", + "attackComplexity": "High", + "privilegesRequired": "Low", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "High", + "integrityImpact": "None", + "availabilityImpact": "None", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" + }, + "cvssScore": 4.7, + "severity": "MEDIUM", + "cves": [ + "CVE-2022-41946" + ], + "unique": false + } + ], + "remediations": {}, + "highestVulnerability": { "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", "title": "Information Exposure", - "source": "snyk", "cvss": { "attackVector": "Local", "attackComplexity": "High", @@ -184,68 +203,35 @@ ], "unique": false } - ], - "remediations": { - "CVE-2022-41946": { - "issueRef": "CVE-2022-41946", - "mavenPackage": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.7.Final-redhat-00003", - "productStatus": "fixed" - } - }, - "highestVulnerability": { - "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", - "title": "Information Exposure", - "source": "snyk", - "cvss": { - "attackVector": "Local", - "attackComplexity": "High", - "privilegesRequired": "Low", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "High", - "integrityImpact": "None", - "availabilityImpact": "None", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" - }, - "cvssScore": 4.7, - "severity": "MEDIUM", - "cves": [ - "CVE-2022-41946" - ], - "unique": false } - } - ], - "recommendation": null, - "remediations": {}, - "highestVulnerability": { - "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", - "title": "Information Exposure", - "source": "snyk", - "cvss": { - "attackVector": "Local", - "attackComplexity": "High", - "privilegesRequired": "Low", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "High", - "integrityImpact": "None", - "availabilityImpact": "None", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" - }, - "cvssScore": 4.7, - "severity": "MEDIUM", - "cves": [ - "CVE-2022-41946" ], - "unique": false + "recommendation": null, + "remediations": {}, + "highestVulnerability": { + "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", + "title": "Information Exposure", + "cvss": { + "attackVector": "Local", + "attackComplexity": "High", + "privilegesRequired": "Low", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "High", + "integrityImpact": "None", + "availabilityImpact": "None", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" + }, + "cvssScore": 4.7, + "severity": "MEDIUM", + "cves": [ + "CVE-2022-41946" + ], + "unique": false + } } - } - ] + ] + } } \ No newline at end of file diff --git a/src/test/resources/__files/reports/report_all_token.html b/src/test/resources/__files/reports/report_all_token.html index cb471017..e0053008 100644 --- a/src/test/resources/__files/reports/report_all_token.html +++ b/src/test/resources/__files/reports/report_all_token.html @@ -130,6 +130,7 @@ +
@@ -160,7 +161,7 @@ - Total Vulnerabilities: 7 + Total Vulnerabilities: 4

@@ -171,7 +172,7 @@ - Vulnerable Dependencies: 2 + Vulnerable Dependencies: 0

@@ -215,7 +216,7 @@ 0 - 6 + 3 - - - CVE-2022-42003 - - - - CVE-2022-42003 - - - - - - - - high - - - - No known exploit - [CVE-2022-42004] CWE-502: Deserialization of Untrusted Data - -
-
7.5/10
-
-
-
-
-
- - - CVE-2022-42004 - - - - CVE-2022-42004 + SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244 @@ -555,11 +436,6 @@ -
- - - 1 Transitive -
@@ -624,12 +500,10 @@ CVE-2022-41946 - - - - + + SNYK-JAVA-ORGPOSTGRESQL-3146847 + diff --git a/src/test/resources/__files/reports/report_all_token.json b/src/test/resources/__files/reports/report_all_token.json index 3001405f..eef49866 100644 --- a/src/test/resources/__files/reports/report_all_token.json +++ b/src/test/resources/__files/reports/report_all_token.json @@ -1,69 +1,110 @@ { - "summary": { - "dependencies": { - "scanned": 2, - "transitive": 7 + "oss-index": { + "status": { + "ok": true, + "name": "oss-index", + "code": 200, + "message": "OK" }, - "vulnerabilities": { - "direct": 2, - "total": 7, - "critical": 0, - "high": 4, - "medium": 3, - "low": 0 - }, - "providerStatuses": [ - { - "ok": true, - "provider": "snyk", - "status": 200, - "message": "OK" + "summary": { + "dependencies": { + "scanned": 2, + "transitive": 7 }, - { - "ok": true, - "provider": "oss-index", - "status": 200, - "message": "OK" + "vulnerabilities": { + "direct": 0, + "total": 3, + "critical": 0, + "high": 3, + "medium": 0, + "low": 0 } - ] - }, - "dependencies": [ - { - "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final", - "issues": [], - "transitive": [ - { - "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", - "issues": [ - { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "Low", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "dependencies": [ + { + "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final", + "issues": [], + "transitive": [ + { + "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "issues": [ + { + "id": "CVE-2020-36518", + "title": "[CVE-2020-36518] CWE-787: Out-of-bounds Write", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2020-36518" + ], + "unique": false }, - "cvssScore": 7.5, - "severity": "HIGH", - "cves": [ - "CVE-2020-36518" - ], - "unique": false - }, - { + { + "id": "CVE-2022-42003", + "title": "[CVE-2022-42003] CWE-502: Deserialization of Untrusted Data", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2022-42003" + ], + "unique": false + }, + { + "id": "CVE-2022-42004", + "title": "[CVE-2022-42004] CWE-502: Deserialization of Untrusted Data", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2022-42004" + ], + "unique": false + } + ], + "remediations": {}, + "highestVulnerability": { "id": "CVE-2020-36518", "title": "[CVE-2020-36518] CWE-787: Out-of-bounds Write", - "source": "oss-index", "cvss": { "attackVector": "Network", "attackComplexity": "Low", @@ -84,36 +125,142 @@ "CVE-2020-36518" ], "unique": false - }, - { - "id": "CVE-2022-42003", - "title": "[CVE-2022-42003] CWE-502: Deserialization of Untrusted Data", - "source": "oss-index", - "cvss": { - "attackVector": "Network", - "attackComplexity": "Low", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + } + } + ], + "recommendation": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.redhat-00001", + "remediations": {}, + "highestVulnerability": { + "id": "CVE-2020-36518", + "title": "[CVE-2020-36518] CWE-787: Out-of-bounds Write", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2020-36518" + ], + "unique": false + } + } + ] + }, + "snyk": { + "status": { + "ok": true, + "name": "snyk", + "code": 200, + "message": "OK" + }, + "summary": { + "dependencies": { + "scanned": 2, + "transitive": 7 + }, + "vulnerabilities": { + "direct": 0, + "total": 4, + "critical": 0, + "high": 1, + "medium": 3, + "low": 0 + } + }, + "dependencies": [ + { + "ref": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.Final", + "issues": [], + "transitive": [ + { + "ref": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1", + "issues": [ + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2020-36518" + ], + "unique": false }, - "cvssScore": 7.5, - "severity": "HIGH", - "cves": [ - "CVE-2022-42003" - ], - "unique": false - }, - { - "id": "CVE-2022-42004", - "title": "[CVE-2022-42004] CWE-502: Deserialization of Untrusted Data", - "source": "oss-index", + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038424", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "High", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": "Proof of concept code", + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" + }, + "cvssScore": 5.9, + "severity": "MEDIUM", + "cves": [], + "unique": true + }, + { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "High", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": "Proof of concept code", + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" + }, + "cvssScore": 5.9, + "severity": "MEDIUM", + "cves": [ + "CVE-2022-42003" + ], + "unique": false + } + ], + "remediations": {}, + "highestVulnerability": { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", + "title": "Denial of Service (DoS)", "cvss": { "attackVector": "Network", "attackComplexity": "Low", @@ -131,132 +278,75 @@ "cvssScore": 7.5, "severity": "HIGH", "cves": [ - "CVE-2022-42004" - ], - "unique": false - }, - { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038424", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "High", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": "Proof of concept code", - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" - }, - "cvssScore": 5.9, - "severity": "MEDIUM", - "cves": [], - "unique": true - }, - { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-3038426", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "High", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": "Proof of concept code", - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H/E:P" - }, - "cvssScore": 5.9, - "severity": "MEDIUM", - "cves": [ - "CVE-2022-42003" + "CVE-2020-36518" ], "unique": false } - ], - "remediations": { - "CVE-2020-36518": { - "issueRef": "CVE-2020-36518", - "mavenPackage": "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.13.1.Final-redhat-00002", - "productStatus": "known_affected" - } - }, - "highestVulnerability": { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "Low", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" - }, - "cvssScore": 7.5, - "severity": "HIGH", - "cves": [ - "CVE-2020-36518" - ], - "unique": false } - } - ], - "recommendation": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.redhat-00001", - "remediations": {}, - "highestVulnerability": { - "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", - "title": "Denial of Service (DoS)", - "source": "snyk", - "cvss": { - "attackVector": "Network", - "attackComplexity": "Low", - "privilegesRequired": "None", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "None", - "integrityImpact": "None", - "availabilityImpact": "High", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" - }, - "cvssScore": 7.5, - "severity": "HIGH", - "cves": [ - "CVE-2020-36518" ], - "unique": false - } - }, - { - "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final", - "issues": [], - "transitive": [ - { - "ref": "pkg:maven/org.postgresql/postgresql@42.5.0", - "issues": [ - { + "recommendation": "pkg:maven/io.quarkus/quarkus-hibernate-orm@2.13.5.redhat-00001", + "remediations": {}, + "highestVulnerability": { + "id": "SNYK-JAVA-COMFASTERXMLJACKSONCORE-2421244", + "title": "Denial of Service (DoS)", + "cvss": { + "attackVector": "Network", + "attackComplexity": "Low", + "privilegesRequired": "None", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "None", + "integrityImpact": "None", + "availabilityImpact": "High", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H" + }, + "cvssScore": 7.5, + "severity": "HIGH", + "cves": [ + "CVE-2020-36518" + ], + "unique": false + } + }, + { + "ref": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.5.Final", + "issues": [], + "transitive": [ + { + "ref": "pkg:maven/org.postgresql/postgresql@42.5.0", + "issues": [ + { + "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", + "title": "Information Exposure", + "cvss": { + "attackVector": "Local", + "attackComplexity": "High", + "privilegesRequired": "Low", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "High", + "integrityImpact": "None", + "availabilityImpact": "None", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" + }, + "cvssScore": 4.7, + "severity": "MEDIUM", + "cves": [ + "CVE-2022-41946" + ], + "unique": false + } + ], + "remediations": {}, + "highestVulnerability": { "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", "title": "Information Exposure", - "source": "snyk", "cvss": { "attackVector": "Local", "attackComplexity": "High", @@ -278,68 +368,35 @@ ], "unique": false } - ], - "remediations": { - "CVE-2022-41946": { - "issueRef": "CVE-2022-41946", - "mavenPackage": "pkg:maven/io.quarkus/quarkus-jdbc-postgresql@2.13.7.Final-redhat-00003", - "productStatus": "fixed" - } - }, - "highestVulnerability": { - "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", - "title": "Information Exposure", - "source": "snyk", - "cvss": { - "attackVector": "Local", - "attackComplexity": "High", - "privilegesRequired": "Low", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "High", - "integrityImpact": "None", - "availabilityImpact": "None", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" - }, - "cvssScore": 4.7, - "severity": "MEDIUM", - "cves": [ - "CVE-2022-41946" - ], - "unique": false } - } - ], - "recommendation": null, - "remediations": {}, - "highestVulnerability": { - "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", - "title": "Information Exposure", - "source": "snyk", - "cvss": { - "attackVector": "Local", - "attackComplexity": "High", - "privilegesRequired": "Low", - "userInteraction": "None", - "scope": "Unchanged", - "confidentialityImpact": "High", - "integrityImpact": "None", - "availabilityImpact": "None", - "exploitCodeMaturity": null, - "remediationLevel": null, - "reportConfidence": null, - "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" - }, - "cvssScore": 4.7, - "severity": "MEDIUM", - "cves": [ - "CVE-2022-41946" ], - "unique": false + "recommendation": null, + "remediations": {}, + "highestVulnerability": { + "id": "SNYK-JAVA-ORGPOSTGRESQL-3146847", + "title": "Information Exposure", + "cvss": { + "attackVector": "Local", + "attackComplexity": "High", + "privilegesRequired": "Low", + "userInteraction": "None", + "scope": "Unchanged", + "confidentialityImpact": "High", + "integrityImpact": "None", + "availabilityImpact": "None", + "exploitCodeMaturity": null, + "remediationLevel": null, + "reportConfidence": null, + "cvss": "CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N" + }, + "cvssScore": 4.7, + "severity": "MEDIUM", + "cves": [ + "CVE-2022-41946" + ], + "unique": false + } } - } - ] + ] + } } \ No newline at end of file diff --git a/src/test/resources/__files/reports/report_error.html b/src/test/resources/__files/reports/report_error.html index 7940d177..617f2b06 100644 --- a/src/test/resources/__files/reports/report_error.html +++ b/src/test/resources/__files/reports/report_error.html @@ -130,6 +130,7 @@ +
diff --git a/src/test/resources/__files/reports/report_forbidden.html b/src/test/resources/__files/reports/report_forbidden.html index 53274404..6efe9dbc 100644 --- a/src/test/resources/__files/reports/report_forbidden.html +++ b/src/test/resources/__files/reports/report_forbidden.html @@ -130,6 +130,7 @@ +
diff --git a/src/test/resources/__files/reports/report_unauthorized.html b/src/test/resources/__files/reports/report_unauthorized.html index 24a5a15e..3d0a55a0 100644 --- a/src/test/resources/__files/reports/report_unauthorized.html +++ b/src/test/resources/__files/reports/report_unauthorized.html @@ -130,6 +130,7 @@ +
diff --git a/src/test/resources/__files/trustedcontent/oss_vulns_vex_request.json b/src/test/resources/__files/trustedcontent/oss_vulns_vex_request.json index c370e0a7..e72f5c27 100644 --- a/src/test/resources/__files/trustedcontent/oss_vulns_vex_request.json +++ b/src/test/resources/__files/trustedcontent/oss_vulns_vex_request.json @@ -1,7 +1,7 @@ { "cves": [ - "CVE-2020-36518", "CVE-2022-42003", - "CVE-2022-42004" + "CVE-2022-41946", + "CVE-2020-36518" ] } \ No newline at end of file diff --git a/src/test/resources/__files/trustedcontent/short_vex_report.json b/src/test/resources/__files/trustedcontent/short_vex_report.json index 90a33423..86948eb6 100644 --- a/src/test/resources/__files/trustedcontent/short_vex_report.json +++ b/src/test/resources/__files/trustedcontent/short_vex_report.json @@ -1,20 +1,19 @@ [ - { - "mavenPackage": { - "artifactId": "quarkus-jdbc-postgresql", - "groupId": "io.quarkus", - "version": "2.13.7.Final-redhat-00003" - }, - "productStatus": "fixed" - }, { "mavenPackage": { - "artifactId": "jackson-databind", - "groupId": "com.fasterxml.jackson.core", - "version": "2.13.1.Final-redhat-00002" + "artifactId": "jackson-databind", + "groupId": "com.fasterxml.jackson.core", + "version": "2.13.1.Final-redhat-00002" }, "productStatus": "known_affected" }, null, - null + { + "mavenPackage": { + "artifactId": "quarkus-jdbc-postgresql", + "groupId": "io.quarkus", + "version": "2.13.7.Final-redhat-00003" + }, + "productStatus": "fixed" + } ] \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties index 384f2459..c9faf49a 100644 --- a/src/test/resources/application.properties +++ b/src/test/resources/application.properties @@ -1,2 +1,2 @@ -#quarkus.log.level=DEBUG +quarkus.log.level=DEBUG quarkus.log.category."com.github.tomakehurst".level=DEBUG \ No newline at end of file