Barrion Docs
Public API

CI recipes

Gate a deploy on a passive scan, and run pentests on a schedule, from GitHub Actions, GitLab CI or any shell.

Two patterns cover most pipelines:

  • A passive scan on every deploy. Free, done in minutes, and it can fail the build.
  • A pentest on a schedule or a release. It takes hours and costs credits, so start it and move on, and read the results in a later job instead of keeping a runner waiting.

Every recipe needs a key stored as a CI secret named BARRION_API_KEY. Give it only the permissions it needs, and a budget that fits how often it runs. See API keys.

Fail a deploy on a passive scan

This script starts a scan, waits for it, and fails if it found anything at the severities you choose. It needs curl and jq, which the standard GitHub and GitLab runners have.

barrion-scan.sh
#!/usr/bin/env bash
# Usage: barrion-scan.sh <url> [severities]   e.g. barrion-scan.sh https://staging.example.com critical,high
set -euo pipefail
URL="$1"
FAIL_ON="${2:-critical,high}"
API="https://api.barrion.io/v1"
AUTH="Authorization: Bearer $BARRION_API_KEY"
# The same key on a retried step, a new one when the pipeline is re-run.
IDEMPOTENCY_KEY="${IDEMPOTENCY_KEY:-scan-$(date +%s)}"

scan_id=$(curl -sS --fail-with-body -X POST "$API/scans" \
  -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d "{\"url\": \"$URL\"}" | jq -r .id)
echo "Scan $scan_id started"

for _ in $(seq 1 40); do
  status=$(curl -sS --fail-with-body "$API/scans/$scan_id" -H "$AUTH" | jq -r .status)
  [ "$status" = "COMPLETED" ] && break
  [ "$status" = "FAILED" ] && { echo "The scan failed"; exit 1; }
  sleep 15
done
[ "$status" = "COMPLETED" ] || { echo "The scan did not finish in 10 minutes"; exit 1; }

result=$(curl -sS --fail-with-body "$API/scans/$scan_id/findings?severity=$FAIL_ON" -H "$AUTH")
count=$(echo "$result" | jq .total)
echo "$result" | jq -r '.findings[] | "\(.severity)\t\(.name)"'
if [ "$count" -gt 0 ]; then
  echo "$count finding(s) at $FAIL_ON"
  exit 1
fi
echo "No findings at $FAIL_ON"

GitHub Actions

.github/workflows/barrion-scan.yml
name: Barrion scan
on:
  deployment_status:
jobs:
  scan:
    if: github.event.deployment_status.state == 'success'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Passive scan
        env:
          BARRION_API_KEY: ${{ secrets.BARRION_API_KEY }}
          IDEMPOTENCY_KEY: scan-${{ github.run_id }}-${{ github.run_attempt }}
        run: bash barrion-scan.sh "${{ github.event.deployment_status.environment_url }}" critical,high

GitLab CI

.gitlab-ci.yml
barrion-scan:
  stage: test
  image: alpine:3.20
  before_script:
    - apk add --no-cache bash curl jq
  variables:
    # Stable when the job is retried, new when the pipeline runs again.
    IDEMPOTENCY_KEY: scan-$CI_PIPELINE_ID-$CI_JOB_NAME
  script:
    - bash barrion-scan.sh https://staging.example.com critical,high

Add BARRION_API_KEY as a masked CI/CD variable.

Run a pentest on a schedule

A pentest takes hours, so no job should wait for one. One scheduled workflow starts it and saves its id. Another, running later, checks that same run and fails if its released findings include anything you care about.

barrion-pentest-start.sh
#!/usr/bin/env bash
# Starts a pentest and prints its id. Needs TARGET_URL, VERIFICATION_ID and IDEMPOTENCY_KEY.
set -euo pipefail
response=$(curl -sS --fail-with-body -X POST "https://api.barrion.io/v1/pentests" \
  -H "Authorization: Bearer $BARRION_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d "{\"targetUrl\": \"$TARGET_URL\", \"verificationId\": \"$VERIFICATION_ID\", \"depth\": \"LEAD\"}")
id=$(jq -r .id <<<"$response")
if [ -z "$id" ] || [ "$id" = "null" ]; then
  echo "No pentest id in the response: $response" >&2
  exit 1
fi
echo "$id"

The check looks at one run, by id, and answers with one of three exit codes, so a run that has not finished yet is never mistaken for a clean one:

barrion-pentest-check.sh
#!/usr/bin/env bash
# Usage: barrion-pentest-check.sh <pentest id> [severities]
# Exit 0: finished, nothing at those severities.
# Exit 1: findings at those severities, or the run failed or was cancelled.
# Exit 2: not finished yet, or its report is still being reviewed.
set -euo pipefail
ID="$1"
FAIL_ON="${2:-critical,high}"
API="https://api.barrion.io/v1"
AUTH="Authorization: Bearer $BARRION_API_KEY"

run=$(curl -sS --fail-with-body "$API/pentests/$ID" -H "$AUTH")
status=$(jq -r .status <<<"$run")
released=$(jq -r .reportReleased <<<"$run")

case "$status" in
  COMPLETED) ;;
  FAILED | CANCELLED) echo "Pentest $ID ended $status, so nothing was tested"; exit 1 ;;
  *) echo "Pentest $ID is still $status"; exit 2 ;;
