Prasovi Prasovi
← Back to Help

CI/CD Pipeline Gate Setup

Full setup instructions for gating a build on a Prasovi scan from GitHub Actions, GitLab CI, Azure Pipelines, or Bitbucket Pipelines. See Help & Documentation for the product-level overview of what the gate does.

1. Mint a token

From Settings → CI/CD Tokens, create a token scoped to one repository. It's frozen to that repo's provider/project at creation time — there's nothing else to configure on it once it's minted. Copy the raw token value now; like any API credential, it's only shown once.

2. How the API works

Every request is authenticated with Authorization: Bearer <token>. There are two calls:

gate.passed is only true when every scanner actually ran. A code scan runs four scanners (gitleaks, semgrep, trivy and trufflehog); incomplete_scanners lists any that didn't produce a result, each as a scanner name and a reason code (timeout, nonzero_exit, crash, missing_binary or bad_output). It is [] when all four ran, and null while the scan is pending, for a failed scan, and for a scan from before this was recorded. A scanner that didn't run found nothing because it never looked, so a non-empty list fails the gate whatever the findings.

When gate.passed is false, gate_reason says why, as one fixed code (it is null otherwise):

failed means the scan produced no results at all: the repository couldn't be cloned (clone_failed), something failed on our side (scan_error), or the scan ran too long and was stopped (stale). A scanner that crashed, timed out or is missing does not fail the scan: the scan completes, lists that scanner in incomplete_scanners, and the gate fails with gate_reason scanners_incomplete. A failed scan is not a security-gate result: a failed scan has no summary worth reading (its gate.passed is false with gate_reason scan_failed), and should fail your build with a distinct "scan didn't run, check the dashboard" message rather than being confused with a scan that ran fine and found findings above your threshold. failure_reason gives the cause as one fixed code: clone_failed (the repository couldn't be cloned — check the token's access), scan_error (something failed on our side) or stale (the scan ran past our time limit and was stopped). It is null for a scan that hasn't failed, and for older failed scans.

A non-2xx response (both calls) returns a JSON body with a human-readable detail field explaining what went wrong — surface this directly in your pipeline logs rather than a generic "request failed". The two cases you'll hit in practice:

// token missing, malformed, or revoked (401)
{"detail": "Invalid or revoked API token"}

// weekly scan quota used up (429) — shown here for the Starter plan
{"detail": "You've reached your weekly limit of 5 code scans on the Starter plan. Upgrade for more."}

3. How long a scan takes

A single code scan typically finishes in under a minute. Triggering scans across multiple repos at once can take a few minutes in total. An infrastructure (cloud) scan can take several minutes — Prowler checks dozens of cloud services per run, so it's meaningfully slower than a code scan. The 20-minute poll timeout used in every example below covers the slowest a code scan can legitimately run; a scan still running 25 minutes after it started is stopped within the next few minutes and reported as failed with failure_reason stale.

4. GitHub Actions

Add the token as a repository secret: Settings → Secrets and variables → Actions → New repository secret. Reference it as ${{ secrets.PRASOVI_CI_TOKEN }} — GitHub masks secret values in logs automatically.

# .github/workflows/prasovi-scan.yml
name: Prasovi security gate
on: [push]

jobs:
  prasovi-scan:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger and gate on Prasovi scan
        env:
          PRASOVI_TOKEN: ${{ secrets.PRASOVI_CI_TOKEN }}
          PRASOVI_API: https://prasovi.com/api/v1/scans
        run: |
          set -e

          trigger=$(curl -sS -w '\n%{http_code}' -X POST "$PRASOVI_API" \
            -H "Authorization: Bearer $PRASOVI_TOKEN")
          http_code=$(echo "$trigger" | tail -n1)
          body=$(echo "$trigger" | sed '$d')

          if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
            echo "Failed to trigger scan: $(echo "$body" | jq -r .detail)"
            exit 1
          fi

          scan_id=$(echo "$body" | jq -r .scan_id)
          echo "Triggered scan #$scan_id, polling for a result..."

          status="pending"
          for i in $(seq 1 120); do
            sleep 10
            poll=$(curl -sS "${PRASOVI_API}/$scan_id" -H "Authorization: Bearer $PRASOVI_TOKEN")
            status=$(echo "$poll" | jq -r .status)
            if [ "$status" = "complete" ] || [ "$status" = "failed" ]; then
              break
            fi
          done

          if [ "$status" = "failed" ]; then
            echo "Prasovi scan failed ($(echo "$poll" | jq -r .failure_reason)) — check the dashboard, this is not a gate result."
            exit 1
          fi
          if [ "$status" != "complete" ]; then
            echo "Timed out after 20 minutes waiting for scan #$scan_id to finish."
            exit 1
          fi

          passed=$(echo "$poll" | jq -r .gate.passed)
          if [ "$passed" != "true" ]; then
            echo "Prasovi security gate failed:"
            echo "$poll" | jq .
            exit 1
          fi
          echo "Prasovi security gate passed."

5. GitLab CI

Add the token as a CI/CD variable: Settings → CI/CD → Variables → Add variable. Name it PRASOVI_CI_TOKEN and check both Protect variable and Mask variable so it's hidden from job logs and only exposed on protected branches/tags.

