Security

Security in CI/CD

A security scan is most useful when it runs on every change, not when someone remembers to click Run. Routebase exposes the whole scan lifecycle over its REST API, so a pipeline can queue a scan against a deployed environment, wait for it, fail the build on findings above a severity you choose, and hand the results to your code-scanning tool as SARIF 2.1.0.

This guide covers the automation path. The scan itself — profiles, scanners, personas, triage — is described in the Security overview.

What you need

Prerequisite Where it comes from
Pro plan The Security area is a Pro feature — on Free and Starter the app shows an upgrade prompt instead of the tools.
An API key Settings → API Keys. Create it with Full Access: queueing a scan needs security:execute and write access to the project, reading runs and findings needs security:read. Store it as a pipeline secret; see API Keys.
A scan profile Build it in the UI first (Scan profiles). The pipeline runs an existing profile; it does not create one.
A reachable target The profile's environment base URL has to be reachable from Routebase — a deployed staging or preview environment, not localhost.

Authenticate every request with the key in an X-API-Key header against https://api.routebase.dev.

If your organization is hosted in the US region, add X-RB-Region: us to every request as well. One address serves both regions, and the browser app picks the region from a cookie your pipeline doesn't have — without the header, requests land in the EU region and are rejected. EU organizations need nothing extra. Check your region under Settings → Organization; see Region selection.

Finding the two ids

The project id is the GUID in the app URL: app.routebase.dev/projects/<project-id>/security.

The profile id is not shown in the UI, so read it from the API once and paste it into your pipeline configuration:

curl -s -H "X-API-Key: $ROUTEBASE_API_KEY" \
  "https://api.routebase.dev/api/projects/$PROJECT_ID/security/scan-profiles"

Each entry carries its id, name, and isEnabled. A disabled profile cannot be run.

The three calls

1. Queue the scan

POST /api/projects/{projectId}/security/scan-profiles/{profileId}/run

Returns 202 Accepted with the scan run, including its id. The scan runs as a background job — the response comes back immediately, before any scanner has started.

2. Poll until it finishes

GET /api/projects/{projectId}/security/scan-runs/{runId}

The status field moves through queuedrunning → one of completed, failed, or cancelled. Only completed means the scanners finished; failed carries an errorMessage. The same response also carries securityScore, openFindingsCount, scannersCompleted, and enabledScannerCount, so a pipeline can print progress while it waits.

How long to allow depends on the profile's Time budget (default 600 seconds, configurable up to an hour) — give the poll loop at least that, plus queueing time.

3. Read the results

Two shapes, for two purposes:

Call Use it for
GET …/security/findings?status=open Gating the build — a JSON list with a severity per finding (critical, high, medium, low, info) and a total count.
GET …/security/findings/export/sarif Uploading to a code-scanning tool — application/sarif+json, defaulting to open findings.

Both are project-wide and reflect the current triage state, not just the run you started: a finding you marked Accepted risk last week does not come back as open. That is deliberate — the gate should reflect your posture, not one scan in isolation.

The SARIF export

curl -sS -H "X-API-Key: $ROUTEBASE_API_KEY" \
  -o routebase.sarif \
  "https://api.routebase.dev/api/projects/$PROJECT_ID/security/findings/export/sarif"

The document is SARIF 2.1.0 with a single run, and it is shaped for GitHub Code Scanning in particular:

  • Findings are grouped into rules by guidance id — one SARIF rule per issue class, carrying its title as the short description and the full remediation text as the long one. Each finding becomes one result referencing its rule.
  • Severity is expressed twice, because SARIF consumers read it differently. The result level is error for Critical and High, warning for Medium, note for Low, and none for Info. Alongside it, a security-severity property carries the numeric value GitHub buckets on (9.5 / 8.0 / 5.0 / 2.0 / 0.0). A rule's security-severity is the worst severity among its findings.
  • The endpoint path is the location, so results land on the route they were raised against rather than all on one line.
  • Every result carries a partialFingerprints entry (routebase/v1). That is what lets a code-scanning tool recognise the same finding across runs instead of reporting it as new each time.
  • Each result also keeps its owaspCategory and HTTP method as properties, and every rule is tagged security plus its OWASP category.

Add ?status=fixed (or any other finding status) to export a different slice; the default is open findings.

A single export is capped at 1,000 findings, highest severity first.

Gating the build

There is no server-side pass/fail verdict — you decide the threshold in the pipeline. The pattern is to fetch the open findings and fail when any of them is at or above the severity you care about:

SEVERITIES=$(curl -sS -H "X-API-Key: $ROUTEBASE_API_KEY" \
  "https://api.routebase.dev/api/projects/$PROJECT_ID/security/findings?status=open&take=200" \
  | jq -r '.items[].severity')

