Cerberus is a guard dog for your CI: SAST (Semgrep), secrets (Gitleaks), dependencies (Trivy), IaC misconfig (Checkov) and Dockerfile lint (Hadolint) in one container. It merges everything into a single SARIF report, ships it to your tracker, and fails the pipeline only when new findings appear — your old backlog never blocks a merge.
Status: v0. The CLI works end-to-end (scan → merge → upload → gate); the Docker image and CI wrappers are not published yet.
Running scanners in CI is easy. Living with the results is not: the first scan dumps hundreds of findings, every pipeline goes red, and a week later someone turns the gate off. Cerberus stays stateless and delegates memory to a backend (a tracker). The backend deduplicates findings across scans and answers with a delta — so tasks are created only for new findings, fixed ones are auto-closed, triaged false positives stay silent, and the merge gate reacts to new problems only.
CI job ──▶ cerberus scan
├─ Semgrep (SAST)
├─ Gitleaks (secrets, whole history)
├─ Trivy (dependencies, secrets, misconfig)
├─ Checkov (IaC misconfig)
├─ Hadolint (Dockerfiles)
└─ custom (anything that writes SARIF)
│ merge → one SARIF
▼
POST to backend ──▶ dedup, lifecycle, tasks
◀── { new: 1, known: 51, fixed: 3 }
│
exit code by gate policy (fail_on: new-critical)
GitLab — include the template:
include:
- remote: https://raw.githubusercontent.com/startmatter/cerberus/main/templates/gitlab-ci.ymlGitHub — call the reusable workflow:
permissions:
contents: read
packages: read
jobs:
security:
uses: startmatter/cerberus/.github/workflows/scan.yml@main
secrets:
K_SARIF_URL: ${{ secrets.K_SARIF_URL }}
K_SARIF_SECRET: ${{ secrets.K_SARIF_SECRET }}Or drive the action directly when you need control over the surrounding job:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Gitleaks scans the whole history
- uses: startmatter/cerberus@main
with:
url: ${{ secrets.K_SARIF_URL }}
secret: ${{ secrets.K_SARIF_SECRET }}
# dns: 1.1.1.1,8.8.8.8 # self-hosted runners whose resolver containers can't reachCI context (repo, branch, commit, author, changed files) is detected from GitLab CI / GitHub Actions
environment variables. K_SARIF_URL and K_SARIF_SECRET come from the tracker's integration settings —
set them group-level (GitLab) or as org secrets (GitHub) so every repo inherits them. In auto mode
the default branch reports (creates/closes tasks) and every other branch checks (read-only gate) — so
one job line covers pushes and merge requests.
Add a nightly scheduled pipeline for dependency scanning: new CVEs land in code that never changed, so pushes alone will not surface them.
If the runner sits behind a VPN resolver its containers cannot reach, Semgrep cannot fetch its rules
and Trivy cannot fetch its vulnerability database — and both quietly produce an empty report. Pass
dns: 1.1.1.1,8.8.8.8 (GitHub) or set --dns on the runner's docker config (GitLab).
Container packages are private by default, and some organizations disallow public ones. Then the runner has to authenticate:
- GitLab — add a
DOCKER_AUTH_CONFIGCI variable (group-level):{"auths":{"ghcr.io":{"auth":"<base64 of user:token>"}}}where the token is a GitHub PAT withread:packages. - GitHub, same organization — nothing to do: the action logs in with the job's
GITHUB_TOKEN. Give the jobpermissions: packages: read. - GitHub, another organization — a repository's own token cannot read another org's private
package. Pass a PAT with
read:packagesas theREGISTRY_TOKENsecret (reusable workflow) orregistry-tokeninput (action).
Configuration lives in cerberus.yml in the repo root — scanners, extra args,
custom SARIF-emitting checks, gate policy.
Locally: docker run -v $(pwd):/src ghcr.io/startmatter/cerberus scan — prints a findings table,
uploads nothing unless you pass --upload. Exit codes: 0 clean, 1 gate failed, 2 runtime error.
- Stateless. Cerberus scans everything and sends everything. History, dedup, baselines and suppression live in the backend.
- Delta gate. Pipelines fail only on findings introduced by the change. A pre-existing backlog never blocks a merge.
- Says what it found. Every run writes a report to the job summary, and on a pull request posts it as a comment (replacing its own previous one): each new finding with severity, file:line and a link to the task it created.
- No second UI. Triage happens in your tracker: close a task as declined and the finding is suppressed forever; close it as done and the next scan verifies the fix.
- Bring your own backend. The upload contract is a documented JSON envelope around SARIF; any backend implementing it works.
- v0: CLI — run scanners, merge SARIF, upload, gate on the response
- Dockerfile with pinned scanner versions
-
checkmode for merge requests (classify against baseline, no writes) - GitLab CI template and GitHub composite action
- Image published to ghcr.io on every push to main
- Pull-request comment + job summary with the delta table and links to the tasks
- More heads: Hadolint, Checkov, license audit, Zizmor
Scanner rules (e.g. the Semgrep registry) are fetched at runtime and are licensed by their respective owners; Cerberus does not bundle them.