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:
-
POST /api/v1/scans— triggers a scan of the repo the token is scoped to. Takes no request body. Returns 202 with the new scan's id:{"scan_id": 4821, "status": "pending"} -
GET /api/v1/scans/{scan_id}— poll this for the result. While the scan pipeline detects a plausible scenario (a token's scoped repo credential was disconnected, an internal scanner crash) it eventually resolves to one of exactly three status values — pending, complete, or failed — there is no separate "running" or "error" state:{ "scan_id": 4821, "status": "complete", "summary": {"critical": 0, "high": 1, "medium": 3, "low": 5}, "correlated_count": 0, "gate": { "fail_on_severity": "high", "fail_on_correlated": true, "passed": false }, "gate_reason": "findings_over_threshold", "incomplete_scanners": [], "failure_reason": null }gate.passedis evaluated server-side against your token'sfail_on_severity/fail_on_correlatedsettings (set on the token in Settings → CI/CD Tokens) — your pipeline just needs to check this one field, not re-implement the gate logic itself. Treat anything other thantrueas a failed gate.
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):
findings_over_threshold— every scanner ran, and the findings tripped your token's threshold.scanners_incomplete— at least one scanner didn't run; seeincomplete_scanners.scanner_status_unknown— the scan doesn't record whether every scanner ran (a scan from before this was recorded), so its result can't be trusted. Run a new scan.scan_failed— the scan ended failed; seefailure_reason.
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