if echo "$SEVERITIES" | grep -qx 'critical'; then
  echo "Critical findings present — failing the build."
  exit 1
fi

Start at critical on an existing API and tighten to high once the backlog is triaged — a gate that is red on day one gets switched off by day three.

Upload the SARIF even when the gate fails, so the findings still appear in your code-scanning tab and reviewers can see what broke the build rather than only that it did.

A complete GitHub Actions job

name: Routebase Security Scan

on:
  pull_request:
  schedule:
    - cron: "0 3 * * 1" # weekly, Monday 03:00 UTC
  workflow_dispatch:

permissions:
  contents: read
  security-events: write # required to upload the SARIF report

jobs:
  security-scan:
    runs-on: ubuntu-latest
    env:
      ROUTEBASE_API_KEY: ${{ secrets.ROUTEBASE_API_KEY }}
      API: https://api.routebase.dev
      # US-hosted organizations: add -H "X-RB-Region: us" to every curl below.
      PROJECT_ID: 00000000-0000-0000-0000-000000000000 # <-- your project id
      PROFILE_ID: 00000000-0000-0000-0000-000000000000 # <-- your scan profile id
    steps:
      - name: Queue the scan
        run: |
          RUN_ID=$(curl -sS -X POST -H "X-API-Key: $ROUTEBASE_API_KEY" \
            "$API/api/projects/$PROJECT_ID/security/scan-profiles/$PROFILE_ID/run" | jq -r .id)
          echo "RUN_ID=$RUN_ID" >> "$GITHUB_ENV"

      - name: Wait for completion
        run: |
          for _ in $(seq 1 120); do
            STATUS=$(curl -sS -H "X-API-Key: $ROUTEBASE_API_KEY" \
              "$API/api/projects/$PROJECT_ID/security/scan-runs/$RUN_ID" | jq -r .status)
            case "$STATUS" in
              completed) echo "Scan completed."; exit 0 ;;
              failed|cancelled) echo "Scan ended as $STATUS."; exit 1 ;;
            esac
            sleep 5
          done
          echo "Timed out waiting for the scan."; exit 1

      - name: Export SARIF
        if: always()
        run: |
          curl -sS -H "X-API-Key: $ROUTEBASE_API_KEY" -o routebase.sarif \
            "$API/api/projects/$PROJECT_ID/security/findings/export/sarif"

      - name: Upload SARIF
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: routebase.sarif
          category: routebase

      - name: Fail on critical findings
        run: |
          COUNT=$(curl -sS -H "X-API-Key: $ROUTEBASE_API_KEY" \
            "$API/api/projects/$PROJECT_ID/security/findings?status=open&severity=critical" | jq -r .total)
          echo "Open critical findings: $COUNT"
          [ "$COUNT" -eq 0 ]

The same three calls translate directly to GitLab CI, Azure Pipelines, Jenkins, or anything else that can run curl.

The same thing in one command

If installing a tool on the runner is acceptable, the Routebase CLI collapses the queue-poll-gate-export sequence into a single step:

dotnet tool install --global Routebase.Cli
routebase config set-api-key "$ROUTEBASE_API_KEY"

routebase scan "$PROJECT_ID" "$PROFILE_ID" \
  --fail-on critical \
  --format sarif \
  --output routebase.sarif

It queues the scan, polls until it finishes, writes the SARIF, and exits 1 when an open finding is at or above --fail-on — the same threshold logic as the jq block above, and the same project-wide, triage-aware view of findings. Upload the SARIF with if: always() as before, so a tripped gate still surfaces the findings.

Which to pick is a question about your runners, not about capability. The curl version needs nothing installed and works on any image; the CLI version is shorter and harder to get subtly wrong. Both authenticate with the same key, and the CLI needs the same X-RB-Region signal — as ROUTEBASE_REGION=us or routebase config set-region us. See CLI in CI/CD.

Choosing when it runs

Two schedules do different jobs, and most teams end up with both:

  • On every pull request against a preview or staging environment — catches an authorization regression while the change is still in review. Keep the profile small (the passive scanners, a modest time budget) so it finishes inside a normal CI wait.
  • Nightly or weekly against staging with the full profile, including the authorization scanners with personas and, where the environment is yours to stress, the fuzzers. This is the pass that has time to be thorough.

A scan profile can also carry its own cron schedule inside Routebase, which needs no pipeline at all — see Scan profiles. Use that when you want the scan on a clock; use CI when you want it tied to a change.

Point scans at environments you own. The fuzzers and the rate-limit probe deliberately generate load, and the rate-limit settings on the profile are what keep a scan from behaving like an attack against a shared target.