Most teams that say they’ve automated API security testing have actually only automated API scanning by setting up a tool that crawls the endpoints, throws known payloads at them, and reports what matches a signature. That’s a reasonable start, but it won’t catch the vulnerabilities responsible for most real API breaches: broken object-level authorization, broken function-level authorization, and authentication weaknesses that only show up when you test with multiple authenticated user roles at once.
This guide walks through the difference and shows how to build a CI/CD pipeline that runs the kind of authenticated, multi-role testing a security engineer would do by hand – automatically, on every deploy.

There’s a difference between running an automated scanner against your API and actually running automated security testing of your API – and the gap between the two is where most breaches happen.
An automated scanner crawls your endpoints, injects known attack payloads, and reports responses that match vulnerability patterns. That’s useful work, but it’s not the same as testing whether your API correctly enforces per-object authorization across two authenticated sessions, whether a standard user can invoke an admin-only function, or whether your JSON Web Token (JWT) signing secret can be cracked offline. Those aren’t edge cases – they are where the top API security risks of broken object-level authorization (BOLA), broken function-level authorization (BFLA), and broken authentication live. Taken together, they account for most of the API breaches that make headlines.
Automated security testing does what a security engineer would do manually: authenticate with multiple accounts, substitute object identifiers between sessions, invoke admin-only functions from a standard user session, probe rate-limiting thresholds, confirm injection is actually exploitable. The difference is that it does this at the speed and consistency a pipeline requires.
API attacks are outpacing website attacks by a wide margin – average attacks per API host ran 72% higher than attacks per website in the first half of 2025, according to Indusface’s State of Application Security report. Separately, Total Shift Left’s research puts API-related incidents at 43% of all data breaches in 2025, up 12% year over year. Meanwhile, organizations that build security testing into the pipeline rather than bolting it on at the end report up to a 60% reduction in production vulnerabilities and as much as a 90% reduction in remediation costs.
This guide walks through building automated API security testing that actually covers what matters: the OWASP API Security Top 10, authenticated authorization testing, and injection testing with exploit confirmation – not just a scan report.
Automated API security testing is the practice of programmatically executing structured security tests against API endpoints – including authenticated multi-role authorization testing, injection testing with exploit confirmation, authentication weakness detection, and stateful multi-step workflow testing – without requiring a security engineer to run each test by hand. It differs from automated scanning because it tests specific vulnerability classes through contextual, authenticated interaction rather than matching traffic against known signatures.
Before touching pipeline configuration, it’s worth being explicit about vulnerability scope. The seven classes that matter most for automated coverage map directly onto the OWASP API Security Top 10:
A standard scanner may already handle some of this but fundamentally cannot handle the rest because that requires context a signature-matching tool doesn’t have. Getting all this context and systematically acting on it is why you need to build a deliberate pipeline instead of pointing a generic scanner at a staging URL and calling it done:
There are four layers to get right:
Before working through them, it’s worth mentioning one technicality to clarify the whole process discussed below. Some API DAST tools simply expose a REST endpoint for triggering scans (POST /scans with a JSON body, then poll for a scan_id), but the workflow here is based specifically on how the Invicti Platform works.
The Invicti Platform integration runs as a Docker container (invicti-scan-cli) configured through environment variables, not a script that calls a scanning API directly. Your pipeline pulls the container, sets a handful of INVICTI_* variables, and runs it; the container handles triggering the scan, waiting for it to finish, and reporting results back into Invicti Platform. There’s no separate polling loop to write.
The four layers below map onto that model, and the code samples reflect it, though some of the values are illustrative rather than ready-to-paste – see Invicti’s official docs for those.
The scanner needs to know what it’s testing before it can test it. For API targets, this comes down to how the target itself is configured in Invicti Platform, not something you re-specify on every pipeline run:
The practical implication of the whole approach is that your CI/CD script doesn’t carry the API definition. It references a target ID that already has one attached.
# Illustrative only: retrieving a target ID to reuse across pipelines
curl -X GET \
"https://platform.invicti.com/api/inventory/v1/assets?assetType=target&pageSize=50&pageNumber=1" \
-H "accept: application/json" \
-H "X-Auth: $INVICTI_API_TOKEN"
# Target ID is in the response at $.items[0].idTarget IDs are stable, so you can save them alongside the application name once and reuse them across every pipeline that scans that target.
This is the layer that separates security testing from scanning. Without authenticated, multi-role access, BOLA, BFLA, and most authentication weaknesses are simply invisible to the tool. Here’s where the divergence from the typical “pass tokens into the scan request” pattern matters most: Invicti doesn’t take standard-user and admin-user credentials as pipeline parameters. Authentication is configured once on the target itself, in Invicti Platform, and the scan-cli container picks it up automatically at scan time using the target ID you pass in.
For multi-role, multi-session testing specifically, Invicti has a purpose-built method: IDOR / BOLA authentication, set once per API scan target. It’s designed around exactly the two-session comparison BOLA and BFLA testing depend on:
Exactly one of these is marked Default, and each uses an API key, basic authentication, or bearer token credentials.
Comparing the two Standard users tests horizontal access control, meaning whether one user can reach another same-level user’s data, which is the core BOLA scenario. Comparing a Standard user against the Admin user tests vertical access control, meaning whether a low-privilege user can reach admin-only functions, which is the BFLA scenario. Invicti signs in as each configured user and compares what they can each reach, so the comparison itself is what surfaces the vulnerability, not a separate rule written against specific endpoints.
One caution to keep in mind: multi-session tests like these actively attempt to bypass access controls, which makes them potentially riskier to run than a standard scan. Stick to staging or test environments, not live production setups (which is the best practice anyway).
The same credentials can also be managed from the API catalog rather than the target’s Authentication settings, which is convenient if you’re administering many APIs centrally. With the Invicti approach, the target-settings route is still the more robust and recommended default, since credentials entered there apply from the moment scanning starts.
The pipeline side of authentication is much simpler than the target side: it’s just the API token that lets the scan-cli container talk to Invicti Platform at all, stored as a CI/CD secret.
# Illustrative: the pipeline only needs to authenticate to Invicti Platform,
# not to the API under test - target auth is already configured separately
env:
INVICTI_API_TOKEN: ${{ secrets.INVICTI_API_TOKEN }}The scan profile (configured in Invicti Platform, then referenced by name from the pipeline) determines which vulnerability classes get tested and at what depth. This is also configured ahead of time rather than assembled inline in a script.
The profile itself – built once in the UI, or via the platform API if you’re managing it as code – is where you turn on the vulnerability classes that matter for API testing: BOLA, BFLA, injection with exploit confirmation, broken authentication, and the rest of the OWASP API Top 10 categories described earlier. The pipeline’s job is just to point at the right profile by name.
The last piece is deciding what happens with the results. Here again, the mechanism is different from a script that fetches a findings array and filters it: build-failure conditions are typically set when you generate the CI/CD script from the level of Invicti Platform integrations, where you can set thresholds like “fail the build on confirmed critical or high findings.” The container then exits with a failing status code if that threshold is crossed, and your pipeline treats that exit code as the gate.
Conceptually, the logic your pipeline is enforcing looks like this, even though you’re not writing the filtering code by hand:
IF any finding has severity >= threshold
AND finding state == confirmed
THEN fail the build
ELSE passThat confirmed-only condition is the single most important idea in this whole pipeline, however it’s implemented. It’s the difference between a gate developers trust and one they learn to route around – more on that below.
Across every CI/CD platform, the shape is the same: pull the invicti-scan-cli container, set a handful of environment variables, and run it as a build step, ideally after your application is built and deployed to a test environment rather than against source code. The container triggers the scan against the target, waits for it to finish, and (depending on how build-failure conditions are configured) exits with a non-zero status if the results cross your threshold. There’s no separate “wait for scan completion” step to write – the container blocks until the scan is done.
Invicti Platform also has a script generator that produces a working starting point with the right syntax and variables for a given platform and lets you set build-failure conditions visually. The examples below show the general shape, but the individual generated script is always the source of truth.
# .github/workflows/api-security-testing.yml
name: Automated API security testing
on:
push:
branches: [staging]
pull_request:
branches: [main]
jobs:
api-security-test:
runs-on: ubuntu-latest
container:
image: invicti/scan-cli:latest
env:
INVICTI_API_BASE_URL: "https://platform.invicti.com"
INVICTI_API_TOKEN: ${{ secrets.INVICTI_API_TOKEN }}
INVICTI_TARGET_ID: ${{ secrets.INVICTI_API_TARGET_ID }}
INVICTI_SCAN_PROFILE: "API Full Security Test"
INVICTI_SCAN_AGENT: "EphemeralAgent"
steps:
- name: Run Invicti API security scan
run: /home/invicti/scancli/ScanCLI
- name: Upload scan artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: invicti-api-scan-results
path: |
./scan-results/**
./reports/**
if-no-files-found: ignore
retention-days: 7Note the INVICTI_SCAN_AGENT value. Invicti’s regular cloud agent works for publicly reachable targets, but a staging API sitting behind a firewall – which is the normal case for an authenticated, multi-role testing scenario – needs EphemeralAgent instead. That type of agent spins up temporarily inside the CI/CD environment itself, runs the scan from there, and tears down afterward, so it can reach a target the cloud agent never could.
Jenkins needs the Docker Pipeline plugin, but the stage itself just runs the same container with the same environment variables:
// Jenkinsfile — API security testing stage
pipeline {
agent any
environment {
INVICTI_API_BASE_URL = "https://platform.invicti.com"
INVICTI_API_TOKEN = credentials('invicti-api-token')
INVICTI_TARGET_ID = credentials('invicti-api-target-id')
INVICTI_SCAN_PROFILE = "API Full Security Test"
INVICTI_SCAN_AGENT = "EphemeralAgent"
}
stages {
stage('API security testing') {
when { branch 'staging' }
steps {
sh 'docker pull invicti/scan-cli:latest'
sh '''
docker run --rm \
-e INVICTI_API_BASE_URL \
-e INVICTI_API_TOKEN \
-e INVICTI_TARGET_ID \
-e INVICTI_SCAN_PROFILE \
-e INVICTI_SCAN_AGENT \
invicti/scan-cli:latest
'''
}
}
}
}The container’s own exit code carries the pass/fail signal, based on whatever build-failure conditions you set when configuring the scan profile or the generated script – Jenkins doesn’t need separate logic to poll a scan ID or evaluate a findings list.
# .gitlab-ci.yml — API security testing stage
api-security-test:
stage: security
image: invicti/scan-cli:latest
only:
- staging
- merge_requests
variables:
INVICTI_API_BASE_URL: "https://platform.invicti.com"
INVICTI_SCAN_PROFILE: "API Full Security Test"
INVICTI_SCAN_AGENT: "EphemeralAgent"
script:
- /home/invicti/scancli/ScanCLIINVICTI_API_TOKEN and INVICTI_TARGET_ID should come from GitLab’s masked CI/CD variables rather than being written into the file, the same way you’d handle any other credential.
All these examples are conceptual rather than copy-paste-exact. If you’re an Invicti user, confirm variable names, image tags, and available options against Invicti’s CI-driven scans documentation and the linked CI environment variables reference before building against them.
This is the section that most “automate your API scanning” guides skip, because it’s the part that actually requires planning rather than just pointing a tool at a URL.
Invicti’s approach, as described in Layer 2 above, uses a single authentication method – IDOR / BOLA authentication, configured per API target – to drive both BOLA and BFLA testing, since both are really the same underlying mechanism (comparing what different authenticated users can reach) applied at two different levels.
BOLA is the horizontal case: two users at the same privilege level, one reaching data that belongs to the other. For a closer look at the vulnerability itself and detection methods, see How to detect BOLA in APIs.
For BOLA testing to produce meaningful results in an automated pipeline, a few things need to be true:
The part teams most often miss isn’t the scanner configuration but the test data behind it. Without realistic, distinctly owned objects for each user, there’s nothing for the comparison to catch, and the scan will silently produce zero BOLA findings – not because the API is secure, but because there was nothing to check. Here’s an example of setting up BOLA seed data:
# Illustrative: seed data for BOLA testing, run once during environment setup,
# independent of scanner configuration
def seed_bola_test_data(standard_user_a_id, standard_user_b_id):
for _ in range(5):
db.execute(
"INSERT INTO orders (user_id, total, status) VALUES (?, 99.99, 'completed')",
(standard_user_a_id,)
)
for _ in range(5):
db.execute(
"INSERT INTO orders (user_id, total, status) VALUES (?, 299.99, 'completed')",
(standard_user_b_id,)
)
db.commit()BFLA is the vertical case: a low-privilege user reaching functions meant only for a higher-privilege role. See How to test for BFLA for the underlying vulnerability and detection mechanics.
BFLA testing uses the same IDOR / BOLA authentication setup, just comparing a different pair of credential sets – a Standard user against the Admin user, rather than two Standard users against each other. The scanner signs in as both, then checks whether the Standard-level session can successfully invoke functions that should require the Admin credential.
The setup requirement that matters most here is privilege separation: the Admin credential needs to actually have elevated access in the target application, and the Standard credential needs to actually lack it. If both accounts happen to share the same effective permissions – a common mistake in staging environments that were cloned from a single seed user – the comparison has nothing to detect, for the same reason a BOLA scan with identical users finds nothing.
The confirmed-only gate condition described above is worth dwelling on, because it’s the decision that determines whether teams keep this pipeline running or quietly disable it after a few weeks – and what sets Invicti apart from many other tools.
Potential findings, i.e. pattern matches that might be real but haven’t been confirmed, get tracked for visibility but shouldn’t block a deployment. Confirmed findings, where proof-of-exploit evidence exists, should. This is proof-based scanning applied as an operational rule rather than a marketing term: only findings that have been shown to be exploitable get to fail a build.
The reason this matters is developer trust. A gate that blocks builds on theoretical pattern matches will produce false-positive failures often enough that developers stop taking it seriously, and a security gate nobody trusts gets bypassed or turned off. A gate that only blocks on confirmed, exploitable findings is trusted, respected, and acted on.
Rate limiting on the API under test can also produce misleading results – if the target throttles or rejects scan traffic, you get incomplete coverage rather than a clean report. For the Invicti Platform, scan throughput and timeout settings are configured on the scan profile or target, not passed as pipeline parameters. If the API under test enforces aggressive rate limits, that’s worth tuning before you can fully rely on the pipeline’s results.
GraphQL routes everything through a single endpoint, so discovery works differently than for a REST API: the scanner needs schema introspection rather than per-path crawling. Mutations also deserve particular attention, since that’s typically where BFLA-style issues show up.
When configuring a GraphQL scan target, make sure introspection is enabled (or a schema is supplied directly) and that mutation testing isn’t disabled by default – a scan that only exercises queries will miss most of the authorization surface that matters.
For a microservices architecture, each service is generally its own scan target, with its own target ID, its own authentication configuration, and potentially its own scan profile (if services differ meaningfully in what they expose). A pipeline testing multiple services typically runs the scan-cli container once per service, with each invocation pointed at a different INVICTI_TARGET_ID, rather than trying to fold multiple services into a single scan configuration.
A pipeline that runs on every deploy isn’t automatically a good pipeline. Four metrics tell you whether the automation is doing real work:
Automated scanning finds patterns. Automated API security testing – multi-role authenticated BOLA testing, cross-role BFLA testing, authentication weakness detection, and injection testing with exploit confirmation – finds the vulnerabilities behind most real API breaches. The Invicti platform brings proof-based scanning to API testing so that pipeline gates only block on confirmed, exploitable issues instead of suspected pattern matches.
It’s the programmatic execution of structured security tests against API endpoints – multi-role authenticated BOLA testing, function-level authorization testing, injection testing with exploit confirmation, authentication weakness detection, and stateful workflow testing – without a security engineer running each test by hand. It differs from automated scanning in that it tests specific vulnerability classes through authenticated, contextual interaction rather than signature matching.
Scanning crawls endpoints, injects known payload patterns, and flags matches. It’s fast and covers a wide surface, but it can’t test authorization failures that require multi-user or multi-role context, can’t confirm whether an injection point is actually exploitable, and can’t test authentication weaknesses that require specific token manipulation. Security testing simulates the fuller attacker workflow – authenticated sessions, cross-user access attempts, role escalation – and produces confirmed findings instead of pattern-match alerts.
At minimum, the OWASP API Security Top 10: BOLA, broken authentication, broken object property authorization, unrestricted resource consumption, BFLA, security misconfiguration, injection, and improper inventory management. BOLA and BFLA are the categories that differentiate real testing from scanning, since both require two authenticated sessions running at once.
Rather than passing credentials into each pipeline run, authentication for the API under test is configured once against the target itself. Invicti, for example, has a dedicated IDOR / BOLA authentication method on API targets that takes an Admin credential set and two Standard credential sets, so it can compare sessions and detect access control issues. The pipeline itself then only needs to authenticate to the testing platform, usually with a single API token stored as a CI/CD secret.
Proof-based testing confirms a vulnerability is actually exploitable before it’s reported, rather than flagging a pattern match that might or might not be real. In an automated pipeline this is what makes a reliable gate possible: only confirmed findings block a build. Without it, gates fail on theoretical issues often enough to erode developer trust within weeks, and teams either disable the gate or downgrade it to informational-only.
You need two credential sets at the same privilege level, each belonging to a user who owns distinct resources in the environment under test, configured wherever your platform sets up multi-session authentication. The platform signs in as both users and compares what each can reach, flagging cases where one user’s session returns the other’s data. None of this works without realistic seed data behind those accounts – without actual owned objects, there’s nothing to cross-test.
It depends on scope. A lightweight scan of recently changed endpoints, suited to PR-level feedback, typically finishes in a few minutes. A full authenticated scan covering BOLA, BFLA, injection, and authentication testing across 50 to 200 endpoints generally runs somewhere in the 15-to-45-minute range, which is still compatible with pre-release gating in most pipelines.
Automated testing gives consistent, repeatable coverage of known vulnerability classes on every deployment. Manual pentesting goes deeper on complex attack chains, business logic flaws that need human reasoning, and novel vulnerability classes automation doesn’t yet cover. Most mature programs run both: automated testing continuously, manual pentesting periodically for depth.
Most API DAST platforms, including Invicti, support GitHub Actions, Jenkins, GitLab CI, Azure Pipelines, and similar tools, generally through a container-based CLI configured with environment variables rather than a custom script per platform. Triggers can be configured for pull requests, staging deployments, or scheduled scans, and build-failure conditions determine whether confirmed findings above a set severity threshold cause the pipeline to fail. Findings are usually exportable to issue trackers like Jira or ServiceNow so remediation routes to the right team automatically.
