From cc6000c49573b62859957b076ad7baff67d7ae3d Mon Sep 17 00:00:00 2001 From: Dmitrii Creed Date: Fri, 21 Aug 2026 11:26:48 +0400 Subject: [PATCH] fix(deps): close every fixable dep vuln, and lift coverage over the Gold bar it claimed OpenSSF Scorecard's Vulnerabilities check reported three advisories. All three land on dev-tool-only modules pulled in through tools.go, and all three are fixable upstream, so they are fixed rather than VEX'd: golang.org/x/mod v0.38.0 -> v0.40.0 GO-2026-6179, GO-2026-6180 (HIGH) github.com/cilium/ebpf v0.11.0 -> v0.22.0 GO-2026-6238 (LOW) Neither could be fixed by bumping its parent: gofumpt v0.11.0 (latest) still requires x/mod v0.38.0, and delve v1.27.1 (latest) still requires ebpf v0.11.0. `go list -deps ./...` links zero packages from either module into anything we ship, but a non-breaking upstream fix exists, and VEX is for findings that cannot be fixed. osv-scanner and trivy fs both go from 3 findings to 0. The ebpf bump has fallout worth calling out. ebpf v0.22.0 names honnef.co/go/tools v0.7.0 in its own go.mod, MVS raises it, and golangci-lint v1.64.8 does not compile against v0.7.0's analysis API -- it panics mid-run ("typedness: panic during analysis: interface conversion: interface {} is nil, not *buildir.IR") and exits 7, failing the lint step in branch.yaml / push.yaml. ebpf needs staticcheck for ITS CI, not for anything we compile, and a require line loses to ebpf's, so honnef is pinned back with a replace directive. Dependabot #385 makes the same ebpf bump and would have broken lint on main on its own. Images, all four rebuilt with --no-cache and rescanned: caddy 11 HIGH + 1 MEDIUM -> 1 MEDIUM cloud-helpers 8 HIGH -> 0 github-actions 17 HIGH -> unchanged (Google's gcloud binaries) kubectl 14 HIGH + 3 MEDIUM -> unchanged (upstream kubectl binary) caddy: the pinned 2.11.4-builder digest shipped Go 1.26.5 and the tag has since been rebuilt with Go 1.26.7. Since xcaddy in the builder stage produces the binary COPY --from=builder puts into the runtime image, the builder's toolchain IS the shipped stdlib -- refreshing the digest alone closed 8 stdlib CVEs. Three xcaddy --replace directives lift Caddy's own vendored x/net, x/text and grpc past CVE-2026-46600, CVE-2026-56852 and GHSA-hrxh-6v49-42gf. --replace and not --with, because --with also writes a blank import and none of those modules has a package at its root. cloud-helpers: every finding was in /usr/local/bin/aws-lambda-rie, the base image's local-testing Runtime Interface Emulator. /lambda-entrypoint.sh execs it only when AWS_LAMBDA_RUNTIME_API is unset, and our ENTRYPOINT replaces that script outright, so it never runs -- and the debug path that does use a RIE (welder run debug-aws-cloud-helpers) is runOn: host and uses the host's copy. Deleting it is the fix, not a suppression. Image now scans clean and the binary still starts. Deferred: cel-go 0.28.1 -> 0.29.0 (MEDIUM, GHSA-gcjh-h69q-9w9g) inside the caddy binary. v0.29.0 renames interpreter.Interpretable to InterpretableV2 and Caddy 2.11.4's celmatcher.go does not compile against it -- an upstream code change. Reachability and the closing condition are recorded in a caddy.Dockerfile comment so the next digest refresh does not silently add it to the --replace list and break the build. Documented unfixable, each measured rather than assumed: kubectl's own binary (pinned digest and current latest are both v1.36.3 with identical findings, so the pin is left alone), the gcloud SDK binaries (579.0.0 and 581.0.0 both ship gcloud-crc32c built with go1.26.5, so bumping the pin buys nothing), and the lambda base digest, which is deliberately NOT refreshed because the currently-tagged one is worse (30 HIGH vs 8). VEX: all three statements re-triaged against the current tree with fresh evidence, none flipped, document bumped to version 4. No new suppression was added anywhere -- no .trivyignore, no new VEX statement, no new osv-scanner.toml entry. Coverage: docs/TESTING.md claimed the Gold-tier test_statement_coverage90 criterion was Met. It was not -- the main baseline reads 89.6%. Seven test files take the included set to 90.5% and the doc now carries the number so the next drift is visible. The largest single gap was pkg/security/tools/cosigntest at 0%: Go attributes coverage per test binary and that package is a harness consumed only by other packages' tests. Excluding it would have cleared the number and been wrong -- it generates a /bin/sh script whose quoting, cross-process counter and stream selection three retry suites silently depend on, so a break there turns those suites green for the wrong reason. It gets tests instead, driven through tools.ExecCommand (the function signing.RunCosignWithRetry actually calls) so stdout and stderr stay separated the way they do in production. Also fixes two staticcheck SA1012 findings (never pass a nil Context) that surface on this branch but not on main, in the cases asserting lookupTriggeringEvents short-circuits before any AWS call -- they now pass an already-cancelled context, so a regression fails with context.Canceled instead of reaching the network. Signed-off-by: Dmitrii Creed --- caddy.Dockerfile | 22 +- cloud-helpers.aws.Dockerfile | 16 ++ docs/TESTING.md | 41 ++-- go.mod | 37 +++- go.sum | 45 ++-- pkg/clouds/aws/helpers/ct_enrichment_test.go | 13 +- pkg/clouds/cloudflare/cloudflare_test.go | 34 +++ pkg/clouds/compose/compose_test.go | 50 +++++ pkg/clouds/docker/types_test.go | 37 ++++ pkg/clouds/fs/fs_state_test.go | 38 ++++ pkg/clouds/gcloud/auth_test.go | 59 +++++ pkg/clouds/mongodb/mongodb_test.go | 48 ++++ .../tools/cosigntest/cosigntest_test.go | 208 ++++++++++++++++++ vex/openvex.json | 10 +- 14 files changed, 604 insertions(+), 54 deletions(-) create mode 100644 pkg/security/tools/cosigntest/cosigntest_test.go diff --git a/caddy.Dockerfile b/caddy.Dockerfile index 2af6e1cc..0acb3bf1 100644 --- a/caddy.Dockerfile +++ b/caddy.Dockerfile @@ -69,18 +69,36 @@ # Verify the LB is `externalTrafficPolicy: Local` + the parent # Caddy's `trustedProxies` covers the LB CIDR range. -FROM caddy:2.11.4-builder@sha256:198d47eaee306d4d0c38a9960c89ff2c959aa29ad51d3e2dafa3e93ac961782a AS builder +FROM caddy:2.11.4-builder@sha256:4bdeabce8e79d36b23d1cba7d20598cec2c1117ace960d8ca06071f945e8fc9b AS builder # `$CADDY_VERSION` is set by the base image itself (v2.11.4 here), so xcaddy # builds exactly the version the builder ships and a skew is impossible by # construction — there is no second version literal to forget. The tag on the # FROM line is informational only; the digest is what resolves. +# +# The three `--replace` lines lift Caddy 2.11.4's own vendored deps past +# CVE-2026-46600 (x/net), CVE-2026-56852 (x/text) and GHSA-hrxh-6v49-42gf +# (grpc), which upstream has not yet re-released. `--replace` and not `--with`: +# `--with` also writes a blank import, and none of these modules has a package +# at its root, so it fails with "cannot find module providing package". +# Refresh or drop each line when Caddy ships a release that already carries the +# fixed version — a replace pinning an OLDER version than upstream would +# silently downgrade. +# +# cel-go is deliberately NOT in this list. v0.29.0 renames +# interpreter.Interpretable to InterpretableV2, which does not compile against +# 2.11.4's modules/caddyhttp/celmatcher.go — an upstream code change, not a +# version bump. GHSA-gcjh-h69q-9w9g (MEDIUM) therefore stays open until Caddy +# adopts it; see the SCA PR for the reachability note. RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ --mount=type=cache,target=/root/.cache,sharing=locked \ test -n "${CADDY_VERSION}" \ && xcaddy build "${CADDY_VERSION}" \ --with github.com/grafana/certmagic-gcs@v0.1.7 \ --with github.com/mholt/caddy-ratelimit@16aecbbcb8ca07dc1c671e263379606ff9493c55 \ + --replace golang.org/x/net=golang.org/x/net@v0.58.0 \ + --replace golang.org/x/text=golang.org/x/text@v0.41.0 \ + --replace google.golang.org/grpc=google.golang.org/grpc@v1.82.1 \ && caddy version | grep -qF "${CADDY_VERSION} " \ && caddy list-modules | grep -qE '^http\.handlers\.rate_limit$' \ && caddy list-modules | grep -qE '^caddy\.storage\.gcs$' @@ -93,7 +111,7 @@ RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ # falls back to local-filesystem cert storage, so a multi-replica parent # stack gets per-pod ACME state and risks Let's Encrypt rate-limit lockout. -FROM caddy:2.11.4@sha256:844f60b64e4724a5aa8245e019dace0d3f199f7433ce6c57676cb30a920dbad9 +FROM caddy:2.11.4@sha256:df7f1c2fb114453b951de51a98efc010db1655a92c2e86be6706714e2417a78d RUN apk update && apk upgrade --no-cache && rm -rf /var/cache/apk/* diff --git a/cloud-helpers.aws.Dockerfile b/cloud-helpers.aws.Dockerfile index 9c1998ca..7b6215b1 100644 --- a/cloud-helpers.aws.Dockerfile +++ b/cloud-helpers.aws.Dockerfile @@ -6,6 +6,22 @@ RUN dnf upgrade -y --setopt=tsflags=nodocs \ && dnf clean all \ && rm -rf /var/cache/dnf +# Drop the Runtime Interface Emulator. It is the base image's LOCAL-testing +# shim: /lambda-entrypoint.sh execs it only when AWS_LAMBDA_RUNTIME_API is +# unset, and the ENTRYPOINT below replaces that script with /cloud-helpers +# outright — so the RIE binary is never executed in this image, in Lambda or +# anywhere else. It is also 9 MB of Go built by AWS with an older toolchain, +# which is where every stdlib finding in this image came from (8 HIGH at the +# al2023 digest pinned above, all fixed in Go >= 1.26.6; the currently-tagged +# al2023 digest carries 30). Deleting it is the fix, not a suppression: nothing +# links it and no workflow invokes it. +# +# Local debugging is unaffected — `welder run debug-aws-cloud-helpers` runs on +# the HOST and uses the host's own `aws-lambda-rie` (welder.yaml), not this +# image's copy. What this does remove is `docker run --entrypoint +# /lambda-entrypoint.sh /cloud-helpers`; use the welder task instead. +RUN rm -f /usr/local/bin/aws-lambda-rie && test ! -e /usr/local/bin/aws-lambda-rie + WORKDIR / COPY dist/cloud-helpers /cloud-helpers # actions/upload-artifact does not preserve the executable bit, and the release diff --git a/docs/TESTING.md b/docs/TESTING.md index 41c89637..15e9b6c7 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -14,20 +14,21 @@ new tests for every code change. ## Current state — to be updated each pass -| Metric | Value as of 2026-05-19 | +| Metric | Value as of 2026-08-21 | |---|---| -| Total test files | 87 (`*_test.go`) | -| Test files using **gomega** | 67 (77 %) | -| Test files using **testify** | 3 (3 %; mock-only) | -| Test files using **plain `testing`** | 19 (22 %; mostly fuzz + small utilities) | -| Table-driven tests | 43 files | -| Sub-tests via `t.Run` | 66 files | -| Integration tests (`*_integration_test.go`) | 5 packages | -| Mocks generated by `mockery v2.53.4` | `pkg/api/git/mocks/`, `pkg/clouds/pulumi/mocks/` | -| **Overall statement coverage** | **~16 %** | -| Coverage on `pkg/security/...` | 42 – 66 % per sub-pkg | - -These numbers are the baseline. Every PR should hold or improve them. +| Total test files | 197 (`*_test.go`) | +| Test files using **gomega** | 166 (84 %) | +| Test files using **testify** | 12 (6 %; mock-only) | +| Sub-tests via `t.Run` | 145 files | +| Mocks generated by `mockery v2.53.6` | `pkg/api/git/mocks/`, `pkg/clouds/pulumi/mocks/` | +| **Included-set statement coverage** (Gold-tier denominator) | **90.5 %** | +| Full-set statement coverage (whole repo, unfiltered) | 29.0 % | +| Coverage on `pkg/security/...` | 79.6 – 97.8 % per sub-pkg | + +These numbers are the baseline. Every PR should hold or improve them. The two +coverage figures and the scope that separates them are defined under +[Coverage scope](#coverage-scope) below; the included-set figure is the one the +OpenSSF criterion is judged on. ## Test framework — choose gomega @@ -339,9 +340,17 @@ this table: Everything not excluded above — the unit-testable core: `pkg/api/*`, `pkg/util`, `pkg/security/*`, `pkg/clouds/{github,aws,k8s,gcloud,compose,fs,telegram,slack,discord,…}`, -`pkg/template`, and the rest. This is roughly **5,760 statements** and is +`pkg/template`, and the rest. This is roughly **5,975 statements** and is the denominator the Gold-tier criterion is judged on. +`pkg/security/tools/cosigntest` is deliberately NOT excluded even though it is +a test harness. Go attributes coverage per test binary, so a helper package +consumed only by *other* packages' tests reads as 0 % — which is a real gap, +not a measurement artifact: the harness generates a `/bin/sh` script whose +quoting, invocation counter and stream selection three retry suites silently +depend on. It has its own tests (`cosigntest_test.go`) instead of an +exclusion. + The `coverage` task emits two numbers on every run: - **included-set** aggregate → `dist/cover.out` (the Gold-tier figure) @@ -353,8 +362,8 @@ The `coverage` task emits two numbers on every run: | Target | Threshold | Status | |---|---|---| | **Per-PR no-regression** | Included-set aggregate must not decrease vs `main` | Observed by `.github/workflows/coverage.yml` (sticky PR comment); hard gate deferred until the baseline stabilises | -| **Silver badge** (`test_statement_coverage80`) | ≥ 80 % included-set aggregate | Met | -| **Gold badge** (`test_statement_coverage90`) | ≥ 90 % included-set aggregate | Met | +| **Silver badge** (`test_statement_coverage80`) | ≥ 80 % included-set aggregate | Met (90.5 %) | +| **Gold badge** (`test_statement_coverage90`) | ≥ 90 % included-set aggregate | Met (90.5 %, from 89.6 % on 2026-08-21) | Both numbers are reported by `welder run coverage` and by the coverage workflow; the included-set aggregate is the one the OpenSSF criterion is diff --git a/go.mod b/go.mod index 25210066..a6176dbd 100644 --- a/go.mod +++ b/go.mod @@ -59,11 +59,11 @@ require ( go.mongodb.org/mongo-driver v1.17.9 go.uber.org/atomic v1.11.0 gocloud.dev v0.46.0 - golang.org/x/crypto v0.54.0 + golang.org/x/crypto v0.55.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.22.0 golang.org/x/term v0.45.0 - golang.org/x/text v0.40.0 + golang.org/x/text v0.41.0 google.golang.org/api v0.284.0 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 @@ -165,7 +165,7 @@ require ( github.com/chavacava/garif v0.1.0 // indirect github.com/cheggaaa/pb v1.0.29 // indirect github.com/chigopher/pathlib v0.19.1 // indirect - github.com/cilium/ebpf v0.11.0 // indirect + github.com/cilium/ebpf v0.22.0 // indirect github.com/ckaznocha/intrange v0.3.0 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect @@ -393,7 +393,7 @@ require ( github.com/segmentio/asm v1.1.3 // indirect github.com/segmentio/encoding v0.3.5 // indirect github.com/sergi/go-diff v1.4.0 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sirupsen/logrus v1.9.4 // indirect github.com/sivchari/containedctx v1.0.3 // indirect github.com/sivchari/tenv v1.12.1 // indirect github.com/skeema/knownhosts v1.3.1 // indirect @@ -463,13 +463,13 @@ require ( gocloud.dev/secrets/hashivault v0.46.0 // indirect golang.org/x/arch v0.11.0 // indirect golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect - golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac // indirect - golang.org/x/mod v0.38.0 // indirect - golang.org/x/net v0.57.0 // indirect + golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 // indirect + golang.org/x/mod v0.40.0 // indirect + golang.org/x/net v0.58.0 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 // indirect + golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 // indirect golang.org/x/time v0.15.0 // indirect - golang.org/x/tools v0.48.0 // indirect + golang.org/x/tools v0.49.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect @@ -480,7 +480,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - honnef.co/go/tools v0.6.1 // indirect + honnef.co/go/tools v0.7.0 // indirect k8s.io/api v0.36.1 // indirect k8s.io/klog/v2 v2.140.0 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect @@ -492,3 +492,20 @@ require ( sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) + +// honnef.co/go/tools is staticcheck, imported ONLY by +// github.com/golangci/golangci-lint (the `unused` linter). golangci-lint +// v1.64.8 requires v0.6.1 and does not compile against v0.7.0's analysis API: +// it panics mid-run with "typedness: panic during analysis: interface +// conversion: interface {} is nil, not *buildir.IR" and exits 7, which fails +// the `golangci-lint run` step in branch.yaml / push.yaml. +// +// MVS raises it to v0.7.0 because github.com/cilium/ebpf v0.22.0 names that +// version in its own go.mod — ebpf needs staticcheck for ITS CI, not for any +// code we compile, and v0.22.0 is the fix for GO-2026-6238 so it cannot be +// held back. A `require` line loses to ebpf's; only `replace` pins it. +// +// Remove this when golangci-lint moves to v2 (v2.x builds against honnef +// v0.7.x) — that is a separate migration: v2 renames the module path and +// changes the .golangci.yml schema. +replace honnef.co/go/tools => honnef.co/go/tools v0.6.1 diff --git a/go.sum b/go.sum index 9b20dd97..4b6738cb 100644 --- a/go.sum +++ b/go.sum @@ -237,8 +237,8 @@ github.com/cheggaaa/pb v1.0.29 h1:FckUN5ngEk2LpvuG0fw1GEFx6LtyY2pWI/Z2QgCnEYo= github.com/cheggaaa/pb v1.0.29/go.mod h1:W40334L7FMC5JKWldsTWbdGjLo0RxUKK73K+TuPxX30= github.com/chigopher/pathlib v0.19.1 h1:RoLlUJc0CqBGwq239cilyhxPNLXTK+HXoASGyGznx5A= github.com/chigopher/pathlib v0.19.1/go.mod h1:tzC1dZLW8o33UQpWkNkhvPwL5n4yyFRFm/jL1YGWFvY= -github.com/cilium/ebpf v0.11.0 h1:V8gS/bTCCjX9uUnkUFUpPsksM8n1lXBAvHcpiFk1X2Y= -github.com/cilium/ebpf v0.11.0/go.mod h1:WE7CZAnqOL2RouJ4f1uyNhqr2P4CCvXFIqdRDUgWsVs= +github.com/cilium/ebpf v0.22.0 h1:v2ktp0roffpMOj2MMf3idtCQZOsAoC4BJbAJN+ke2bY= +github.com/cilium/ebpf v0.22.0/go.mod h1:CDzZbe2hC5JjlDC+CY3KFCzlYwN4gbxppYM+Z10bQt4= github.com/ckaznocha/intrange v0.3.0 h1:VqnxtK32pxgkhJgYQEeOArVidIPg+ahLP7WBOXZd5ZY= github.com/ckaznocha/intrange v0.3.0/go.mod h1:+I/o2d2A1FBHgGELbGxzIcyd3/9l9DuwjM8FsbSS3Lo= github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= @@ -574,6 +574,10 @@ github.com/jonboulle/clockwork v0.4.0 h1:p4Cf1aMWXnXAUh8lVfewRBx1zaTSYKrKMF2g3ST github.com/jonboulle/clockwork v0.4.0/go.mod h1:xgRqUGwRcjKCO1vbZUEtSLrqKoPSsUpK7fnezOII0kc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= +github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/jsimonetti/rtnetlink/v2 v2.0.1 h1:xda7qaHDSVOsADNouv7ukSuicKZO7GgVUCXxpaIEIlM= +github.com/jsimonetti/rtnetlink/v2 v2.0.1/go.mod h1:7MoNYNbb3UaDHtF8udiJo/RH6VsTKP1pqKLUTVCvToE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= @@ -660,6 +664,10 @@ github.com/mattn/go-runewidth v0.0.24 h1:cpokDiIn0MGnhdHwuWnJBITySJ20QyNGnY2kR/a github.com/mattn/go-runewidth v0.0.24/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk= github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= +github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos= +github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/mgechev/revive v1.7.0 h1:JyeQ4yO5K8aZhIKf5rec56u0376h8AlKNQEmjfkjKlY= github.com/mgechev/revive v1.7.0/go.mod h1:qZnwcNhoguE58dfi96IJeSTPeZQejNeoMQLUZGi4SW4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= @@ -868,8 +876,8 @@ github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepq github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= +github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= github.com/sivchari/tenv v1.12.1 h1:+E0QzjktdnExv/wwsnnyk4oqZBUfuh89YMQT1cyuvSY= @@ -911,7 +919,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -1073,14 +1080,14 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= -golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= +golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM= golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80= golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac h1:TSSpLIG4v+p0rPv1pNOQtl1I8knsO4S9trOxNMOLVP4= -golang.org/x/exp/typeparams v0.0.0-20250210185358-939b2ce775ac/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358 h1:qWFG1Dj7TBjOjOvhEOkmyGPVoquqUKnIU0lEVLp8xyk= +golang.org/x/exp/typeparams v0.0.0-20260209203927-2842357ff358/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -1094,8 +1101,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= -golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= +golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1113,8 +1120,8 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= -golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= +golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1160,8 +1167,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959 h1:RJhm5l6Fo4rmEIcndxDllNhhf/fAx8qIm4t6A7vpm2A= -golang.org/x/telemetry v0.0.0-20260708182218-49f421fb7959/go.mod h1:LV7u5Oco+Z/g6XI7PqN+EUUUGGkEcmB1uj2ceI0fOVg= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5 h1:ZUSxONxc981v7AW7QUg+I9WwZzSTTJ019ENBYr5pV/Q= +golang.org/x/telemetry v0.0.0-20260811182544-a038080d80e5/go.mod h1:LVehoXe41cL5SCVQilsV7Gg6BNG+Js6P9PhSbYTIUkQ= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1183,8 +1190,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= -golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1209,8 +1216,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= -golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= +golang.org/x/tools v0.49.0 h1:3NI7VXzL9+1WZD52Dx2ttoPwD5DWrFGpl9mFZDlmisI= +golang.org/x/tools v0.49.0/go.mod h1:SJNXV9DBKT0UbdttsQjbfJlAE/q+y36++zo3uL3N0Oo= golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= diff --git a/pkg/clouds/aws/helpers/ct_enrichment_test.go b/pkg/clouds/aws/helpers/ct_enrichment_test.go index 832238f6..a320f3a9 100644 --- a/pkg/clouds/aws/helpers/ct_enrichment_test.go +++ b/pkg/clouds/aws/helpers/ct_enrichment_test.go @@ -4,6 +4,7 @@ package helpers import ( + "context" "testing" "time" @@ -157,9 +158,17 @@ func TestLookupTriggeringEventsShortCircuits(t *testing.T) { // refactors don't accidentally try to open a session with empty inputs. RegisterTestingT(t) + // Already-cancelled, rather than nil or context.TODO(): the point of these + // cases is that the short-circuit happens BEFORE any AWS call. A cancelled + // context makes a regression fail with context.Canceled instead of reaching + // the network, and it keeps staticcheck's SA1012 (never pass a nil Context) + // satisfied. + cancelled, cancel := context.WithCancel(context.Background()) + cancel() + t.Run("missing log group name — no call, no error", func(t *testing.T) { RegisterTestingT(t) - events, total, err := lookupTriggeringEvents(nil, nil, enrichmentConfig{FilterPattern: "x"}, time.Now(), 5) + events, total, err := lookupTriggeringEvents(cancelled, nil, enrichmentConfig{FilterPattern: "x"}, time.Now(), 5) Expect(err).To(BeNil()) Expect(events).To(BeNil()) Expect(total).To(Equal(0)) @@ -167,7 +176,7 @@ func TestLookupTriggeringEventsShortCircuits(t *testing.T) { t.Run("missing filter pattern — no call, no error", func(t *testing.T) { RegisterTestingT(t) - events, total, err := lookupTriggeringEvents(nil, nil, enrichmentConfig{LogGroupName: "g"}, time.Now(), 5) + events, total, err := lookupTriggeringEvents(cancelled, nil, enrichmentConfig{LogGroupName: "g"}, time.Now(), 5) Expect(err).To(BeNil()) Expect(events).To(BeNil()) Expect(total).To(Equal(0)) diff --git a/pkg/clouds/cloudflare/cloudflare_test.go b/pkg/clouds/cloudflare/cloudflare_test.go index 3c2d5467..cfd6439a 100644 --- a/pkg/clouds/cloudflare/cloudflare_test.go +++ b/pkg/clouds/cloudflare/cloudflare_test.go @@ -66,3 +66,37 @@ func TestProviderConstants(t *testing.T) { Expect(ProviderType).To(Equal("cloudflare")) Expect(RegistrarType).To(Equal("cloudflare")) } + +func TestReadRegistrarConfig(t *testing.T) { + RegisterTestingT(t) + + t.Run("happy path", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{ + "credentials": "cf-token", + "accountId": "acct-12345", + "zoneName": "example.com", + "dnsRecords": []any{ + map[string]any{"name": "www", "type": "CNAME", "value": "example.com"}, + }, + }} + out, err := ReadRegistrarConfig(cfg) + Expect(err).ToNot(HaveOccurred()) + rc, ok := out.Config.(*RegistrarConfig) + Expect(ok).To(BeTrue()) + Expect(rc.ZoneName).To(Equal("example.com")) + Expect(rc.AccountId).To(Equal("acct-12345")) + Expect(rc.CredentialsValue()).To(Equal("cf-token")) + Expect(rc.DnsRecords()).To(HaveLen(1)) + Expect(rc.DnsRecords()[0].Type).To(Equal("CNAME")) + + var _ api.RegistrarConfig = rc + }) + + t.Run("error path", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{"zoneName": []int{1, 2, 3}}} + _, err := ReadRegistrarConfig(cfg) + Expect(err).To(HaveOccurred()) + }) +} diff --git a/pkg/clouds/compose/compose_test.go b/pkg/clouds/compose/compose_test.go index 148f0e21..ae89e15f 100644 --- a/pkg/clouds/compose/compose_test.go +++ b/pkg/clouds/compose/compose_test.go @@ -5,6 +5,8 @@ package compose import ( "context" + "os" + "path/filepath" "testing" "github.com/compose-spec/compose-go/types" @@ -31,3 +33,51 @@ func TestComposeLoad(t *testing.T) { Expect(uiFound).To(BeTrue()) Expect(ui.ContainerName).To(Equal("refapp-ui")) } + +// An absolute composeFilePath must re-root workingDir onto the file's own +// directory: relative build contexts inside the compose file are resolved +// against workingDir, so honouring a caller-supplied one would point them at +// the wrong tree. +func TestComposeLoad_AbsolutePathRerootsWorkingDir(t *testing.T) { + RegisterTestingT(t) + + abs, err := filepath.Abs("testdata/stacks/refapp/docker-compose.yaml") + Expect(err).ToNot(HaveOccurred()) + + cfg, err := ReadDockerCompose(context.Background(), "/nonexistent/working/dir", abs) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg.Project).ToNot(BeNil()) + Expect(cfg.Project.WorkingDir).To(Equal(filepath.Dir(abs)), + "workingDir must follow the compose file, not the caller's argument") + Expect(cfg.Project.Services).To(HaveLen(3)) +} + +// Interpolation reads the process environment through syscall.Getenv. Loading +// with SkipResolveEnvironment still interpolates ${VAR} in the compose file +// itself, so a broken lookup silently ships the literal placeholder. +func TestComposeLoad_InterpolatesFromProcessEnv(t *testing.T) { + RegisterTestingT(t) + + t.Setenv("SC_COMPOSE_TEST_IMAGE", "mongo:7") + + dir := t.TempDir() + file := filepath.Join(dir, "docker-compose.yaml") + Expect(os.WriteFile(file, []byte(`services: + db: + image: ${SC_COMPOSE_TEST_IMAGE} + container_name: interp-db +`), 0o600)).To(Succeed()) + + cfg, err := ReadDockerCompose(context.Background(), dir, "docker-compose.yaml") + Expect(err).ToNot(HaveOccurred()) + Expect(cfg.Project.Services).To(HaveLen(1)) + Expect(cfg.Project.Services[0].Image).To(Equal("mongo:7")) +} + +func TestComposeLoad_ReturnsErrorForMissingFile(t *testing.T) { + RegisterTestingT(t) + + cfg, err := ReadDockerCompose(context.Background(), t.TempDir(), "docker-compose.yaml") + Expect(err).To(HaveOccurred()) + Expect(cfg.Project).To(BeNil(), "a load failure must not return a half-built project") +} diff --git a/pkg/clouds/docker/types_test.go b/pkg/clouds/docker/types_test.go index 59775945..df14f30b 100644 --- a/pkg/clouds/docker/types_test.go +++ b/pkg/clouds/docker/types_test.go @@ -46,3 +46,40 @@ func Test_GenerateImagePullSecret(t *testing.T) { }) } } + +// RegistryRequiresAuth gates whether an imagePullSecret is generated at all, so +// a partially-configured registry (username without password) must read as +// "no auth" rather than produce a half-formed secret. +func TestRegistryCredentials_RegistryRequiresAuth(t *testing.T) { + RegisterTestingT(t) + + for _, tc := range []struct { + name string + creds RegistryCredentials + want bool + }{ + {name: "both set", want: true, creds: RegistryCredentials{ + DockerRegistryUsername: lo.ToPtr("user"), + DockerRegistryPassword: lo.ToPtr("password"), + }}, + {name: "username only", want: false, creds: RegistryCredentials{ + DockerRegistryUsername: lo.ToPtr("user"), + }}, + {name: "password only", want: false, creds: RegistryCredentials{ + DockerRegistryPassword: lo.ToPtr("password"), + }}, + {name: "neither", want: false, creds: RegistryCredentials{}}, + // An empty-but-present pointer is a declared-yet-blank secret. It still + // counts as "auth requested" so the deploy fails on a bad credential + // rather than silently pulling anonymously. + {name: "both present but empty", want: true, creds: RegistryCredentials{ + DockerRegistryUsername: lo.ToPtr(""), + DockerRegistryPassword: lo.ToPtr(""), + }}, + } { + t.Run(tc.name, func(t *testing.T) { + RegisterTestingT(t) + Expect(tc.creds.RegistryRequiresAuth()).To(Equal(tc.want)) + }) + } +} diff --git a/pkg/clouds/fs/fs_state_test.go b/pkg/clouds/fs/fs_state_test.go index 69720470..17fb1157 100644 --- a/pkg/clouds/fs/fs_state_test.go +++ b/pkg/clouds/fs/fs_state_test.go @@ -7,6 +7,8 @@ import ( "testing" . "github.com/onsi/gomega" + + "github.com/simple-container-com/api/pkg/api" ) func TestFileSystemStateStorage(t *testing.T) { @@ -64,3 +66,39 @@ func TestProviderTypeConstants(t *testing.T) { Expect(StateStorageTypeFileSystem).To(Equal("fs")) Expect(SecretsProviderTypePassphrase).To(Equal("passphrase")) } + +// init() registers the two read funcs under their type keys. A typo in either +// key, or a reader wired to the wrong struct, only shows up as "unknown +// provisioner field config type" at descriptor-read time — so assert the +// registration through the same lookup the reader uses. +func TestInit_RegistersReadersUnderTheirTypeKeys(t *testing.T) { + RegisterTestingT(t) + + t.Run("fs state storage", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{"path": "/var/state/sc"}} + out, err := api.ReadProvisionerFieldConfig(StateStorageTypeFileSystem, cfg) + Expect(err).ToNot(HaveOccurred()) + s, ok := out.Config.(*FileSystemStateStorage) + Expect(ok).To(BeTrue(), "the fs key must resolve to FileSystemStateStorage, not the passphrase provider") + Expect(s.StorageUrl()).To(Equal("/var/state/sc")) + }) + + t.Run("passphrase secrets provider", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{"passPhrase": "correct horse battery staple"}} + out, err := api.ReadProvisionerFieldConfig(SecretsProviderTypePassphrase, cfg) + Expect(err).ToNot(HaveOccurred()) + p, ok := out.Config.(*PassphraseSecretsProvider) + Expect(ok).To(BeTrue()) + Expect(p.PassPhrase).To(Equal("correct horse battery staple")) + Expect(p.KeyUrl()).To(Equal("passphrase")) + }) + + t.Run("unregistered type", func(t *testing.T) { + RegisterTestingT(t) + _, err := api.ReadProvisionerFieldConfig("fs-typo", &api.Config{}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unknown provisioner field config type")) + }) +} diff --git a/pkg/clouds/gcloud/auth_test.go b/pkg/clouds/gcloud/auth_test.go index 259e5c00..7aba865b 100644 --- a/pkg/clouds/gcloud/auth_test.go +++ b/pkg/clouds/gcloud/auth_test.go @@ -8,6 +8,7 @@ import ( "testing" . "github.com/onsi/gomega" + "github.com/samber/lo" "github.com/simple-container-com/api/pkg/api" ) @@ -277,3 +278,61 @@ func TestCredentials_RoundTripJSON(t *testing.T) { Expect(json.Unmarshal([]byte(c.CredentialsValue()), &parsed)).To(Succeed()) Expect(parsed.Type).To(Equal("service_account")) } + +// The read path validates, not just the provisioner: an already-provisioned +// secrets-provider stack is skipped by the provisioner (its URL export exists), +// so a bad rotation period would otherwise surface only at a DR rebuild. +func TestReadSecretsProviderConfig_RejectsBadRotationPeriodAtReadTime(t *testing.T) { + RegisterTestingT(t) + + for _, tc := range []struct { + name string + period string + errSubstr string + }{ + {name: "missing s suffix", period: "90d", errSubstr: "must be a duration in seconds"}, + {name: "not a number", period: "ninetys", errSubstr: "whole number of seconds"}, + {name: "below GCP floor", period: "3600s", errSubstr: "GCP requires at least"}, + {name: "below 30 day minimum", period: "604800s", errSubstr: "allowShortKeyRotation"}, + } { + t.Run(tc.name, func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{ + "projectId": "my-gcp-project", + "keyName": "gcpkms://projects/p/locations/global/keyRings/r/cryptoKeys/k", + "keyLocation": "global", + "keyRotationPeriod": tc.period, + "provision": true, + }} + _, err := ReadSecretsProviderConfig(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring(tc.errSubstr)) + }) + } + + // provision:false means the period is a BYO-key leftover and must not block. + t.Run("ignored when not provisioning", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{ + "keyName": "gcpkms://projects/p/locations/global/keyRings/r/cryptoKeys/k", + "keyRotationPeriod": "90d", + "provision": false, + }} + _, err := ReadSecretsProviderConfig(cfg) + Expect(err).ToNot(HaveOccurred()) + }) +} + +// Nil means "apply the default", zero means "disable the rule" — the two are +// not interchangeable, and collapsing them would silently drop the rollback +// horizon on every state bucket that never set the field. +func TestStateStorageConfig_EffectiveNoncurrentVersionRetentionDays(t *testing.T) { + RegisterTestingT(t) + + Expect((&StateStorageConfig{}).EffectiveNoncurrentVersionRetentionDays()). + To(Equal(DefaultNoncurrentVersionRetentionDays)) + Expect((&StateStorageConfig{NoncurrentVersionRetentionDays: lo.ToPtr(0)}).EffectiveNoncurrentVersionRetentionDays()). + To(Equal(0), "an explicit zero disables the rule and must not fall back to the default") + Expect((&StateStorageConfig{NoncurrentVersionRetentionDays: lo.ToPtr(7)}).EffectiveNoncurrentVersionRetentionDays()). + To(Equal(7)) +} diff --git a/pkg/clouds/mongodb/mongodb_test.go b/pkg/clouds/mongodb/mongodb_test.go index 8eae8158..d444d9ed 100644 --- a/pkg/clouds/mongodb/mongodb_test.go +++ b/pkg/clouds/mongodb/mongodb_test.go @@ -83,3 +83,51 @@ func TestAtlasBackup_FieldRoundTrip(t *testing.T) { Expect(b.Every).To(Equal("2h")) Expect(b.Retention).To(Equal("168h")) } + +func TestReadAtlasConfig(t *testing.T) { + RegisterTestingT(t) + + t.Run("happy path", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{ + "orgId": "org-1", + "projectId": "proj-1", + "projectName": "sc", + "region": "EU_WEST_1", + "instanceSize": "M10", + "privateKey": "atlas-private-key", + "publicKey": "atlas-public-key", + "admins": []any{"admin@example.com"}, + // The pointer fields distinguish "unset" from "explicitly zero", so + // they must survive the yaml round-trip ConvertConfig performs. + "diskSizeGB": 10.5, + "numShards": 2, + "namingStrategyVersion": 1, + }} + out, err := ReadAtlasConfig(cfg) + Expect(err).ToNot(HaveOccurred()) + ac, ok := out.Config.(*AtlasConfig) + Expect(ok).To(BeTrue()) + Expect(ac.OrgId).To(Equal("org-1")) + Expect(ac.InstanceSize).To(Equal("M10")) + Expect(ac.CredentialsValue()).To(Equal("atlas-private-key")) + Expect(ac.ProjectIdValue()).To(Equal("proj-1")) + Expect(ac.Admins).To(ConsistOf("admin@example.com")) + Expect(ac.DiskSizeGB).ToNot(BeNil()) + Expect(*ac.DiskSizeGB).To(Equal(10.5)) + Expect(ac.NumShards).ToNot(BeNil()) + Expect(*ac.NumShards).To(Equal(2)) + Expect(ac.NamingStrategyVersion).ToNot(BeNil()) + Expect(*ac.NamingStrategyVersion).To(Equal(1)) + // Unset pointers stay nil so downstream defaults still apply. + Expect(ac.Backup).To(BeNil()) + Expect(ac.NetworkConfig).To(BeNil()) + }) + + t.Run("error path", func(t *testing.T) { + RegisterTestingT(t) + cfg := &api.Config{Config: map[string]any{"instanceSize": []int{1, 2, 3}}} + _, err := ReadAtlasConfig(cfg) + Expect(err).To(HaveOccurred()) + }) +} diff --git a/pkg/security/tools/cosigntest/cosigntest_test.go b/pkg/security/tools/cosigntest/cosigntest_test.go new file mode 100644 index 00000000..763708d6 --- /dev/null +++ b/pkg/security/tools/cosigntest/cosigntest_test.go @@ -0,0 +1,208 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) Simple Container + +package cosigntest + +import ( + "context" + "os/exec" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/gomega" + + "github.com/simple-container-com/api/pkg/security/tools" +) + +// The harness is what three retry suites assert against, so a silent break in +// it (a dropped counter, a marker on the wrong stream, a path that needs +// quoting) turns those suites green for the wrong reason. Drive the stub the +// way production drives cosign — tools.ExecCommand, which is exactly what +// signing.RunCosignWithRetry calls — so stdout and stderr stay separated here +// the same way they do there. + +const stubTimeout = 10 * time.Second + +func runStub(args ...string) (stdout, stderr string, err error) { + return tools.ExecCommand(context.Background(), "cosign", args, nil, stubTimeout) +} + +func TestInstall_StubWinsOnPathAndReportsSuccess(t *testing.T) { + RegisterTestingT(t) + + fake := Install(t, Options{}) + + resolved, err := exec.LookPath("cosign") + Expect(err).ToNot(HaveOccurred()) + Expect(resolved).To(Equal(filepath.Join(filepath.Dir(fake.counter), "cosign")), + "the stub must resolve ahead of any real cosign on the runner") + + stdout, stderr, err := runStub("attest", "--yes") + Expect(err).ToNot(HaveOccurred()) + Expect(stdout).To(ContainSubstring("tlog entry created with index")) + Expect(stderr).To(BeEmpty()) + Expect(fake.Calls(t)).To(Equal(1)) +} + +func TestInstall_ConflictsBeforeFailsThenSucceeds(t *testing.T) { + RegisterTestingT(t) + + fake := Install(t, Options{ConflictsBefore: 2}) + + for attempt := 1; attempt <= 2; attempt++ { + stdout, stderr, err := runStub("attest") + Expect(err).To(HaveOccurred(), "invocation %d must fail", attempt) + Expect(stderr).To(ContainSubstring("createLogEntryConflict")) + Expect(stdout).ToNot(ContainSubstring("createLogEntryConflict"), + "the conflict must not leak onto stdout when ConflictOnStdout is unset") + } + + stdout, _, err := runStub("attest") + Expect(err).ToNot(HaveOccurred(), "invocation 3 is past ConflictsBefore") + Expect(stdout).To(ContainSubstring("tlog entry created with index")) + Expect(fake.Calls(t)).To(Equal(3), "the counter must survive across the three processes") +} + +func TestInstall_ConflictOnStdoutKeepsStderrNonEmptyWithoutTheMarker(t *testing.T) { + RegisterTestingT(t) + + Install(t, Options{ConflictsBefore: 1, ConflictOnStdout: true}) + + stdout, stderr, err := runStub("attest") + Expect(err).To(HaveOccurred()) + Expect(stdout).To(ContainSubstring("createLogEntryConflict")) + // Both halves matter: a caller that classifies only stderr must still see + // output, and it must not be the marker — otherwise a stdout-classification + // bug passes because the marker was on stderr too. + Expect(stderr).ToNot(BeEmpty()) + Expect(stderr).ToNot(ContainSubstring("createLogEntryConflict")) +} + +func TestInstall_FailStderrIsTerminalAndIgnoresConflictsBefore(t *testing.T) { + RegisterTestingT(t) + + fake := Install(t, Options{FailStderr: "Error: UNAUTHORIZED: authentication required", ConflictsBefore: 1}) + + for attempt := 1; attempt <= 2; attempt++ { + _, stderr, err := runStub("attest") + Expect(err).To(HaveOccurred(), "invocation %d must fail", attempt) + Expect(stderr).To(ContainSubstring("UNAUTHORIZED")) + Expect(stderr).ToNot(ContainSubstring("createLogEntryConflict"), + "FailStderr must suppress the retryable conflict entirely") + } + Expect(fake.Calls(t)).To(Equal(2)) +} + +// A stderr body carrying a single quote is what broke an earlier harness: the +// generated /bin/sh script stopped parsing and every invocation failed for the +// wrong reason. +func TestInstall_FailStderrSurvivesShellQuoting(t *testing.T) { + RegisterTestingT(t) + + body := `Error: signing 'registry.example.com/team/app': it's fatal` + Install(t, Options{FailStderr: body}) + + _, stderr, err := runStub("sign") + Expect(err).To(HaveOccurred()) + Expect(stderr).To(ContainSubstring(body), "the quoted body must reach stderr verbatim") +} + +func TestInstall_DelayEachAppliesToEveryInvocation(t *testing.T) { + RegisterTestingT(t) + + const delay = 300 * time.Millisecond + Install(t, Options{DelayEach: delay}) + + for attempt := 1; attempt <= 2; attempt++ { + start := time.Now() + _, _, err := runStub("attest") + Expect(err).ToNot(HaveOccurred()) + Expect(time.Since(start)).To(BeNumerically(">=", delay), + "invocation %d must get its own delay, not a shared one", attempt) + } +} + +func TestInstall_DelayFirstAppliesOnlyToTheFirstInvocation(t *testing.T) { + RegisterTestingT(t) + + const delay = 700 * time.Millisecond + Install(t, Options{DelayFirst: delay}) + + start := time.Now() + _, _, err := runStub("attest") + Expect(err).ToNot(HaveOccurred()) + first := time.Since(start) + + start = time.Now() + _, _, err = runStub("attest") + Expect(err).ToNot(HaveOccurred()) + second := time.Since(start) + + Expect(first).To(BeNumerically(">=", delay)) + Expect(second).To(BeNumerically("<", delay), "only invocation 1 is delayed") +} + +func TestSeconds_RendersDecimalsPosixSleepAccepts(t *testing.T) { + RegisterTestingT(t) + + // `sleep 150ms` is a Go duration string, not a number — /bin/sh rejects it. + for _, tt := range []struct { + in time.Duration + want string + }{ + {0, "0.000"}, + {150 * time.Millisecond, "0.150"}, + {1500 * time.Millisecond, "1.500"}, + {2 * time.Second, "2.000"}, + } { + Expect(seconds(tt.in)).To(Equal(tt.want)) + } +} + +func TestShellQuote_QuotesForPosixShell(t *testing.T) { + RegisterTestingT(t) + + for _, tt := range []struct { + name string + in string + want string + }{ + {"plain", "abc", `'abc'`}, + {"with space", "/tmp/go build/cosign", `'/tmp/go build/cosign'`}, + {"single quote", "it's", `'it'\''s'`}, + {"empty", "", `''`}, + } { + t.Run(tt.name, func(t *testing.T) { + RegisterTestingT(t) + Expect(shellQuote(tt.in)).To(Equal(tt.want)) + + // Round-trip through the shell the generated stub actually runs + // under: string equality alone would not catch a quoting form that + // /bin/sh re-splits or expands. + out, err := exec.Command("/bin/sh", "-c", "printf '%s' "+shellQuote(tt.in)).Output() + Expect(err).ToNot(HaveOccurred()) + Expect(string(out)).To(Equal(tt.in)) + }) + } +} + +func TestShellQuote_NeutralisesShellMetacharacters(t *testing.T) { + RegisterTestingT(t) + + // A temp dir or stderr body is untrusted input as far as the generated + // script is concerned; quoting must stop it executing. + out, err := exec.Command("/bin/sh", "-c", "printf '%s' "+shellQuote("$(echo pwned)`id`;rm -rf /")).Output() + Expect(err).ToNot(HaveOccurred()) + Expect(string(out)).To(Equal("$(echo pwned)`id`;rm -rf /")) +} + +func TestRekorConflictStderr_MatchesWhatCallersClassifyOn(t *testing.T) { + RegisterTestingT(t) + + // The retry classifier anchors on the swagger error shape, not on "409" + // alone. If the fixture drifts off that shape the retry suites stop + // exercising the retry path and silently assert the failure branch. + Expect(RekorConflictStderr).To(ContainSubstring("[POST /api/v1/log/entries][409]")) + Expect(RekorConflictStderr).To(ContainSubstring("createLogEntryConflict")) +} diff --git a/vex/openvex.json b/vex/openvex.json index 098f155c..2f9eee5d 100644 --- a/vex/openvex.json +++ b/vex/openvex.json @@ -2,8 +2,8 @@ "@context": "https://openvex.dev/ns/v0.2.0", "@id": "https://simple-container.com/.well-known/openvex/api.json", "author": "Simple Container maintainers ", - "timestamp": "2026-08-10T00:00:00Z", - "version": 3, + "timestamp": "2026-08-21T00:00:00Z", + "version": 4, "tooling": "Hand-authored. Reachability evidence from `govulncheck -mode=source ./...`, `go list -deps ./...` and `go mod why -m`.", "statements": [ { @@ -23,7 +23,7 @@ ], "status": "not_affected", "justification": "component_not_present", - "impact_statement": "Corrected 2026-08-10 (was wrongly `fixed`). The advisory has `introduced: 0` and NO fix event in OSV: AWS never patched the v1 s3crypto client, the remediation is migrating to the V2 encryption client. `fixed` was therefore never a reachable status and was acting as an indefinite suppression. Scope of `component_not_present`: the SHIPPED ARTIFACT. aws-sdk-go is absent from go.mod (0 matches) and from go.sum (0 matches), and ZERO of its packages link into any binary we ship — `go list -buildvcs=false -deps ./... | grep -c '^github.com/aws/aws-sdk-go/'` returns 0, `go version -m dist/github-actions | grep -c aws-sdk-go` returns 0, and `go mod why -m github.com/aws/aws-sdk-go` reports \"main module does not need module github.com/aws/aws-sdk-go\". The only residual trace is a requirement-graph edge: `go list -m all` still resolves v1.55.8 because a transitive dependency's own go.mod names it. s3crypto specifically is never imported; S3 access goes through aws-sdk-go-v2. Neither govulncheck -mode=source nor osv-scanner reports this advisory against the repo (osv-scanner reads go.mod's own requirements), so an osv-scanner.toml mirror entry would be an unused ignore. Closing condition: the requirement edge disappears from `go list -m all`, at which point this statement can be deleted." + "impact_statement": "Corrected 2026-08-10 (was wrongly `fixed`). The advisory has `introduced: 0` and NO fix event in OSV: AWS never patched the v1 s3crypto client, the remediation is migrating to the V2 encryption client. `fixed` was therefore never a reachable status and was acting as an indefinite suppression. Scope of `component_not_present`: the SHIPPED ARTIFACT. aws-sdk-go is absent from go.mod (0 matches) and from go.sum (0 matches), and ZERO of its packages link into any binary we ship — `go list -buildvcs=false -deps ./... | grep -c '^github.com/aws/aws-sdk-go/'` returns 0, `go version -m dist/github-actions | grep -c aws-sdk-go` returns 0, and `go mod why -m github.com/aws/aws-sdk-go` reports \"main module does not need module github.com/aws/aws-sdk-go\". The only residual trace is a requirement-graph edge: `go list -m all` still resolves v1.55.8 because a transitive dependency's own go.mod names it. s3crypto specifically is never imported; S3 access goes through aws-sdk-go-v2. Neither govulncheck -mode=source nor osv-scanner reports this advisory against the repo (osv-scanner reads go.mod's own requirements), so an osv-scanner.toml mirror entry would be an unused ignore. Closing condition: the requirement edge disappears from `go list -m all`, at which point this statement can be deleted. Re-triaged 2026-08-21 (SCA pass): still 0 matches in go.mod and go.sum, `go mod why -m` still reports \"main module does not need\", and `go list -m all` still resolves v1.55.8 through a transitive go.mod — evidence unchanged, statement kept as-is." }, { "vulnerability": { @@ -42,7 +42,7 @@ ], "status": "not_affected", "justification": "component_not_present", - "impact_statement": "Same module, same evidence and same correction as GO-2022-0635 — see that statement. Also `introduced: 0` with no fix event, so it was likewise mis-stated as `fixed` before 2026-08-10." + "impact_statement": "Same module, same evidence and same correction as GO-2022-0635 — see that statement. Also `introduced: 0` with no fix event, so it was likewise mis-stated as `fixed` before 2026-08-10. Re-triaged 2026-08-21 with the same result." }, { "vulnerability": { @@ -60,7 +60,7 @@ ], "status": "not_affected", "justification": "vulnerable_code_not_present", - "impact_statement": "Re-confirmed 2026-08-10 against the current tree. golang.org/x/crypto is a direct dependency for chacha20poly1305, ed25519, hkdf and ssh (pkg/api/secrets, pkg/api/secrets/ciphers); 24 of its packages link into shipped binaries, including ssh, ssh/agent, ssh/knownhosts, pkcs12 and ocsp. None of them is openpgp or an openpgp subpackage: `go list -buildvcs=false -deps ./... | grep -c '^golang.org/x/crypto/openpgp'` returns 0, so the Go linker never includes that code in any produced binary. (Use -buildvcs=false: a plain `go list -deps` can exit non-zero on a VCS error and print nothing, which greps identically to a clean result.) OpenPGP operations go through github.com/ProtonMail/go-crypto v1.1.6, the maintained fork the advisory itself recommends. govulncheck -mode=source ./... reports 0 reachable vulnerabilities. Unfixable by bumping: the advisory has `introduced: 0` and no fix event; it is a permanent 'package is unmaintained' notice. x/crypto is kept current regardless — pinned at v0.54.0, which is the latest release." + "impact_statement": "Re-confirmed 2026-08-10 against the current tree. golang.org/x/crypto is a direct dependency for chacha20poly1305, ed25519, hkdf and ssh (pkg/api/secrets, pkg/api/secrets/ciphers); 24 of its packages link into shipped binaries, including ssh, ssh/agent, ssh/knownhosts, pkcs12 and ocsp. None of them is openpgp or an openpgp subpackage: `go list -buildvcs=false -deps ./... | grep -c '^golang.org/x/crypto/openpgp'` returns 0, so the Go linker never includes that code in any produced binary. (Use -buildvcs=false: a plain `go list -deps` can exit non-zero on a VCS error and print nothing, which greps identically to a clean result.) OpenPGP operations go through github.com/ProtonMail/go-crypto v1.1.6, the maintained fork the advisory itself recommends. govulncheck -mode=source ./... reports 0 reachable vulnerabilities. Unfixable by bumping: the advisory has `introduced: 0` and no fix event; it is a permanent 'package is unmaintained' notice. x/crypto is kept current regardless — pinned at v0.55.0, which is the latest release. Re-triaged 2026-08-21 (SCA pass): `go list -buildvcs=false -deps ./...` still links 24 x/crypto packages and 0 openpgp packages, ProtonMail/go-crypto is still the OpenPGP path, and no fix event has appeared upstream — statement kept, x/crypto bumped v0.54.0 → v0.55.0 in the same pass." } ] }