Skip to main content

Gate CI/CD builds on APIsec scan results

Run an APIsec scan from your pipeline and fail the build when serious vulnerabilities exceed the limits you set. This shifts API security testing left: every push or merge request is scanned, and a build cannot ship while CRITICAL or HIGH findings breach your thresholds.

APIsec publishes a maintained Docker image — a scan-and-gate script — that your pipeline runs. In one step it triggers a scan of an application instance, waits for the scan to finish, counts the CRITICAL and HIGH findings, and exits non-zero when either count is over the limit. Your CI platform reads the exit code and passes or fails the build accordingly.

This guide covers the fastest path (generate a snippet in the console), the input reference, ready-to-use pipeline examples, and the exact pass/fail rules.

Prerequisites​

  • An onboarded APIsec application. You need its Application ID and Instance ID — the instance is the environment the scan runs against.

  • An API credential authorized to run and read scans. Either an API Token (Personal Access Token) or an OAuth Client Credential, with both of these scopes:

    • Initiate Scan (scan:init)
    • Get Scan Details (scan:get)

    Create either one from the API Credentials page.

  • A CI runner that can run docker run — for example, a GitHub-hosted ubuntu-latest runner. If you run the script directly without Docker, the runner needs bash, curl, jq, mktemp, and date instead.

  • Your API credential stored as a CI secret (GitHub Secrets, GitLab masked variables, or Jenkins credentials). Never commit it to your repository.

tip

For CI/CD, prefer short-lived OAuth Client Credentials over a long-lived API Token. A short-lived credential limits exposure if a pipeline log or secret store leaks. Both options and their scopes are documented on the API Credentials page.

For an authenticated scan, you also need a scan auth ID — the scanWithAuthId UUID for the credential the scan should use. Leave APISEC_AUTH_ID unset or empty for an unauthenticated scan. See Configure an authenticated scan.

Generate a pipeline snippet in the console​

The quickest way to start is to let the console generate a snippet pre-filled with your Application ID and Instance ID.

  1. In the console, open your application's detail page.
  2. Choose the CI/CD action. The CI/CD Integrations dialog opens.
  3. Select your platform tile — GitHub Actions, GitLab, or Jenkins — and click View.
  4. Copy the generated Integration Script into your pipeline configuration.
  5. Add your API credential as a CI secret named APISEC_TOKEN (see Add your credential as a secret).
warning

On cloud tenants the generated snippet does not include APISEC_HOST. Add it yourself before running the pipeline — without it the container exits 1 with missing --host or APISEC_HOST. Set it to your tenant's API base URL.

note

The Harness tile is listed but does not generate a snippet yet. For Harness, adapt the plain docker run example.

If you prefer to build the step by hand, use the pipeline examples below and set the input variables yourself.

Add your credential as a secret​

The image reads your credential from the APISEC_TOKEN environment variable. Store the value in your platform's secret manager and inject it into the job — do not place it on the command line, where it can land in process listings or logs.

  • GitHub Actions: add a repository or environment secret, then reference it as ${{ secrets.APISEC_TOKEN }}.
  • GitLab CI/CD: add a masked (and optionally protected) CI/CD variable named APISEC_TOKEN.
  • Jenkins: store it as a Secret text credential and bind it with withCredentials.

Configure thresholds​

You set two limits. The build fails when either is exceeded:

  • APISEC_MAX_CRITICAL — the most CRITICAL findings allowed before the build fails. Set 0 to fail on any CRITICAL finding.
  • APISEC_MAX_HIGH — the most HIGH findings allowed before the build fails.

The comparison is strictly greater-than. With APISEC_MAX_HIGH=5, a scan that reports exactly 5 HIGH findings passes; 6 fails. Only CRITICAL and HIGH findings are gated — there are no Medium or Low controls.

note

The gate counts vulnerabilities only. A scan result carries findings in two separate groups, vulnerabilities and issues; the threshold check reads the first and ignores the second. A build can therefore pass while the scan still reports issues, so review the full scan result in the console rather than treating a green build as "nothing found".

A common starting point is APISEC_MAX_CRITICAL=0 and APISEC_MAX_HIGH=5: block every CRITICAL, and allow a small, deliberate HIGH budget while your team works findings down.

Input reference​