esac
if [ "$released" != "true" ]; then
  echo "Pentest $ID is complete; its report is still being reviewed"
  exit 2
fi

findings=$(curl -sS --fail-with-body "$API/pentests/$ID/findings?severity=$FAIL_ON&limit=100" -H "$AUTH")
total=$(jq .total <<<"$findings")
# A report can be taken back between the two requests: judge only a released
# report with a real count, and treat anything else as not finished.
if [ "$(jq -r .reportReleased <<<"$findings")" != "true" ] || ! [[ "$total" =~ ^[0-9]+$ ]]; then
  echo "Pentest $ID's report is not released any more; checking again later"
  exit 2
fi
jq -r '.findings[] | "\(.severity)\t\(.title)"' <<<"$findings"
if [ "$total" -gt 0 ]; then
  echo "$total finding(s) at $FAIL_ON"
  exit 1
fi
echo "No findings at $FAIL_ON"

A Light run's findings are released while it is still running, and a Standard or Deep run's only after review, so the check waits for both a COMPLETED status and a released report. See Read the findings.

GitHub Actions

The start workflow saves the run's id as an artifact:

.github/workflows/barrion-pentest.yml
name: Barrion pentest
on:
  schedule:
    - cron: "0 2 * * 1"   # Mondays at 02:00 UTC
  workflow_dispatch:
jobs:
  start:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Start pentest
        env:
          BARRION_API_KEY: ${{ secrets.BARRION_API_KEY }}
          # run_id alone: re-running this run gets the same pentest back instead of paying for a second.
          IDEMPOTENCY_KEY: pentest-${{ github.run_id }}
          TARGET_URL: https://staging.example.com
          VERIFICATION_ID: ${{ vars.BARRION_VERIFICATION_ID }}
        run: bash barrion-pentest-start.sh > pentest-id.txt && cat pentest-id.txt
      - uses: actions/upload-artifact@v4
        with:
          name: barrion-pentest
          path: pentest-id.txt
          retention-days: 14

The check workflow fetches that id from the newest successful start, and checks that run:

.github/workflows/barrion-pentest-check.yml
name: Barrion pentest check
on:
  schedule:
    - cron: "0 6 * * *"   # daily at 06:00 UTC
  workflow_dispatch:
permissions:
  actions: read
  contents: read
jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Find the pentest the start workflow saved
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          run_id=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow barrion-pentest.yml \
            --status success --limit 1 --json databaseId -q '.[0].databaseId')
          gh run download "$run_id" --repo "$GITHUB_REPOSITORY" --name barrion-pentest
          echo "PENTEST_ID=$(cat pentest-id.txt)" >> "$GITHUB_ENV"
      - name: Check it
        env:
          BARRION_API_KEY: ${{ secrets.BARRION_API_KEY }}
        run: |
          code=0
          bash barrion-pentest-check.sh "$PENTEST_ID" critical,high || code=$?
          if [ "$code" -eq 2 ]; then
            echo "::notice::The pentest has not finished yet. The next scheduled check will look again."
            exit 0
          fi
          exit "$code"

The same two scripts work in any CI that can run a schedule and keep a file between runs, GitLab included.

Stopping a run when a job is cancelled

This only applies to a job that starts a pentest and then waits for it, for example a short Light run on a release. If that job is cancelled, stop the run too, using the id the start step saved:

      - name: Start pentest
        run: |
          id=$(bash barrion-pentest-start.sh)
          echo "PENTEST_ID=$id" >> "$GITHUB_ENV"
      # ... steps that wait for the run ...
      - name: Stop the pentest
        if: cancelled() && env.PENTEST_ID != ''
        run: |
          curl -sS -X POST "https://api.barrion.io/v1/pentests/$PENTEST_ID/cancel" \
            -H "Authorization: Bearer ${{ secrets.BARRION_API_KEY }}" \
            -d ''

Use cancelled(), not always(): always() also runs when everything went well, and would stop the run you just started. Do not add this step to the start-only workflow above, where the job ends as soon as the run has started. A cancelled run is charged only for the work it got through.