f15db0e20a
Part of the same security-hardening pass as the last two commits. make
lint was go vet only; govulncheck and gitleaks already scanned deps and
secrets on every push, but nothing read this repo's own source for
risky patterns (weak crypto, injection shapes, insecure cookies, ...).
New `make security-code` runs gosec, wired into ci.yaml's security job
alongside the other two. G104 (unchecked error) is excluded at the
Makefile level: every one of its 41 initial hits was this codebase's
existing, deliberate idiom for a best-effort write or an already-
reviewed json.Unmarshal of its own JSONB, predating gosec, and the rule
cannot tell that apart from a mistake -- seventeen individual #nosec
comments would hide a future real G104 regression in the suppression
noise rather than surface it. Reasoning is on the Makefile target.
Of the 12 remaining hits:
- Genuinely real: oidc.go's callback logged error_description (and,
two call sites down, identity.Subject) via %s before the request's
state was even checked against its cookie -- an attacker-reachable
value going into the log unquoted. Switched to %q, matching
identity.Username's existing treatment, so a value holding a
newline can't forge a second log line.
- False positives, annotated inline rather than globally suppressed:
4x G124 on cookies that already set Secure via cookieSecure(...)
(a function call, not the literal `true` the rule wants), 3x G202
on sqlArgs-built queries that only ever splice in a "$N"
placeholder, never a value, and the remaining 5x G706 on log lines
that were already %q-quoted -- gosec's taint analysis doesn't
model format verbs, so it flags the tainted argument regardless.
Also fixed handleMe's swallowed Scan error (gosec's catch, pre-fix):
a transient DB error left hash/dismissed at their zero values and the
response claimed no password and no onboarding dismissal regardless
of the truth, rather than surfacing a 500.
Checked both workflow files for the injection class letsvisit found
there (a `${{ }}` expression spliced straight into a `run:` block):
every one here already goes through `env:` as a quoted shell variable,
documented in ci.yaml's own header comment. Nothing to fix.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
155 lines
6.0 KiB
YAML
155 lines
6.0 KiB
YAML
name: CI
|
|
|
|
# The release workflow gates a tag, which is late: a broken commit sits green until
|
|
# somebody decides to publish. This runs the same checks on the way in.
|
|
#
|
|
# push is scoped to main rather than all branches so that a branch pushed as part of a
|
|
# pull request is not checked twice.
|
|
#
|
|
# No actions/checkout, deliberately -- same as the letsvisit and charts workflows. The
|
|
# runner image is ubuntu:22.04 whose `nodejs` package is Node 12, and actions/checkout@v4
|
|
# is built with ES2022 static initialiser blocks, so it dies with
|
|
# `SyntaxError: Unexpected token '{'` before running. Cloning with git directly avoids JS
|
|
# actions entirely. This repo is public, so the clone needs no credential at all.
|
|
#
|
|
# `${{ }}` values are passed through `env:` and referenced as quoted shell variables: a
|
|
# ref name is attacker-influenced by anyone who can push a branch or open a PR, and
|
|
# expanding one straight into `run:` is a shell-injection vector.
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
pull_request:
|
|
|
|
# A rapid series of pushes only needs the last one checked.
|
|
concurrency:
|
|
group: ci-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
REPO_URL: https://git.ryuvia.com/niklas/terdut-server.git
|
|
|
|
jobs:
|
|
test:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
# Runs inside the toolchain image rather than installing Go per job. Note this puts
|
|
# the job on the dind bridge, which cannot reach github.com or get.helm.sh --
|
|
# proxy.golang.org and git.ryuvia.com are reachable, which is all this job needs.
|
|
image: golang:1.26.6-bookworm
|
|
# act_runner destroys a job's own volumes when it finishes, so without these every
|
|
# run re-downloads the whole module graph. The names must appear in the runner's
|
|
# container.valid_volumes allowlist (charts/act-runner in the k8s repo); unlisted
|
|
# volumes are dropped silently, so a workflow that looks correct can still be
|
|
# running uncached.
|
|
volumes:
|
|
- go-mod-cache:/go/pkg/mod
|
|
- go-build-cache:/root/.cache/go-build
|
|
- gobin-cache:/go/bin
|
|
|
|
# The suite needs a real Postgres -- there is no in-memory Postgres the way there was
|
|
# an in-memory SQLite, so each test gets its own schema on a shared server instead.
|
|
# The job and the service share the dind bridge, so the service is reachable by its
|
|
# name rather than on localhost.
|
|
services:
|
|
postgres:
|
|
image: postgres:17-alpine
|
|
env:
|
|
POSTGRES_USER: terdut
|
|
POSTGRES_PASSWORD: terdut
|
|
POSTGRES_DB: terdut_test
|
|
options: >-
|
|
--health-cmd "pg_isready -U terdut -d terdut_test"
|
|
--health-interval 5s
|
|
--health-timeout 5s
|
|
--health-retries 12
|
|
|
|
env:
|
|
# `make test` fails without this rather than skipping, so a green job here means
|
|
# the tests actually ran against a database.
|
|
TERDUT_TEST_DSN: postgres://terdut:terdut@postgres:5432/terdut_test?sslmode=disable
|
|
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if [ -n "$HEAD_SHA" ]; then
|
|
# A pull_request ref_name is "<n>/merge", which is not a fetchable branch.
|
|
git clone "$REPO_URL" .
|
|
git checkout -q "$HEAD_SHA"
|
|
else
|
|
git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
fi
|
|
|
|
# The gate is the Makefile's rather than a second copy of it here, the way riksdata
|
|
# and rd-web already do it. `make fmt lint test` is exactly what a developer runs, so
|
|
# a green pipeline and a green working copy mean the same thing by construction
|
|
# instead of by remembering to update two files together.
|
|
#
|
|
# The reasoning that used to live here moved with the targets: why gofmt is checked
|
|
# at all (import order survives `go vet`, and both repos sat unformatted through a
|
|
# green run and a release -- 9046f6e), why both of gofmt's failure modes need
|
|
# handling, and why `test` adds -race when this job does not have to.
|
|
- name: Format, vet and test
|
|
run: make fmt lint test
|
|
|
|
# Runs on every push and pull request, unlike the image scan, which needs something
|
|
# published to scan and so lives in release.yaml. Both are needed: govulncheck reads the
|
|
# source and its module graph, trivy reads the built artifact, and neither sees what the
|
|
# other does.
|
|
security:
|
|
runs-on: ubuntu-latest
|
|
container:
|
|
image: golang:1.26.6-bookworm
|
|
volumes:
|
|
- go-mod-cache:/go/pkg/mod
|
|
- go-build-cache:/root/.cache/go-build
|
|
- gobin-cache:/go/bin
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if [ -n "$HEAD_SHA" ]; then
|
|
git clone "$REPO_URL" .
|
|
git checkout -q "$HEAD_SHA"
|
|
else
|
|
git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
fi
|
|
|
|
- name: Go vulnerability scan (govulncheck)
|
|
run: make security-go
|
|
|
|
- name: Secret scan (gitleaks)
|
|
run: make security-secrets
|
|
|
|
- name: Code security scan (gosec)
|
|
run: make security-code
|
|
|
|
# Host mode, no `container:`: helm is baked into the runner image, and a container job
|
|
# could not install it -- get.helm.sh is unreachable from the dind bridge. Same reason
|
|
# release.yaml's chart job runs on the host.
|
|
#
|
|
# The chart had no lint step in any workflow until 2026-09-01: release.yaml packaged and
|
|
# pushed it without rendering it first, so a template that did not compile would have
|
|
# been found by Flux rather than here.
|
|
chart:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
run: |
|
|
if [ -n "$HEAD_SHA" ]; then
|
|
git clone "$REPO_URL" .
|
|
git checkout -q "$HEAD_SHA"
|
|
else
|
|
git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" .
|
|
fi
|
|
|
|
- name: Lint and render the chart
|
|
run: make helm-lint
|