The image reads its configuration from environment variables. Each variable has an equivalent CLI flag, shown in parentheses. There is no config file.

Variable (flag)RequiredDescription
APISEC_TOKEN (--token)YesAPI bearer token or PAT. Pass it as a secret, not on the command line. The Bearer prefix is optional.
APISEC_HOST (--host)YesAPI base URL, for example https://cloud.apisecapps.com.
APISEC_APP_ID (--app-id)YesApplication ID to scan.
APISEC_INSTANCE_ID (--instance-id)YesInstance (environment) ID to scan.
APISEC_MAX_CRITICAL (--max-critical)YesMaximum CRITICAL findings allowed before failing. 0 fails on any.
APISEC_MAX_HIGH (--max-high)YesMaximum HIGH findings allowed before failing.
APISEC_AUTH_ID (--auth-id)NoscanWithAuthId UUID for an authenticated scan. Leave unset or empty for an unauthenticated scan. The literal string null (any casing) is also treated as empty, which is convenient for CI systems that interpolate absent values as "null".
APISEC_POLL_INTERVAL (--poll-interval)NoSeconds between scan-status polls. Default 30.
APISEC_TIMEOUT (--timeout)NoMaximum seconds to wait for the scan to finish. Default 7200 (2 hours).
note

The API host in these examples is https://cloud.apisecapps.com. Self-hosted and regional deployments differ — set APISEC_HOST to your own tenant's API base URL.

Pipeline examples​

Each example injects secrets with -e and relies on the image entrypoint.

note

The examples use the image docker.io/apisec/apisec-scan-and-gate:latest. Pin a specific tag or digest for reproducible builds — see Pin the image version.

GitHub Actions​

Save as .github/workflows/apisec-scan-gate.yml. Store APISEC_TOKEN as a secret; store the IDs as repository variables.

name: APIsec scan and gate
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
jobs:
apisec-gate:
runs-on: ubuntu-latest
steps:
- name: Run APIsec scan and enforce thresholds
env:
APISEC_TOKEN: ${{ secrets.APISEC_TOKEN }}
APISEC_HOST: https://cloud.apisecapps.com
APISEC_APP_ID: ${{ vars.APISEC_APP_ID }}
APISEC_INSTANCE_ID: ${{ vars.APISEC_INSTANCE_ID }}
APISEC_AUTH_ID: ${{ vars.APISEC_AUTH_ID }}
APISEC_MAX_CRITICAL: "0"
APISEC_MAX_HIGH: "5"
IMAGE: docker.io/apisec/apisec-scan-and-gate:latest
run: |
docker run --rm \
-e APISEC_TOKEN -e APISEC_HOST -e APISEC_APP_ID -e APISEC_INSTANCE_ID \
-e APISEC_AUTH_ID -e APISEC_MAX_CRITICAL -e APISEC_MAX_HIGH \
"$IMAGE"

GitLab CI/CD​

Save as .gitlab-ci.yml. Set APISEC_TOKEN, APISEC_APP_ID, APISEC_INSTANCE_ID, and APISEC_AUTH_ID as masked CI/CD variables. This example runs Docker-in-Docker.

stages:
- test
apisec-scan-gate:
stage: test
image: docker:24-cli
services:
- name: docker:24-dind
alias: docker
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
APISEC_HOST: "https://cloud.apisecapps.com"
APISEC_MAX_CRITICAL: "0"
APISEC_MAX_HIGH: "5"
IMAGE: "docker.io/apisec/apisec-scan-and-gate:latest"
script:
- |
docker run --rm \
-e APISEC_TOKEN -e APISEC_HOST -e APISEC_APP_ID -e APISEC_INSTANCE_ID \
-e APISEC_AUTH_ID -e APISEC_MAX_CRITICAL -e APISEC_MAX_HIGH \
"$IMAGE"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == "main"

If your runners already expose Docker to jobs, drop the services: and DOCKER_HOST lines and keep the script block.

Jenkins​

A declarative pipeline. Bind the token with the Credentials plugin.

