From 69c443add63241f0a305c26c9794ac9573ad6a78 Mon Sep 17 00:00:00 2001 From: Dan Schuman Date: Tue, 14 Jul 2026 15:20:07 -0700 Subject: [PATCH 1/2] [DAT-707] Add Harness integration support to the relay agent ## Claude description > Registers `harness` as a valid relay integration and adds its accept > file, the first one to authenticate via header injection instead of > basic/bearer `auth`: the route sends `any /*` to `${HARNESS_API}` and > injects `x-api-key` from `${HARNESS_TOKEN}` through the accept-file > `headers` mechanism (requires reflector traffic mode, the default). > > Adds `config.harness.json` with a `GET $HARNESS_API/ng/api/user/currentUser` > credential-validation endpoint; the final endpoint choice is being > verified in brain-backend work (DAT-708). Note the broker systemcheck > can only send validation credentials in the `Authorization` header > today, so this validation will not authenticate against Harness until > the broker fork supports custom validation header names. > > Tests: accept-file load/render + missing-var + validation-config cases > in `common`, and a live reflector test proving the shipped accept file > injects `x-api-key` on proxied requests and that an inbound placeholder > `x-api-key` is replaced rather than duplicated. > > Also lists Harness in both READMEs. Co-Authored-By: Claude Fable 5 --- README.md | 1 + README.relay.md | 1 + agent/common/integration.go | 3 +- agent/common/integration_test.go | 34 +++++++ .../accept_files/accept.harness.json | 12 +++ .../accept_files/config.harness.json | 11 +++ .../reflector_headers_harness_test.go | 96 +++++++++++++++++++ 7 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 agent/server/snykbroker/accept_files/accept.harness.json create mode 100644 agent/server/snykbroker/accept_files/config.harness.json create mode 100644 agent/server/snykbroker/reflector_headers_harness_test.go diff --git a/README.md b/README.md index e180617..989d1e2 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Axon Relay currently supports access to: * Jira * SonarQube * Prometheus +* Harness For details on Relay, see [here](README.relay.md). diff --git a/README.relay.md b/README.relay.md index eebadf1..89c38cb 100644 --- a/README.relay.md +++ b/README.relay.md @@ -85,6 +85,7 @@ Generally the naming works like: | **Bitbucket Hosted** | `BITBUCKET_API=https://bitbucket.mycompany.com`, `BITBUCKET_USERNAME`, `BITBUCKET_PASSWORD` | | **Jira** | `JIRA_API=https://jira.mycompany.com`, `JIRA_USERNAME`, `JIRA_TOKEN` | | **Jira Bearer/Cloud** | Arg `-s bearer`, `JIRA_API=https://mycompany.atlassian.com`, `JIRA_TOKEN` | +| **Harness** | `HARNESS_API=https://app.harness.io`, `HARNESS_TOKEN` | ## How it works diff --git a/agent/common/integration.go b/agent/common/integration.go index c27ed4b..cd1bfdf 100644 --- a/agent/common/integration.go +++ b/agent/common/integration.go @@ -27,6 +27,7 @@ const ( IntegrationSonarqube Integration = "sonarqube" IntegrationBitbucket Integration = "bitbucket" IntegrationPrometheus Integration = "prometheus" + IntegrationHarness Integration = "harness" ) var subtypes = map[Integration][]string{ @@ -59,7 +60,7 @@ func ParseIntegration(s string) (Integration, error) { } func ValidIntegrations() []Integration { - return []Integration{IntegrationCustom, IntegrationGithub, IntegrationJira, IntegrationGitlab, IntegrationBitbucket, IntegrationSonarqube, IntegrationPrometheus} + return []Integration{IntegrationCustom, IntegrationGithub, IntegrationJira, IntegrationGitlab, IntegrationBitbucket, IntegrationSonarqube, IntegrationPrometheus, IntegrationHarness} } type IntegrationInfo struct { diff --git a/agent/common/integration_test.go b/agent/common/integration_test.go index 58494da..eeb7ddf 100644 --- a/agent/common/integration_test.go +++ b/agent/common/integration_test.go @@ -140,6 +140,40 @@ func TestLoadIntegrationAcceptFilePoolVars(t *testing.T) { require.NotContains(t, string(contents), "GITHUB_TOKEN_POOL") } +func TestLoadIntegrationAcceptFileHarness(t *testing.T) { + os.Setenv("HARNESS_API", "https://app.harness.io") + os.Setenv("HARNESS_TOKEN", "the-harness-token") + + acceptFile, err := loadAcceptFile(t, IntegrationHarness) + require.NoError(t, err) + contents, err := acceptFile.Render(zap.NewNop()) + require.NoError(t, err) + require.Contains(t, string(contents), `"x-api-key":"${HARNESS_TOKEN}"`) + require.Contains(t, string(contents), `"origin":"${HARNESS_API}"`) +} + +func TestLoadIntegrationAcceptFileHarnessMissingVars(t *testing.T) { + os.Setenv("HARNESS_API", "") + os.Setenv("HARNESS_TOKEN", "") + + acceptFile, err := loadAcceptFile(t, IntegrationHarness) + require.Error(t, err) + require.Contains(t, err.Error(), "HARNESS_API") + require.Empty(t, acceptFile) +} + +func TestLoadValidationParamsHarness(t *testing.T) { + ii := IntegrationInfo{ + Integration: IntegrationHarness, + } + + validationParams := ii.GetValidationConfig() + require.NotNil(t, validationParams) + require.Equal(t, "$HARNESS_API/ng/api/user/currentUser", validationParams.URL) + require.Equal(t, "header", validationParams.Auth.Type) + require.Equal(t, "$HARNESS_TOKEN", validationParams.Auth.Value) +} + func TestGetOrigin(t *testing.T) { os.Setenv("USER", "testuser") diff --git a/agent/server/snykbroker/accept_files/accept.harness.json b/agent/server/snykbroker/accept_files/accept.harness.json new file mode 100644 index 0000000..d4cd278 --- /dev/null +++ b/agent/server/snykbroker/accept_files/accept.harness.json @@ -0,0 +1,12 @@ +{ + "private": [ + { + "method": "any", + "path": "/*", + "origin": "${HARNESS_API}", + "headers": { + "x-api-key": "${HARNESS_TOKEN}" + } + } + ] +} diff --git a/agent/server/snykbroker/accept_files/config.harness.json b/agent/server/snykbroker/accept_files/config.harness.json new file mode 100644 index 0000000..9663c66 --- /dev/null +++ b/agent/server/snykbroker/accept_files/config.harness.json @@ -0,0 +1,11 @@ +{ + "validation": [ + { + "url": "$HARNESS_API/ng/api/user/currentUser", + "auth": { + "type": "header", + "value": "$HARNESS_TOKEN" + } + } + ] +} diff --git a/agent/server/snykbroker/reflector_headers_harness_test.go b/agent/server/snykbroker/reflector_headers_harness_test.go new file mode 100644 index 0000000..e5c0c37 --- /dev/null +++ b/agent/server/snykbroker/reflector_headers_harness_test.go @@ -0,0 +1,96 @@ +package snykbroker + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/cortexapps/axon/config" + "github.com/cortexapps/axon/server/snykbroker/acceptfile" + "github.com/stretchr/testify/require" + "go.uber.org/zap" +) + +// Tests the shipped Harness accept file end-to-end through the reflector: +// the x-api-key header is injected from HARNESS_TOKEN on proxied requests, +// and an inbound placeholder x-api-key is replaced (not duplicated). +func TestHarnessAcceptFileInjectsApiKeyHeader(t *testing.T) { + + content, err := os.ReadFile("accept_files/accept.harness.json") + require.NoError(t, err) + + var receivedApiKeys []string + backendServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + receivedApiKeys = r.Header.Values("x-api-key") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status": "ok"}`)) + })) + defer backendServer.Close() + + os.Setenv("HARNESS_API", backendServer.URL) + os.Setenv("HARNESS_TOKEN", "real-harness-api-key") + defer func() { + os.Unsetenv("HARNESS_API") + os.Unsetenv("HARNESS_TOKEN") + }() + + logger := zap.NewNop() + cfg := config.AgentConfig{ + HttpRelayReflectorMode: config.RelayReflectorAllTraffic, + } + reflector := NewRegistrationReflector(RegistrationReflectorParams{ + Logger: logger, + Config: cfg, + }) + + _, err = reflector.Start() + require.NoError(t, err) + defer reflector.Stop() + + af, err := acceptfile.NewAcceptFile(content, cfg, nil) + require.NoError(t, err) + + var proxyURI string + _, err = af.Render( + logger, + func(renderContext acceptfile.RenderContext) error { + for _, entry := range renderContext.AcceptFile.PrivateRules() { + originalURI := entry.Origin() + if originalURI == cfg.HttpBaseUrl() { + continue + } + newURI := reflector.ProxyURI(originalURI, WithHeadersResolver(entry.Headers())) + entry.SetOrigin(newURI) + proxyURI = newURI + } + return nil + }, + ) + require.NoError(t, err) + require.NotEmpty(t, proxyURI) + + t.Run("injects x-api-key when absent", func(t *testing.T) { + resp, err := http.Get(fmt.Sprintf("%s/ng/api/projects", proxyURI)) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, []string{"real-harness-api-key"}, receivedApiKeys) + }) + + t.Run("replaces inbound placeholder x-api-key", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/ng/api/projects", proxyURI), nil) + require.NoError(t, err) + req.Header.Set("x-api-key", "placeholder-from-cortex") + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) + // exactly one value: the placeholder is replaced, not appended to + require.Equal(t, []string{"real-harness-api-key"}, receivedApiKeys) + }) +} From 6601daebf14680166b3bd28afe2dc9826201221e Mon Sep 17 00:00:00 2001 From: Dan Schuman Date: Tue, 14 Jul 2026 16:52:26 -0700 Subject: [PATCH 2/2] [DAT-707] Pin npm and move broker images to Node 22 to fix image builds ## Claude description > CI `build` and `docker-tests` jobs fail on any branch since npm@12.0.1 > released: both broker images run `npm install --global npm@latest` on > Node 20, and npm 12 requires Node >= 22.22.2 (EBADENGINE). Unrelated to > the Harness change on this branch. > > Mirrors the fix from PR #114 exactly so the branches merge cleanly: > `NODE_VERSION` 20 -> 22 and `npm@latest` -> `npm@11.18.0` in > `docker/Dockerfile` and `agent/test/relay/Dockerfile.snyk-broker-server`. > > Verified locally: `make docker-build` and a direct build of > `Dockerfile.snyk-broker-server` both complete; `go build ./...` and > `go test ./...` still pass. Co-Authored-By: Claude Fable 5 --- agent/test/relay/Dockerfile.snyk-broker-server | 4 ++-- docker/Dockerfile | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/agent/test/relay/Dockerfile.snyk-broker-server b/agent/test/relay/Dockerfile.snyk-broker-server index cc51140..45851a4 100644 --- a/agent/test/relay/Dockerfile.snyk-broker-server +++ b/agent/test/relay/Dockerfile.snyk-broker-server @@ -1,12 +1,12 @@ FROM debian:stable-slim # Install NodeJS and Snyk Broker -ENV NODE_VERSION=20 +ENV NODE_VERSION=22 ARG SNYK_BROKER_VERSION=v1.0.6-axon RUN apt update && apt install -y git wget && \ wget -q -O - https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \ apt install -y nodejs && \ - npm install --global npm@latest typescript@4.9.3 + npm install --global npm@11.18.0 typescript@4.9.3 RUN git clone https://github.com/cortexapps/snyk-broker.git /tmp/snyk-broker && \ cd /tmp/snyk-broker && \ diff --git a/docker/Dockerfile b/docker/Dockerfile index 743b860..9a3b634 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -33,11 +33,11 @@ RUN echo "apt cache bust: $APT_CACHE_BUST" \ protobuf-compiler git python3 python3-venv wget build-essential openssl jq # Install NodeJS and Snyk Broker -ENV NODE_VERSION=20 +ENV NODE_VERSION=22 ARG SNYK_BROKER_VERSION=v1.0.16-axon RUN wget -q -O - https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && apt-get install -y nodejs -RUN npm install --global npm@latest typescript@4.9.3 +RUN npm install --global npm@11.18.0 typescript@4.9.3 RUN git clone https://github.com/cortexapps/snyk-broker.git /tmp/snyk-broker && \ cd /tmp/snyk-broker && \ git checkout ${SNYK_BROKER_VERSION} && \