Skip to content

Run Bravos in CI/CD

There are two different things people mean by “Bravos in CI”, and only one of them needs a licence.

What it is Needs a licence
Gate and file a pipeline reads a ledger a run already produced, fails the build on it, and files it with a server no (the push needs a server, which is licensed)
Run the loop the pipeline itself annotates, probes and verifies, inside the prebuilt runner image yes

This page covers the first in full, because it works today with nothing but the free CLI. The second is described at the end, honestly, including what is not published yet.

  • The CLI on the runner. See Install Bravos.
  • A run whose ledger is available to the job. On a pipeline that does not run the loop itself, that means the $BRAVOS_HOME the job can see holds one.
  • For the push step: a server URL and a member token. See Connect the CLI to a server.

bravos ci and bravos push are different commands and only one of them talks to a server. A pipeline usually runs both: the gate decides whether the build passes, and the push makes the finding visible to the security team.

- run: bravos ci # the gate. local, no server, no credential
- run: bravos push # files the run with bugb-server
env:
BRAVOS_SERVER_URL: https://bravos.example.com
BRAVOS_TOKEN: ${{ secrets.BRAVOS_TOKEN }}
bravos ci bravos push
Contacts a server no yes
Credential none BRAVOS_TOKEN, role member or above
Where the credential lives n/a your CI provider’s secret store
Decides the build yes, by exit code no
Runs a scan no no

Neither command runs a scan. Both read an artifact a completed run already wrote, which is why either can be re-run as often as the pipeline likes and always answers the same way.

  1. Order them gate-first. bravos push fails on a server that is unreachable or whose licence has lapsed, and a push placed before the gate would fail the build for a reason that has nothing to do with the code under test. Put the gate first so its exit code is what decides.

  2. Store the token in your CI provider’s secret store, never in the repository. There is no git config key for it on purpose: a bearer token in a checkout is one git add -A away from being committed.

  3. Mint one token per pipeline, so revoking one does not revoke a developer. A seat is a distinct identity, so a pipeline’s token costs a seat.

Branch on the exit code, not on the output

Section titled “Branch on the exit code, not on the output”
0 passed
1 the policy was breached
2 the gate could not evaluate
3 the runner is missing something Bravos needs

2 is the one that matters most in a pipeline. A gate that cannot find its ledger has proven nothing, and a pipeline that treats “could not evaluate” as “passed” is a green light with nothing behind it. Bravos keeps them apart deliberately: a run that died before it wrote a ledger answers 2, not 0.

bravos: run '20260910-010702-virgin' never produced a ledger, so there is nothing to gate on — it did not get past preflight. Run one that completes (`bravos auto <repo> --attestation ...`), or name a finished run with --run (run_id='20260910-010702-virgin')

A finished run that found nothing still passes. The predicate is whether a ledger was written, not what is in it.

Machine-readable output carries the same decision:

Terminal window
bravos ci --format json
{
"schema": "bravos.ci/v1",
"run_id": "20260910-004305-demo",
"codebase": "/Users/you/demo",
"policy": {
"source": null,
"fail_on_severity": "high",
"max_confirmed": null,
"fail_on": [
"confirmed"
]
},
"passed": true,
"exit_code": 0,
"counts": {
"total": 4,
"counted": 0,
"by_severity": {},
"breaches": 0
},
"breaches": [],
"findings": []
}

exit_code is in the payload as well as on the process, so a step that captured stdout does not have to have kept $?. See Gate a build on a run for the policy file and what fail_on chooses between.

bravos ci --reporter github renders the verdict onto GitHub Actions’ own surfaces, and is the default when Bravos detects it is running inside Actions. --reporter none turns that off.

The steps above gate on a ledger somebody else produced. Running the loop inside the pipeline is the licensed part of Bravos, and it exists because two things about a pipeline make a local recipe the wrong answer:

  • A job that installs cxg, guardlink, Playwright and a headless Chromium pays several minutes on every build.
  • A gate whose own toolchain floats is not a gate. A scanner that changes version between two builds makes their two verdicts incomparable.

Both are answered by one artifact: ghcr.io/bugb-technologies/bugb-ci, a runner image with every component pinned.

Component Pinned to
base ubuntu:24.04, by manifest digest
cxg v1.3.0, the public release binary, SHA256-verified against the release’s own SHA256SUMS
the cxg pentest pipeline materialised at build time by cxg pentest install
playwright and Chromium the version that materialised pipeline’s own requirements.txt names
guardlink 2.0.0, from public npm
bravos a pinned commit, installed from source into the image

It runs as a non-root user with a real writable $HOME, because cxg’s auth store is created 0700 on first use and the --ci guard refuses a directory with any other-user bit set.

The image is published to a private registry, the same one the server image lives in, and pulling it needs a credential that comes with your licence. The tag is pinned in every template, never latest: a gate that pulls a different runner tomorrow is not reproducible.