pipeline {
agent { label 'docker' }
environment {
IMAGE = 'docker.io/apisec/apisec-scan-and-gate:latest'
APISEC_HOST = 'https://cloud.apisecapps.com'
APISEC_APP_ID = '...'
APISEC_INSTANCE_ID = '...'
APISEC_AUTH_ID = '...'
APISEC_MAX_CRITICAL = '0'
APISEC_MAX_HIGH = '5'
}
stages {
stage('APIsec gate') {
steps {
withCredentials([string(credentialsId: 'apisec-token-secret-text', variable: 'APISEC_TOKEN')]) {
sh '''
docker run --rm \
-e APISEC_TOKEN -e APISEC_HOST -e APISEC_APP_ID -e APISEC_INSTANCE_ID \
-e APISEC_AUTH_ID -e APISEC_MAX_CRITICAL -e APISEC_MAX_HIGH \
"$IMAGE"
'''
}
}
}
}
}

Plain docker run​

Any runner with Docker can run the image directly. Set APISEC_TOKEN in the environment beforehand so it is not written on the command line.

docker run --rm \
-e APISEC_TOKEN \
-e APISEC_HOST=https://cloud.apisecapps.com \
-e APISEC_APP_ID -e APISEC_INSTANCE_ID -e APISEC_AUTH_ID \
-e APISEC_MAX_CRITICAL=0 -e APISEC_MAX_HIGH=5 \
docker.io/apisec/apisec-scan-and-gate:latest

Understand pass/fail behavior​

Your CI platform decides the build outcome purely from the exit code.

Exit 0 — the build passes when all of these hold:

  • The scan completes, and
  • CRITICAL count ≤ APISEC_MAX_CRITICAL, and
  • HIGH count ≤ APISEC_MAX_HIGH.

Exit 1 — the build fails when any of these occur:

  • Either threshold is exceeded.
  • An HTTP or usage error occurs (for example, a bad credential or a missing input).
  • The scan does not finish within APISEC_TIMEOUT.
  • The scan ends in any terminal status other than Complete.

Scan statuses​

A scan is either running or finished. There are four values:

StatusMeaningGate result
ProcessingStill running. The script keeps polling.—
CompleteFinished successfully.The only status that can pass
FailedThe scan run failed, or it timed out internally.Fails the build
AbortedThe scan was stopped before finishing, usually by a user.Fails the build

Only Complete passes the gate; Failed and Aborted fail it. The script recognizes a fixed set of terminal statuses. A status outside that set is treated as still running and polled until APISEC_TIMEOUT expires, which then fails the build — so an unexpected status shows up as a timeout rather than an immediate failure. Matching is case-insensitive.

When a scan does not complete, the API also returns a statusReason describing why — for example User Requested Abort. The script prints it, and it is the first thing to read when diagnosing a red build.

note

Scan status casing differs by surface. The API returns Complete, Failed, Aborted, and Processing in title case. The scan-history API filter accepts the lowercase tokens completed, failed, aborted, and running. The console displays Completed. Match the casing to whichever surface you are calling.

Script output​

The script prints progress and a summary to stderr. Every line carries an apisec-scan-and-gate: prefix:

apisec-scan-and-gate: scanId=0198c4a1-7b2e-7f31-9d64-2a5e8c1f4b70
apisec-scan-and-gate: terminal status=Complete
apisec-scan-and-gate: vulnerabilities CRITICAL=0 HIGH=3 (limits CRITICAL<=0 HIGH<=5)
apisec-scan-and-gate: exiting with status 0 (within thresholds)

This output is for your logs. The pass/fail decision comes from the exit code alone, so you do not need to parse it.

Track CI/CD scans in Scan History​

Scans launched this way are recorded in the console's Scan History with a Trigger Source of CI_CD. Use that column to see which findings came from a pipeline run.

The column shows the raw value, not a friendly label. The three possible values are MANUAL, CI_CD, and SCHEDULED.

Pin the image version​

The image is published as :latest and with immutable, timestamp-based version tags. For reproducible pipelines and predictable rollback, pin a specific version tag (or a digest) rather than :latest. Pinning is also better supply-chain hygiene: a build then runs the exact image you reviewed.

Alternative scan initiation​

You can also initiate a CI/CD scan by scan profile through the API, which lets you reuse a saved scan configuration. See Scan Profiles for what a profile controls.

note

The specific API endpoint and payload for launching a scan by profile are not documented here yet. Use the Docker image described above as the supported path until the endpoint is confirmed.