GuardAPI

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 /x and GET /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: Bearer accepts).

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 key
  • STAGING_API_URL — HTTPS base URL, no trailing-path surprises (the spec paths are appended)
  • TOKEN_USER_A — owner tenant
  • TOKEN_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?
failB 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
pass401, 403, or 404 without leaked owner fields.No
inconclusive2xx (or other non-deny) without owner evidence. Empty harvest. No collection/item pairs.No
errorInvalid spec, TOKEN_A 401 or non-2xx on list, TOKEN_B 401.Yes (default)

What a leak looks like

Example finding Illustrative fixture. Not a customer scan.

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-path vs 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.

Start 14-day trial Why this check exists