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..6425a1f6 100644
--- a/README.md
+++ b/README.md
@@ -205,12 +205,25 @@ 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.
+
+Telemetry connects to [Segment](https://segment.com/) for sending events.
+The connection can be configured with the following properties.
+
+- `telemetry.disabled`: To completely disable telemetry
+- `telemetry.write-key`: Authentication key to connect to Segment
+- `quarkus.rest-client.segment-api.url`: Segment API endpoint
+
## 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/deploy/openshift/template.yaml b/deploy/openshift/template.yaml
index 63b3948f..6c096724 100644
--- a/deploy/openshift/template.yaml
+++ b/deploy/openshift/template.yaml
@@ -86,6 +86,11 @@ objects:
secretKeyRef:
name: exhort-secret
key: api-snyk-token
+ - name: TELEMETRY_WRITE_KEY
+ valueFrom:
+ secretKeyRef:
+ name: exhort-secret
+ key: telemetry-write-key
securityContext:
runAsNonRoot: true
resources:
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..0f0fcf62
--- /dev/null
+++ b/src/main/java/com/redhat/exhort/analytics/AnalyticsService.java
@@ -0,0 +1,210 @@
+/*
+ * 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.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.getProperty(Constants.REQUEST_CONTENT_PROPERTY, String.class));
+ properties.put("providers", providers);
+ properties.put("sbom", exchange.getProperty(Constants.SBOM_TYPE_PARAM, 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 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..bbae132e
--- /dev/null
+++ b/src/main/java/com/redhat/exhort/analytics/segment/AuthenticationHeaderFactory.java
@@ -0,0 +1,62 @@
+/*
+ * 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
+ .orElseThrow(
+ () ->
+ new IllegalArgumentException(
+ "Missing required telemetry.write-key"))
+ .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..84bb6e00 100644
--- a/src/main/java/com/redhat/exhort/integration/Constants.java
+++ b/src/main/java/com/redhat/exhort/integration/Constants.java
@@ -32,9 +32,11 @@ public final class Constants {
private Constants() {}
public static final String PROVIDERS_PARAM = "providers";
+ public static final String SBOM_TYPE_PARAM = "sbomType";
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 +48,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..39e6c365 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
}
@@ -134,6 +157,7 @@ private void processAnalysisRequest(Exchange exchange) {
throw new ClientErrorException(Response.Status.UNSUPPORTED_MEDIA_TYPE, e);
}
SbomParser parser = SbomParserFactory.newInstance(ct.getBaseType());
+ exchange.setProperty(Constants.SBOM_TYPE_PARAM, ct.getBaseType());
DependencyTree tree = parser.parse(exchange.getIn().getBody(InputStream.class));
List providers = exchange.getProperty(Constants.PROVIDERS_PARAM, List.class);
exchange
@@ -148,5 +172,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();
}
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 384f2459..ed2425cb 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -1,2 +1,3 @@
#quarkus.log.level=DEBUG
-quarkus.log.category."com.github.tomakehurst".level=DEBUG
\ No newline at end of file
+quarkus.log.category."com.github.tomakehurst".level=DEBUG
+telemetry.disabled=true
\ No newline at end of file