From b6de10386b236f3bfbed44ab11599263d47ea01b Mon Sep 17 00:00:00 2001 From: Ruben Romero Montes Date: Thu, 31 Aug 2023 14:35:33 +0200 Subject: [PATCH] feat: add telemetry Signed-off-by: Ruben Romero Montes --- .github/workflows/inform-cli.yml | 2 +- README.md | 10 +- api-spec/v3/openapi.yaml | 6 + pom.xml | 23 +- .../exhort/analytics/AnalyticsService.java | 213 ++++++++++++++++++ .../segment/AuthenticationHeaderFactory.java | 54 +++++ .../exhort/analytics/segment/Context.java | 21 ++ .../analytics/segment/IdentifyEvent.java | 73 ++++++ .../exhort/analytics/segment/Library.java | 21 ++ .../analytics/segment/SegmentService.java | 44 ++++ .../exhort/analytics/segment/TrackEvent.java | 73 ++++++ .../redhat/exhort/integration/Constants.java | 2 + .../backend/ExhortIntegration.java | 26 ++- .../integration/report/ReportIntegration.java | 10 +- src/main/resources/application.properties | 8 + .../exhort/integration/AnalysisTest.java | 2 + .../integration/TokenValidationTest.java | 2 +- 17 files changed, 576 insertions(+), 14 deletions(-) create mode 100644 src/main/java/com/redhat/exhort/analytics/AnalyticsService.java create mode 100644 src/main/java/com/redhat/exhort/analytics/segment/AuthenticationHeaderFactory.java create mode 100644 src/main/java/com/redhat/exhort/analytics/segment/Context.java create mode 100644 src/main/java/com/redhat/exhort/analytics/segment/IdentifyEvent.java create mode 100644 src/main/java/com/redhat/exhort/analytics/segment/Library.java create mode 100644 src/main/java/com/redhat/exhort/analytics/segment/SegmentService.java create mode 100644 src/main/java/com/redhat/exhort/analytics/segment/TrackEvent.java diff --git a/.github/workflows/inform-cli.yml b/.github/workflows/inform-cli.yml index 55694924..169f670a 100644 --- a/.github/workflows/inform-cli.yml +++ b/.github/workflows/inform-cli.yml @@ -19,7 +19,7 @@ jobs: with: github-token: ${{ secrets.CRDA_CLI_REPO_PAT }} script: | - ['crda-java-api', 'crda-javascript-api'].forEach(async repo => { + ['exhort-java-api', 'exhort-javascript-api'].forEach(async repo => { await github.rest.repos.createDispatchEvent({ owner: "RHEcosystemAppEng", repo: repo, diff --git a/README.md b/README.md index 15c4c2c0..3bb11537 100644 --- a/README.md +++ b/README.md @@ -205,12 +205,18 @@ http -v :8080/api/v3/token ex-snyk-token==example-token The possible responses are: - 200 - Token validated successfully -- 400 - Missing authentication header -- 401 - Invalid auth token provided +- 400 - Missing provider authentication headers +- 401 - Invalid auth token provided or Missing required authentication header (rhda-token) - 403 - The token is not authorized - 429 - Rate limit exceeded - 500 - Server error +## Telemetry + +API Clients are expected to send a `rhda-token` HTTP Header that will be used to correlate +different events related to the same user. +If the header is not provided an anonymous event with a generated UUID will be sent instead. + ## Deploy on OpenShift The required parameters can be injected as environment variables through a secret. Create the `exhort-secret` Secret before deploying the application. diff --git a/api-spec/v3/openapi.yaml b/api-spec/v3/openapi.yaml index a67091e2..408a7589 100644 --- a/api-spec/v3/openapi.yaml +++ b/api-spec/v3/openapi.yaml @@ -21,6 +21,7 @@ paths: operationId: analysis summary: Takes a client-resolved dependency graph to perform a full stack analysis from all the available Vulnerability sources security: + - RhdaTokenAuth: [] - SnykTokenAuth: [] - OssIndexUserAuth: [] OssIndexTokenAuth: [] @@ -75,6 +76,7 @@ paths: operationId: validateToken summary: Validates a vulnerability provider token security: + - RhdaTokenAuth: [] - SnykTokenAuth: [] - OssIndexUserAuth: [] OssIndexTokenAuth: [] @@ -117,6 +119,10 @@ paths: components: securitySchemes: + RhdaTokenAuth: + type: apiKey + in: header + name: rhda-token SnykTokenAuth: type: apiKey in: header diff --git a/pom.xml b/pom.xml index 107a6cc3..06c92e68 100644 --- a/pom.xml +++ b/pom.xml @@ -6,6 +6,7 @@ 4.0.0 com.redhat.ecosystemappeng exhort + RHDA - Exhort 0.0.1-SNAPSHOT @@ -17,6 +18,8 @@ + ${maven.build.timestamp} + yyyy-MM-dd'T'HH:mm:ss.SSS'Z' 17 UTF-8 UTF-8 @@ -96,6 +99,14 @@ io.quarkus quarkus-micrometer-registry-prometheus + + io.quarkus + quarkus-rest-client-reactive-jackson + + + io.quarkus + quarkus-smallrye-openapi + org.apache.camel.quarkus camel-quarkus-direct @@ -118,11 +129,11 @@ org.apache.camel.quarkus - camel-quarkus-log + camel-quarkus-seda - io.quarkus - quarkus-smallrye-openapi + org.apache.camel.quarkus + camel-quarkus-log org.cyclonedx @@ -164,6 +175,12 @@ + + + src/main/resources + true + + diff --git a/src/main/java/com/redhat/exhort/analytics/AnalyticsService.java b/src/main/java/com/redhat/exhort/analytics/AnalyticsService.java new file mode 100644 index 00000000..1c919f6d --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/AnalyticsService.java @@ -0,0 +1,213 @@ +/* + * 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.analytics; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; + +import org.apache.camel.Exchange; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.redhat.exhort.analytics.segment.Context; +import com.redhat.exhort.analytics.segment.IdentifyEvent; +import com.redhat.exhort.analytics.segment.Library; +import com.redhat.exhort.analytics.segment.SegmentService; +import com.redhat.exhort.analytics.segment.TrackEvent; +import com.redhat.exhort.api.AnalysisReport; +import com.redhat.exhort.api.DependencyReport; +import com.redhat.exhort.integration.Constants; + +import io.quarkus.runtime.annotations.RegisterForReflection; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@ApplicationScoped +@RegisterForReflection +public class AnalyticsService { + + private static final Logger LOGGER = LoggerFactory.getLogger(AnalyticsService.class); + + private static final String RHDA_TOKEN = "rhda-token"; + private static final String ANONYMOUS_ID = "telemetry-anonymous-id"; + private static final String ANALYSIS_EVENT = "rhda.exhort.analysis"; + private static final String TOKEN_EVENT = "rhda.exhort.token"; + + @ConfigProperty(name = "telemetry.disabled", defaultValue = "false") + Boolean disabled; + + @ConfigProperty(name = "project.id") + String projectId; + + @ConfigProperty(name = "project.name") + String projectName; + + @ConfigProperty(name = "project.version") + String projectVersion; + + @ConfigProperty(name = "project.build") + String projectBuild; + + @RestClient SegmentService segmentService; + + public void identify(Exchange exchange) { + if (disabled) { + return; + } + + String userId = exchange.getIn().getHeader(RHDA_TOKEN, String.class); + if (userId == null) { + String anonymousId = UUID.randomUUID().toString(); + Map traits = new HashMap<>(); + traits.put("serverName", projectName); + traits.put("serverVersion", projectVersion); + traits.put("serverBuild", projectBuild); + IdentifyEvent event = + new IdentifyEvent.Builder() + .context(new Context(new Library(projectId, projectVersion))) + .anonymousId(anonymousId) + .traits(traits) + .build(); + try { + Response response = segmentService.identify(event); + if (response.getStatus() >= 400) { + LOGGER.warn( + String.format( + "Unable to send event to segment: %d - %s", + response.getStatus(), response.getStatusInfo())); + } + } catch (Exception e) { + LOGGER.warn("Unable to send event to segment", e); + } + exchange.setProperty(ANONYMOUS_ID, anonymousId); + } else { + // no need to IDENTIFY as we expect the caller to have done that already + exchange.setProperty(RHDA_TOKEN, userId); + exchange.getIn().removeHeader(RHDA_TOKEN); + } + } + + public void trackAnalysis(Exchange exchange) { + if (disabled) { + return; + } + TrackEvent.Builder builder = prepareTrackEvent(exchange, ANALYSIS_EVENT); + AnalysisReport report = exchange.getProperty(Constants.REPORT_PROPERTY, AnalysisReport.class); + Map properties = new HashMap<>(); + if (report != null) { + Map providers = new HashMap<>(); + Map reportProps = new HashMap<>(); + // TODO: Adapt after multi-source is implemented + reportProps.put("dependencies", report.getSummary().getDependencies()); + reportProps.put("vulnerabilities", report.getSummary().getVulnerabilities()); + providers.put("report", reportProps); + providers.put("provider", Constants.SNYK_PROVIDER); + providers.put("recommendations", countRecommendations(report)); + providers.put("remediations", countRemediations(report)); + properties.put( + "requestType", + exchange + .getIn() + .getHeader(Exchange.CONTENT_TYPE, MediaType.APPLICATION_JSON, String.class)); + properties.put("providers", providers); + } + try { + Response response = segmentService.track(builder.properties(properties).build()); + if (response.getStatus() >= 400) { + LOGGER.warn( + String.format( + "Unable to send event to segment: %d - %s", + response.getStatus(), response.getStatusInfo())); + } + } catch (Exception e) { + LOGGER.warn("Unable to send event to segment", e); + } + } + + public void trackToken(Exchange exchange) { + if (disabled) { + return; + } + TrackEvent.Builder builder = prepareTrackEvent(exchange, TOKEN_EVENT); + Map properties = new HashMap<>(); + properties.put("providers", exchange.getProperty(Constants.PROVIDERS_PARAM, List.class)); + properties.put( + "statusCode", exchange.getIn().getHeader(Exchange.HTTP_RESPONSE_CODE, String.class)); + try { + Response response = segmentService.track(builder.properties(properties).build()); + if (response.getStatus() >= 400) { + LOGGER.warn( + String.format( + "Unable to send event to segment: %d - %s", + response.getStatus(), response.getStatusInfo())); + } + } catch (Exception e) { + LOGGER.warn("Unable to enqueue event to segment", e); + } + } + + private TrackEvent.Builder prepareTrackEvent(Exchange exchange, String eventName) { + TrackEvent.Builder builder = new TrackEvent.Builder(eventName); + String userId = exchange.getProperty(RHDA_TOKEN, String.class); + if (userId != null) { + builder.userId(userId); + } else { + String anonymousId = exchange.getProperty(ANONYMOUS_ID, String.class); + builder.anonymousId(anonymousId); + } + return builder.context(new Context(new Library(projectId, projectVersion))); + } + + private long countRemediations(AnalysisReport report) { + AtomicLong counter = new AtomicLong(); + report + .getDependencies() + .forEach( + d -> { + if (d.getRemediations() != null) { + counter.addAndGet(d.getRemediations().size()); + } + if (d.getTransitive() != null) { + d.getTransitive() + .forEach( + t -> { + if (t.getRemediations() != null) { + counter.addAndGet(t.getRemediations().size()); + } + }); + } + }); + return counter.get(); + } + + private long countRecommendations(AnalysisReport report) { + return report.getDependencies().stream() + .map(DependencyReport::getRecommendation) + .filter(Objects::nonNull) + .count(); + } +} diff --git a/src/main/java/com/redhat/exhort/analytics/segment/AuthenticationHeaderFactory.java b/src/main/java/com/redhat/exhort/analytics/segment/AuthenticationHeaderFactory.java new file mode 100644 index 00000000..68fcee1e --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/segment/AuthenticationHeaderFactory.java @@ -0,0 +1,54 @@ +/* + * 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.analytics.segment; + +import java.util.Base64; +import java.util.Optional; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.eclipse.microprofile.rest.client.ext.ClientHeadersFactory; + +import jakarta.annotation.PostConstruct; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.core.MultivaluedHashMap; +import jakarta.ws.rs.core.MultivaluedMap; + +@ApplicationScoped +public class AuthenticationHeaderFactory implements ClientHeadersFactory { + + @ConfigProperty(name = "telemetry.write-key") + Optional writeKey; + + String basicAuthHeader; + + @PostConstruct + void initialize() { + this.basicAuthHeader = + "Basic " + Base64.getEncoder().encodeToString(writeKey.orElse("").getBytes()); + } + + @Override + public MultivaluedMap update( + MultivaluedMap incomingHeaders, + MultivaluedMap clientOutgoingHeaders) { + MultivaluedMap result = new MultivaluedHashMap<>(); + result.add("Authorization", basicAuthHeader); + return result; + } +} diff --git a/src/main/java/com/redhat/exhort/analytics/segment/Context.java b/src/main/java/com/redhat/exhort/analytics/segment/Context.java new file mode 100644 index 00000000..8f2da527 --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/segment/Context.java @@ -0,0 +1,21 @@ +/* + * 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.analytics.segment; + +public record Context(Library library) {} diff --git a/src/main/java/com/redhat/exhort/analytics/segment/IdentifyEvent.java b/src/main/java/com/redhat/exhort/analytics/segment/IdentifyEvent.java new file mode 100644 index 00000000..ec6d5ebf --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/segment/IdentifyEvent.java @@ -0,0 +1,73 @@ +/* + * 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.analytics.segment; + +import java.util.Date; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +@JsonInclude(Include.NON_NULL) +public record IdentifyEvent( + String anonymousId, + String userId, + String messageId, + Date timestamp, + Context context, + Map traits) { + + public static final class Builder { + String anonymousId; + String userId; + String messageId; + Date timestamp; + Context context; + Map traits; + + public Builder anonymousId(String anonymousId) { + this.anonymousId = anonymousId; + return this; + } + + public Builder userId(String userId) { + this.userId = userId; + return this; + } + + public Builder messageId(String messageId) { + this.messageId = messageId; + return this; + } + + public Builder context(Context context) { + this.context = context; + return this; + } + + public Builder traits(Map traits) { + this.traits = traits; + return this; + } + + public IdentifyEvent build() { + return new IdentifyEvent(anonymousId, userId, messageId, new Date(), context, traits); + } + } +} diff --git a/src/main/java/com/redhat/exhort/analytics/segment/Library.java b/src/main/java/com/redhat/exhort/analytics/segment/Library.java new file mode 100644 index 00000000..4a83afa2 --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/segment/Library.java @@ -0,0 +1,21 @@ +/* + * 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.analytics.segment; + +public record Library(String name, String version) {} diff --git a/src/main/java/com/redhat/exhort/analytics/segment/SegmentService.java b/src/main/java/com/redhat/exhort/analytics/segment/SegmentService.java new file mode 100644 index 00000000..11e8f545 --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/segment/SegmentService.java @@ -0,0 +1,44 @@ +/* + * 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.analytics.segment; + +import org.eclipse.microprofile.rest.client.annotation.RegisterClientHeaders; +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; + +@Path("/v1") +@RegisterRestClient(configKey = "segment-api") +@RegisterClientHeaders(AuthenticationHeaderFactory.class) +public interface SegmentService { + + @POST + @Path("/identify") + @Consumes(MediaType.APPLICATION_JSON) + Response identify(IdentifyEvent event); + + @POST + @Path("/track") + @Consumes(MediaType.APPLICATION_JSON) + Response track(TrackEvent event); +} diff --git a/src/main/java/com/redhat/exhort/analytics/segment/TrackEvent.java b/src/main/java/com/redhat/exhort/analytics/segment/TrackEvent.java new file mode 100644 index 00000000..2d9f9a04 --- /dev/null +++ b/src/main/java/com/redhat/exhort/analytics/segment/TrackEvent.java @@ -0,0 +1,73 @@ +/* + * 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.analytics.segment; + +import java.util.Date; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; + +@JsonInclude(Include.NON_NULL) +public record TrackEvent( + String anonymousId, + String userId, + String event, + Context context, + Date timestamp, + Map properties) { + + public static final class Builder { + String anonymousId; + String userId; + String event; + Context context; + Date timestamp; + Map properties; + + public Builder(String event) { + this.event = event; + this.timestamp = new Date(); + } + + public Builder anonymousId(String anonymousId) { + this.anonymousId = anonymousId; + return this; + } + + public Builder userId(String userId) { + this.userId = userId; + return this; + } + + public Builder context(Context context) { + this.context = context; + return this; + } + + public Builder properties(Map properties) { + this.properties = properties; + return this; + } + + public TrackEvent build() { + return new TrackEvent(anonymousId, userId, event, context, timestamp, properties); + } + } +} diff --git a/src/main/java/com/redhat/exhort/integration/Constants.java b/src/main/java/com/redhat/exhort/integration/Constants.java index c1a2653b..8c5115b1 100644 --- a/src/main/java/com/redhat/exhort/integration/Constants.java +++ b/src/main/java/com/redhat/exhort/integration/Constants.java @@ -35,6 +35,7 @@ private Constants() {} public static final String ACCEPT_HEADER = "Accept"; public static final String ACCEPT_ENCODING_HEADER = "Accept-Encoding"; + public static final String RHDA_TOKEN_HEADER = "rhda-token"; public static final String SNYK_TOKEN_HEADER = "ex-snyk-token"; public static final String OSS_INDEX_USER_HEADER = "ex-oss-index-user"; public static final String OSS_INDEX_TOKEN_HEADER = "ex-oss-index-token"; @@ -46,6 +47,7 @@ private Constants() {} public static final String SNYK_PROVIDER = "snyk"; public static final String OSS_INDEX_PROVIDER = "oss-index"; + public static final String UNKNOWN_PROVIDER = "unknown"; public static final String TRUSTED_CONTENT_NAME = "trusted-content"; 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..712e33b9 100644 --- a/src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/backend/ExhortIntegration.java @@ -21,6 +21,7 @@ import static com.redhat.exhort.integration.Constants.REQUEST_CONTENT_PROPERTY; import java.io.InputStream; +import java.util.Arrays; import java.util.List; import org.apache.camel.Exchange; @@ -30,6 +31,7 @@ import org.apache.camel.component.micrometer.MicrometerConstants; import org.apache.camel.component.micrometer.routepolicy.MicrometerRoutePolicyFactory; +import com.redhat.exhort.analytics.AnalyticsService; import com.redhat.exhort.integration.Constants; import com.redhat.exhort.integration.ProviderAggregationStrategy; import com.redhat.exhort.integration.VulnerabilityProvider; @@ -55,6 +57,8 @@ public class ExhortIntegration extends EndpointRouteBuilder { @Inject VulnerabilityProvider vulnerabilityProvider; + @Inject AnalyticsService analytics; + ExhortIntegration(MeterRegistry registry) { this.registry = registry; } @@ -92,12 +96,14 @@ public void configure() { from(direct("analysis")) .routeId("dependencyAnalysis") + .to(seda("analyticsIdentify")) .setProperty(Constants.PROVIDERS_PARAM, method(vulnerabilityProvider, "getProvidersFromQueryParam")) .setProperty(REQUEST_CONTENT_PROPERTY, method(BackendUtils.class, "getResponseMediaType")) .process(this::processAnalysisRequest) .to(direct("findVulnerabilities")) .to(direct("recommendAllTrustedContent")) .to(direct("report")) + .to(seda("analyticsTrackAnalysis")) .process(this::cleanUpHeaders); from(direct("findVulnerabilities")) @@ -108,17 +114,34 @@ public void configure() { from(direct("validateToken")) .routeId("validateToken") + .to(seda("analyticsIdentify")) .choice() .when(header(Constants.SNYK_TOKEN_HEADER).isNotNull()) + .setProperty(Constants.PROVIDERS_PARAM, constant(Arrays.asList(Constants.SNYK_PROVIDER))) .to(direct("snykValidateToken")) .when(header(Constants.OSS_INDEX_TOKEN_HEADER).isNotNull()) + .setProperty(Constants.PROVIDERS_PARAM, constant(Arrays.asList(Constants.OSS_INDEX_PROVIDER))) .to(direct("ossValidateCredentials")) .otherwise() + .setProperty(Constants.PROVIDERS_PARAM, constant(Arrays.asList(Constants.UNKNOWN_PROVIDER))) .setHeader(Exchange.HTTP_RESPONSE_CODE, constant(Response.Status.BAD_REQUEST.getStatusCode())) - .setBody(constant("Missing authentication header")) + .setBody(constant("Missing provider authentication headers")) .end() .setHeader(Exchange.CONTENT_TYPE, constant(MediaType.TEXT_PLAIN)) + .to(seda("analyticsTrackToken")) .process(this::cleanUpHeaders); + + from(seda("analyticsIdentify")) + .routeId("analyticsIdentify") + .process(analytics::identify); + + from(seda("analyticsTrackToken")) + .routeId("analyticsTrackToken") + .process(analytics::trackToken); + + from(seda("analyticsTrackAnalysis")) + .routeId("analyticsTrackAnalysis") + .process(analytics::trackAnalysis); //fmt:on } @@ -148,5 +171,6 @@ private void cleanUpHeaders(Exchange exchange) { msg.removeHeaders("ex-.*-user"); msg.removeHeaders("ex-.*-token"); msg.removeHeader("Authorization"); + msg.removeHeader(Constants.RHDA_TOKEN_HEADER); } } 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 73f2ee50..8735f8d3 100644 --- a/src/main/java/com/redhat/exhort/integration/report/ReportIntegration.java +++ b/src/main/java/com/redhat/exhort/integration/report/ReportIntegration.java @@ -48,6 +48,8 @@ public void configure() { // fmt:off from(direct("report")) .routeId("report") + .bean(ReportTransformer.class, "transform") + .setProperty(Constants.REPORT_PROPERTY, body()) .choice() .when(exchangeProperty(Constants.REQUEST_CONTENT_PROPERTY).isEqualTo(MediaType.TEXT_HTML)) .to(direct("htmlReport")) @@ -60,8 +62,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")); @@ -69,15 +69,13 @@ public void configure() { .routeId("multipartReport") .to(direct("htmlReport")) .bean(ReportTransformer.class, "attachHtmlReport") - .setBody(exchangeProperty(Constants.REPORT_PROPERTY)) - .bean(ReportTransformer.class, "filterVerboseResult") - .marshal().json() + .to(direct("jsonReport")) .marshal().mimeMultipart(false, false, true) .setHeader(Exchange.CONTENT_TYPE, constant(MediaType.TEXT_HTML)); from(direct("jsonReport")) .routeId("jsonReport") - .bean(ReportTransformer.class, "transform") + .setBody(exchangeProperty(Constants.REPORT_PROPERTY)) .bean(ReportTransformer.class, "filterVerboseResult") .marshal().json(); //fmt:on diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3901ddd5..96527019 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,10 @@ %dev.quarkus.log.level=DEBUG +project.id=${pom.groupId}:${pom.artifactId} +project.name=${pom.name} +project.version=${pom.version} +project.build=${timestamp} + ## podman run -p 18080:8080 --rm ghcr.io/seedwing-io/swio:0.1.0-trusted api.trustedContent.gav.host=http://swio.trusted-content:8080 api.trustedContent.vex.host=http://tc-camel:8080 @@ -19,6 +24,9 @@ report.vex.link=https://tc-storage-mvp.s3.amazonaws.com/vexes/ report.sbom.link=https://tc-storage-mvp.s3.amazonaws.com/sboms/sbom.json report.snyk.signup.link=https://app.snyk.io/login?utm_campaign=Code-Ready-Analytics-2020&utm_source=code_ready&code_ready=FF1B53D9-57BE-4613-96D7-1D06066C38C9 +## Segment API +quarkus.rest-client.segment-api.url=https://api.segment.io/ + ## Native configuration ## See https://quarkus.io/guides/native-and-ssl quarkus.ssl.native=true diff --git a/src/test/java/com/redhat/exhort/integration/AnalysisTest.java b/src/test/java/com/redhat/exhort/integration/AnalysisTest.java index edc3b22b..bddbe452 100644 --- a/src/test/java/com/redhat/exhort/integration/AnalysisTest.java +++ b/src/test/java/com/redhat/exhort/integration/AnalysisTest.java @@ -65,6 +65,7 @@ public class AnalysisTest extends AbstractAnalysisTest { private static final String CYCLONEDX = "cyclonedx"; private static final String SPDX = "spdx"; + private static final String DEFAULT_RHDA_TOKEN = "example-rhda-token"; @ParameterizedTest @ValueSource(strings = {CYCLONEDX, SPDX}) @@ -490,6 +491,7 @@ public void testMultipart_HttpVersions(String version) throws IOException, Inter HttpClient client = HttpClient.newHttpClient(); HttpRequest request = HttpRequest.newBuilder(URI.create("http://localhost:8081/api/v3/analysis")) + .setHeader(Constants.RHDA_TOKEN_HEADER, DEFAULT_RHDA_TOKEN) .setHeader(CONTENT_TYPE, CycloneDxMediaType.APPLICATION_CYCLONEDX_JSON) .setHeader("Accept", Constants.MULTIPART_MIXED) .setHeader(Constants.SNYK_TOKEN_HEADER, OK_TOKEN) diff --git a/src/test/java/com/redhat/exhort/integration/TokenValidationTest.java b/src/test/java/com/redhat/exhort/integration/TokenValidationTest.java index 2944536f..be2d21c5 100644 --- a/src/test/java/com/redhat/exhort/integration/TokenValidationTest.java +++ b/src/test/java/com/redhat/exhort/integration/TokenValidationTest.java @@ -50,7 +50,7 @@ public void testMissingToken() { .extract() .body() .asString(); - assertEquals("Missing authentication header", msg); + assertEquals("Missing provider authentication headers", msg); verifyNoInteractions(); }