# .gitlab-ci.yml
prasovi-scan:
  stage: test
  image: alpine:3
  variables:
    PRASOVI_API: https://prasovi.com/api/v1/scans
  before_script:
    - apk add --no-cache curl jq
  script:
    - |
      set -e

      trigger=$(curl -sS -w '\n%{http_code}' -X POST "$PRASOVI_API" \
        -H "Authorization: Bearer $PRASOVI_CI_TOKEN")
      http_code=$(echo "$trigger" | tail -n1)
      body=$(echo "$trigger" | sed '$d')

      if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
        echo "Failed to trigger scan: $(echo "$body" | jq -r .detail)"
        exit 1
      fi

      scan_id=$(echo "$body" | jq -r .scan_id)
      echo "Triggered scan #$scan_id, polling for a result..."

      status="pending"
      for i in $(seq 1 120); do
        sleep 10
        poll=$(curl -sS "${PRASOVI_API}/$scan_id" -H "Authorization: Bearer $PRASOVI_CI_TOKEN")
        status=$(echo "$poll" | jq -r .status)
        if [ "$status" = "complete" ] || [ "$status" = "failed" ]; then
          break
        fi
      done

      if [ "$status" = "failed" ]; then
        echo "Prasovi scan failed ($(echo "$poll" | jq -r .failure_reason)) — check the dashboard, this is not a gate result."
        exit 1
      fi
      if [ "$status" != "complete" ]; then
        echo "Timed out after 20 minutes waiting for scan #$scan_id to finish."
        exit 1
      fi

      passed=$(echo "$poll" | jq -r .gate.passed)
      if [ "$passed" != "true" ]; then
        echo "Prasovi security gate failed:"
        echo "$poll" | jq .
        exit 1
      fi
      echo "Prasovi security gate passed."

6. Azure Pipelines

Add the token as a pipeline variable: pipeline's Edit → Variables → New variable, name it PrasoviCiToken, and check Keep this value secret. Secret variables aren't automatically mapped to an env var by name — map it explicitly in the step's env: block.

# azure-pipelines.yml
steps:
  - bash: |
      set -e

      trigger=$(curl -sS -w '\n%{http_code}' -X POST "$PRASOVI_API" \
        -H "Authorization: Bearer $PRASOVI_TOKEN")
      http_code=$(echo "$trigger" | tail -n1)
      body=$(echo "$trigger" | sed '$d')

      if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
        echo "Failed to trigger scan: $(echo "$body" | jq -r .detail)"
        exit 1
      fi

      scan_id=$(echo "$body" | jq -r .scan_id)
      echo "Triggered scan #$scan_id, polling for a result..."

      status="pending"
      for i in $(seq 1 120); do
        sleep 10
        poll=$(curl -sS "${PRASOVI_API}/$scan_id" -H "Authorization: Bearer $PRASOVI_TOKEN")
        status=$(echo "$poll" | jq -r .status)
        if [ "$status" = "complete" ] || [ "$status" = "failed" ]; then
          break
        fi
      done

      if [ "$status" = "failed" ]; then
        echo "Prasovi scan failed ($(echo "$poll" | jq -r .failure_reason)) — check the dashboard, this is not a gate result."
        exit 1
      fi
      if [ "$status" != "complete" ]; then
        echo "Timed out after 20 minutes waiting for scan #$scan_id to finish."
        exit 1
      fi

      passed=$(echo "$poll" | jq -r .gate.passed)
      if [ "$passed" != "true" ]; then
        echo "Prasovi security gate failed:"
        echo "$poll" | jq .
        exit 1
      fi
      echo "Prasovi security gate passed."
    displayName: 'Prasovi security gate'
    env:
      PRASOVI_TOKEN: $(PrasoviCiToken)
      PRASOVI_API: https://prasovi.com/api/v1/scans

7. Bitbucket Pipelines

Add the token as a repository variable: Repository settings → Repository variables, name it PRASOVI_CI_TOKEN, and check Secured so its value is masked in build logs.

# bitbucket-pipelines.yml
pipelines:
  default:
    - step:
        name: Prasovi security gate
        image: alpine:3
        script:
          - apk add --no-cache curl jq
          - export PRASOVI_API=https://prasovi.com/api/v1/scans
          - |
            set -e

            trigger=$(curl -sS -w '\n%{http_code}' -X POST "$PRASOVI_API" \
              -H "Authorization: Bearer $PRASOVI_CI_TOKEN")
            http_code=$(echo "$trigger" | tail -n1)
            body=$(echo "$trigger" | sed '$d')

            if [ "$http_code" -lt 200 ] || [ "$http_code" -ge 300 ]; then
              echo "Failed to trigger scan: $(echo "$body" | jq -r .detail)"
              exit 1
            fi

            scan_id=$(echo "$body" | jq -r .scan_id)
            echo "Triggered scan #$scan_id, polling for a result..."

            status="pending"
            for i in $(seq 1 120); do
              sleep 10
              poll=$(curl -sS "${PRASOVI_API}/$scan_id" -H "Authorization: Bearer $PRASOVI_CI_TOKEN")
              status=$(echo "$poll" | jq -r .status)
              if [ "$status" = "complete" ] || [ "$status" = "failed" ]; then
                break
              fi
            done

            if [ "$status" = "failed" ]; then
              echo "Prasovi scan failed ($(echo "$poll" | jq -r .failure_reason)) — check the dashboard, this is not a gate result."
              exit 1
            fi
            if [ "$status" != "complete" ]; then
              echo "Timed out after 20 minutes waiting for scan #$scan_id to finish."
              exit 1
            fi

            passed=$(echo "$poll" | jq -r .gate.passed)
            if [ "$passed" != "true" ]; then
              echo "Prasovi security gate failed:"
              echo "$poll" | jq .
              exit 1
            fi
            echo "Prasovi security gate passed."

Questions about integrating the gate into your pipeline? Reach out at [email protected].

← Back to Help