ADR-0008: Security scanning and CI quality gates
Architecture decision record 0008.
ADR-0008: Security scanning and CI quality gates
- Status: Accepted
- Date: 2026-06-02
- Authors: IBEX Harness team
Context
web/engineering/SECURITY.md §12.2 and web/engineering/DEPENDENCIES.md §9 listed recommended scanners that were not wired in CI. ADR-0003 required only repo-guards, markdownlint, and gitleaks. golangci-lint ran with continue-on-error: true, providing no merge enforcement.
The repo has a single root go.mod, Go services auth and proxy, two Dockerfiles, and no Python/TypeScript application code yet.
Decision
Workflows added
| Workflow | Purpose | PR required check |
|---|---|---|
.github/workflows/codeql.yml | CodeQL (go now; python/javascript when app code exists) | CodeQL |
.github/workflows/semgrep.yml | IBEX custom rules (hard gate) + community rules (SARIF, non-blocking) | semgrep |
.github/workflows/scorecard.yml | OSSF supply-chain score | No |
.github/workflows/sbom.yml | Syft SBOM + Grype scan (CRITICAL); Grype pinned via GRYPE_VERSION with checksum verify; reports as workflow artifacts only (no SARIF upload—Grype SBOM SARIF lacks GitHub artifactLocation) | No |
CI jobs added or changed (.github/workflows/ci.yml)
| Job | Failure threshold |
|---|---|
trivy | CRITICAL/HIGH filesystem CVEs; ignore-unfixed: true |
osv-scan | CRITICAL/HIGH via OSV reusable workflow |
hadolint | ≥ warning (.hadolint.yaml) |
bandit | HIGH+HIGH when services/memory exists; skip (success) until then; install via hash-pinned .github/requirements/bandit.txt |
golangci-lint | Any lint issue; single job for auth+proxy; no continue-on-error |
Weekly schedule on CI runs only osv-scan (other jobs use if: github.event_name != 'schedule').
CI uses go-version-file: go.mod so the runner Go version tracks go.mod (currently 1.25.13; golang.org/x/crypto ≥ v0.54.0; golangci-lint v2.4+ for Go 1.25).
Go vs JavaScript vulnerability scanning
| Ecosystem | Merge gate | Tool | Rationale |
|---|---|---|---|
| JavaScript | osv-scan | OSV on pnpm-lock.yaml | Lockfile advisories with fix versions |
| Go | govulncheck | govulncheck on packages + auth + proxy | Reachable stdlib and module vulnerabilities only |
osv-scan does not scan go.mod because advisory GO-2026-5932 is module-level for the unmaintained openpgp subpackage (no fixed version; use github.com/ProtonMail/go-crypto/openpgp only if OpenPGP is required). This repo imports only golang.org/x/crypto/argon2 via packages/crypto (ADR-0010). OSV call-analysis cannot mark GO-2026-5932 unexecuted; govulncheck confirms no reachable Go impact. This is not an ignore list — it is the correct tool split for module-level vs reachability advisories.
CodeQL default vs advanced
GitHub Default CodeQL setup conflicts with the advanced .github/workflows/codeql.yml (SARIF rejected). Repo admin must disable Default setup (Settings → Code security → Code scanning → CodeQL → Advanced) before the CodeQL check is reliable.
Dependabot
- Active:
github-actions,gomodat/ - Deferred (documented in
.github/dependabot.yml):pip(/services/memory),npm(/services/dashboard)
Branch protection
.github/branch-protection-main.json requires area gate jobs (stable merge contexts) instead of every individual CI job:
| Required check | Covers |
|---|---|
ci-gate-repo | repo-guards, shellcheck, markdownlint, compose-validate, go-mod-tidy, license-check |
ci-gate-go | Buf/proto, Go smokes, go-services, golangci-lint, coverage, govulncheck |
ci-gate-python | embedder-test, embedder-coverage |
ci-gate-web | web-build |
ci-gate-security | trivy, osv-scan, bandit, hadolint |
gitleaks | Secret scan (always runs — secrets can appear in any path) |
semantic-pr-title | Conventional PR title (PRs only) |
Gate jobs always run (if: always()). When an area did not change, the gate auto-passes even if child jobs were skipped (see .github/scripts/evaluate-ci-gate.sh). This avoids GitHub branch protection blocking merges when path-filtered jobs are skipped.
Not required for merge (path-scoped or informational): CodeQL, semgrep, dependency-review, scorecard, sbom, individual smoke job names, buf-lint, go-services.
Apply after gate job names have appeared on at least one PR (GitHub rejects unknown check names):
gh api --method PUT \
repos/Rick1330/ibex-harness/branches/main/protection \
--input .github/branch-protection-main.jsonPath-filtered CI (2026-07)
Heavy jobs run only when their area changed. Filter definitions live in .github/path-filters.yml; all workflows reuse .github/actions/detect-changes.
| Filter | Paths (summary) | Effective flags |
|---|---|---|
go | services/**, packages/**, go.mod, migrations, .golangci.yml | run_go, run_security, run_docker |
python | services/**/app/**, services/**/tests/**, Python lockfiles | run_python, run_security |
web | web/**, pnpm/turbo root files | run_web, run_security |
proto | packages/proto/** | (included in go via shared paths) |
docker | Dockerfiles, .hadolint.yaml | run_docker, run_security |
workflows | .github/workflows/**, actions, scripts | full → all areas |
deps | go.mod, go.sum, pnpm-lock.yaml | full + run_security |
compose | infra/compose/** | run_compose |
benchmarks | benchmarks/**, web/public/benchmarks/**, benchmark.yml, services/auth/**, services/proxy/**, go.mod, go.sum | benchmark.yml workflow trigger |
docs | **/*.md, **/*.mdx | fast markdownlint only |
Cross-cutting rule: workflows or deps changes set full=true and run Go, web, and security scans. workflow_dispatch with force_full: true (nightly, manual) runs the full matrix.
Sibling workflows:
| Workflow | PR behavior |
|---|---|
codeql.yml | Runs when run_go or run_security; weekly schedule is full Go analysis |
semgrep.yml | Runs when run_go or run_web |
dependency-review.yml | Runs when deps or run_security |
sbom.yml | No PR trigger — push: main + weekly schedule only |
docker-publish.yml | workflow_run after CI on main skips publish when neither run_docker nor run_go |
Safety net (no missed coverage): nightly.yml dispatches full ci.yml with force_full: true (Sun 03:00 UTC); CodeQL and Scorecard weekly schedules; any .github/workflows/** change forces full PR CI.
Validation matrix (run after merge or on a dry-run PR branch):
| Test PR | Expected heavy jobs | Gates |
|---|---|---|
web/content MDX only | web-build, markdownlint, gitleaks | All gates green; Go jobs skipped |
services/proxy Go change | Full Go matrix; no web-build | ci-gate-go exercises real jobs |
.github/workflows/ci.yml | Full matrix | All gates exercise real jobs |
pnpm-lock.yaml only | web-build, security scans | Go smokes skipped |
Dependabot go.mod PR | Go + security + web-build (deps sets full=true) | All gates green; web-build from deps cross-cut, not from go.mod path alone |
PR auto-labeler: .github/workflows/labeler.yml applies area/*, dependencies, and area/infra labels from .github/labeler.yml (sync-labels: true).
AI agent enforcement
- Custom Semgrep rules encode AGENTS.md / SECURITY.md invariants
AGENTS.mdfor agent security/CI review expectations
Consequences
Positive
- Unified dependency CVE coverage (OSV) and container/filesystem scanning (Trivy)
- IBEX-specific invariants enforced mechanically on every PR
- Supply-chain visibility (Scorecard, SBOM) without blocking solo merge velocity on informational jobs
Negative
- First PR may fail until CVEs/lint/hadolint findings are fixed
- Branch protection cannot include new checks until GitHub has seen them on a PR
- CodeQL requires one-time disable of GitHub Default setup
References
Was this page helpful?
Last updated on