GitHub Action · GuardAPI/ghost-api@v6
Install the BOLA merge gate
The engine runs in your GitHub Actions runner. Tokens stay in GitHub Secrets. GuardAPI stores the verdict and redacted evidence only. Compute is your GitHub minutes, which is why PR runs are not metered.
Before you start
- An OpenAPI 3 spec in the repo (JSON or YAML) with
GET /xandGET /x/{id}pairs. - An HTTPS staging API you own. The Action will send real GETs from GitHub-hosted (or your) runners.
- Two test tenants. User A must own at least one listable object. User B must be a different tenant, not an admin of A’s data.
- Bearer tokens for both users (JWT, opaque session, whatever your
Authorization: Beareraccepts).
Row-level security in Postgres is not a substitute for this check. RLS can be correct and a handler can still load by primary key. This Action proves the HTTP behavior.
1. Login and copy the API key
Create an account, open the dashboard, copy the guard_ci_ key.
Trial is 14 days on one repository. Checkout is Stripe and requires being logged in.
2. Add repository secrets
GUARD_API_KEY— dashboard keySTAGING_API_URL— HTTPS base URL, no trailing-path surprises (the spec paths are appended)TOKEN_USER_A— owner tenantTOKEN_USER_B— attacker tenant
Do not put tokens in the workflow file. Do not send them to GuardAPI. The Action never POSTs them.
3. Workflow
name: bola
on: pull_request
permissions:
contents: read
security-events: write
jobs:
bola:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: GuardAPI/ghost-api@v6
with:
api-key: ${{ secrets.GUARD_API_KEY }}
openapi-path: ./openapi.json
base-url: ${{ secrets.STAGING_API_URL }}
token-a: ${{ secrets.TOKEN_USER_A }}
token-b: ${{ secrets.TOKEN_USER_B }}
fail-on-error: true
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: guardapi-bola.sarif ghost-api is the repository name on GitHub. The published Action is GuardAPI BOLA Gate v6.
fail-on-error defaults to true: proven leak or invalid credentials fail the job.
SARIF upload is optional. GitHub Code Scanning needs security-events: write and, on forks, the usual Code Scanning caveats.
The file is always written to guardapi-bola.sarif in the workspace.
OpenAPI requirements
- Only GET is probed. PUT, PATCH, DELETE are not in v6.
- A target is a collection GET plus an item GET whose last segment is a path parameter.
- If the collection path still contains
{parent}, that pair is skipped (no nested harvest in v6). - At most 40 pairs. Extra routes are not probed.
- List JSON should be an array, or wrapped in
data/items/results/records/content. - Object ids are read from the id parameter name, snake_case, or
id/uuid/_id. - Unique markers are other string fields ≥ 6 chars (emails, names, memos). Generic timestamps are skipped.
Verdicts
| Result | When | Blocks merge? |
|---|---|---|
| fail | B got HTTP 2xx and the body contains A’s resource id or unique markers. Also fail if a 401/403/404 body still contains those markers. | Yes |
| pass | 401, 403, or 404 without leaked owner fields. | No |
| inconclusive | 2xx (or other non-deny) without owner evidence. Empty harvest. No collection/item pairs. | No |
| error | Invalid spec, TOKEN_A 401 or non-2xx on list, TOKEN_B 401. | Yes (default) |
What a leak looks like
bola / GuardAPI BOLA Gate
failed in 12s
GET /invoices/inv_a8f2 as TOKEN_B
HTTP 200
body contains id "inv_a8f2" owned by tenant A
verdict: fail
reason: proven cross-tenant leak {
"id": "inv_a8f2",
"tenant_id": "org_acme",
"amount_cents": 129900,
"memo": "Q3 retainer — Acme"
} Fail only if B’s 2xx body contains A’s id or unique markers. 401 / 403 / 404 without those fields pass. 200 without owner evidence is inconclusive and does not block merge.
Staging constraints
- HTTPS only from the Action’s HTTP client expectations — use a real staging host, not localhost, unless you self-host runners that can reach it.
- Both tokens must work on the same base URL. Seed tenant A with at least one object the list endpoint returns.
- Do not point this at production customer data. Use fixtures.
- The runner’s IP must be allowed on staging if you IP-restrict.
Troubleshooting
- TOKEN_A rejected (HTTP 401) — expired JWT, wrong audience, or secret pasted with a newline. The job errors on purpose.
- TOKEN_A preflight failed (HTTP 404/500) — first list route in the graph is wrong, or staging is down. Check
openapi-pathvs the live router. - TOKEN_B rejected (HTTP 401) — attacker token invalid. Error, not pass.
- No GET collection/item pairs — spec has only nested routes or no GETs. Inconclusive.
- Always inconclusive — list returns objects without harvestable ids, or B’s 200 body is an empty envelope with no markers. Fix fixtures or id field names; do not “force fail on 200”.
- Could not ingest run — the check still ran locally; dashboard POST failed (key, plan, or repo cap). Trial is 1 repo; Team is 3; Scale is unlimited.
What v6 does not do
No public URL scanner. No A–F grade. No BFLA, GraphQL, or MCP. No DELETE/PUT/PATCH. No hosted runner hitting your API from Cloudflare. Those may exist later; they are not for sale today.