# CI/CD and automation

> Run SAST, DAST and infrastructure scans from GitLab, GitHub, Jenkins, Azure DevOps, Bitbucket or a script with a team API token, test the token with --check, and fail the build on what they find.

Source: https://scansuite.eu/documentation/reference/cicd
Last reviewed: 2026-10-19

`scansuite-ci` starts a ScanSuite scan from a CI job, a script or a terminal, waits for it, and fails the job when the scan finds confirmed vulnerabilities or secrets. It runs static analysis of the code (the default), dynamic scans of a running web application, and infrastructure scans of hosts, networks and container images. The scan runs on the ScanSuite server, so the CI runner needs only Python 3.8 or newer, or the client's container image `appsec4u/scansuite-ci:1`. This page covers client version 1.4.5.

> **ScanSuite Teams:** The client talks to the team API, so it needs ScanSuite Teams.

The steps below set up one pipeline. [GitLab CI step by step](https://scansuite.eu/documentation/reference/gitlab-ci) walks through the same setup in GitLab with screenshots and tested recipes.

> **Agents in CI:** Running a coding agent in the pipeline instead of the client? Follow [No user in the loop](https://scansuite.eu/documentation/agent-setup#no-user) in Connect your AI agent: the agent takes the token from the CI secret store, never from a file it writes.

## Before you start

| You need | How to check |
|---|---|
| A ScanSuite Teams server the runner reaches over HTTPS | curl -fsS <YOUR_SCANSUITE_URL>/ci/version answers with JSON that names the client version the server publishes. |
| The Team admin role in the team the pipeline scans for | The Teams page says Your role in this team: Team admin. A team admin issues the token; the rest can be done by anyone who can edit the pipeline. |
| A product for the results | It is listed on the Products page. Or let the first run create it (--create-product, see Products). |
| For the *-ai bundles and AI scanners: an AI provider for the team | After setup, --list-scanners marks mlsast NOT AVAILABLE with "No AI provider is set up for this team" when there is none. Set one up on the Teams page, AI tab; see AI providers and cost. |
| For the *-classic bundles, the DAST profiles and the classic infrastructure scanners: the scanner images on the server | On the server, in the installation directory: grep SCANSUITE_SCANNERS .env prints static, dynamic or all. A default installation prints none: a system administrator adds them with ./scansuite update --static-only, --dynamic-only or --all-scanners. |
| For DAST and infrastructure scans: the targets allowed for the team | Teams page, Scanning tab, Web targets and Infrastructure targets. Check whether targets are allowed tests a list of them. |

## Setup

The pipeline signs in with the token of a **service account**, not a person's password. A service account belongs to the team and keeps working when people leave.

1. **Create a service account**

   Teams in the sidebar, then the People tab, Automation and API tokens card. Under New service account type a name such as ci, set Role to Operator (the default, Reader, cannot start scans) and select Create. Check: the account appears in the card with the role Operator and the badge Active.
2. **Note the team name**

   The line at the top of the same card reads: This team's name in pipelines and API addresses (SCANSUITE_TEAM), followed by the name in bold. Use that name, not the team's display name. The first team of an installation is default, whatever it was renamed to.
3. **Issue a token**

   Select New token on the service account. Keep the CI pipeline preset, set Valid for (days) and select Create token. Check: the Your new token dialog opens with the token, the team name and two test commands.

> **A person does this step:** Copy the token from the Your new token dialog and keep it only in your CI system's secret store. It is shown once; if you lose it, revoke it on the card and issue another. Never paste the token into a chat with an assistant, a ticket or the repository.

The **Your new token** dialog shows, in order: the token with a **Copy** button; the row **This team's name in pipelines and API addresses (SCANSUITE_TEAM)** with its own Copy button; and **Test the connection** with two ready-to-run commands, **With the pipeline client (Docker)** and **With curl**, that already contain this server's address, the team and the token. Both commands contain the token, so running them as they are leaves it in your shell history. Prefer the test below, which reads the token without echoing it.

| Field | What it does | Default | When to change it |
|---|---|---|---|
| Token name | A label shown in the card and in the audit log. | empty (required) | Name it after the pipeline that uses it, such as gitlab-backend, so it can be revoked alone. |
| Valid for (days) | How long the token works. | 30 | 1 to 90. The job log warns from 14 days before it expires. |
| What can it do? | CI pipeline, Reporting, or Custom (permissions picked one by one). | CI pipeline | Reporting for read-only dashboards. Custom when a script must also triage findings or read assets. |

A token can never do more than its service account's role allows. A service account is Reader or Operator.

### Test the connection

This test proves that the server answers, takes the token for this team, and that the token can run pipeline scans. It starts nothing.

> **A person does this step:** Run this test in your own terminal: step 2 asks you to paste the token.
>
> An agent stops here and asks you to run it, and reads only the output you choose to share (it contains no token).

1. **Set the server and team**

   ```bash
   export SCANSUITE_URL=<YOUR_SCANSUITE_URL> SCANSUITE_TEAM=<YOUR_TEAM>
   ```
2. **Read the token without echoing it (paste it, then press Enter)**

   ```bash
   read -rs SCANSUITE_TOKEN && export SCANSUITE_TOKEN
   ```
3. **Run the check**

   ```bash
   docker run --rm -e SCANSUITE_URL -e SCANSUITE_TEAM -e SCANSUITE_TOKEN appsec4u/scansuite-ci:1 --check
   ```

*Expected output*

```
[scansuite] Connected to https://scansuite.example.com: the server accepts the API token for team default (role operator)
[scansuite] The token may: credential.read, finding.read, product.read, product.write, report.read, scan.cancel, scan.execute, scan.read
[scansuite] The token expires in 30 day(s) (2026-11-18T09:12:44Z)
[scansuite] The token can run pipeline scans
```

Without Docker, run `python3 scansuite-ci.py --check` with the client from [Getting the client](https://scansuite.eu/documentation/reference/cicd#client). A token without the permissions a pipeline needs prints `WARNING: The token cannot run pipeline scans: it lacks …` instead of the last line; issue a new token with the CI pipeline preset. A refused token ends with exit code 3 and the reason:

| The message says | Fix |
|---|---|
| the API token has expired | Issue a new token. |
| the API token was revoked | Issue a new token. |
| the token's service account is deactivated | Activate it on the Teams page, People tab. |
| the token's team is not active on this server | A system administrator activates the team in System Settings. |
| the API token belongs to team X, not Y | Set SCANSUITE_TEAM to the name the message gives: the team name in addresses, not the display name. |
| this server does not know the API token | The token was copied incompletely, or SCANSUITE_URL points at another ScanSuite server. |
| the token did not reach ScanSuite | A proxy or gateway between the runner and the server removes the Authorization header. |
| Set SCANSUITE_TOKEN (masked CI variable) … | The variable is empty or is not a ScanSuite token (they start with sst_). |

### Add the variables to your CI system

| Placeholder | Variable | Where the value comes from | How to store it |
|---|---|---|---|
| <YOUR_SCANSUITE_URL> | SCANSUITE_URL | The address you open ScanSuite at, for example https://scansuite.example.com. | Plain variable |
| <YOUR_TEAM> | SCANSUITE_TEAM | The team name from setup step 2 (also in the new-token dialog). | Plain variable |
| <YOUR_PRODUCT> | SCANSUITE_PRODUCT | The product's exact name on the Products page. Or SCANSUITE_PRODUCT_ID with its number. | Plain variable |
| (the token) | SCANSUITE_TOKEN | The token from setup step 3. | Secret: masked, never printed |

> **A person does this step:** Add SCANSUITE_TOKEN to the CI system as a masked or secret variable yourself.
>
> An agent that sets up the pipeline lists the variables and stops here; it never asks for the token or writes it into a file in the repository.

> **Agent prompt: Let an agent add the pipeline job**
>
> ```text
> Add a ScanSuite scan to this repository's CI pipeline by following https://scansuite.eu/documentation/reference/cicd. Work out which CI system the repository uses, use the image appsec4u/scansuite-ci:1, and use the merge-request and default-branch bundles the page recommends. Do not put a token anywhere in the repository and do not ask me for it: tell me which secret and variables to add, and stop at every step marked "A person does this step".
> ```

### Token permissions

Before anything starts, the client checks that the token has every permission the run needs and names any that is missing (exit code 3).

| Permission | Needed for |
|---|---|
| product.read · See products | Always: finds the product. |
| scan.execute · Start scans | Always: starts the scan. |
| scan.read · See scans and results | Always: follows the scan. |
| finding.read · See findings | Always: reads the findings for the gate. |
| credential.read · See discovered credentials | The secrets gate (--fail-on-secrets new or all, the default). Not needed with --fail-on-secrets none. |
| scan.cancel · Stop scans | Cancelling the scan when the job is cancelled or --cancel-on-timeout fires. |
| report.read · Download reports | --report-zip. |
| product.write · Create and edit products | --create-product. |

| Preset | Permissions | Use it for |
|---|---|---|
| CI pipeline | All eight permissions above. | Pipelines and scripts that scan. |
| Reporting | product.read, scan.read, finding.read, report.read, asset.read. | Dashboards and exports that only read. |
| Custom | Picked one by one. | Scripts that also triage findings (Change findings and write proof-of-concept exploits) or read assets (See assets). |

## Getting the client

Download it from your ScanSuite server, which serves the version that matches it, and check its checksum:

```bash
curl -fsSO <YOUR_SCANSUITE_URL>/ci/scansuite-ci.py
curl -fsS <YOUR_SCANSUITE_URL>/ci/scansuite-ci.py.sha256 | sha256sum -c -
python3 scansuite-ci.py --version
```

On a server that still has its first, self-signed certificate, `curl` stops with `SSL certificate problem`: add `-k` to these two lines until a real certificate is in place ([SSL/TLS Setup](https://scansuite.eu/documentation/installation/ssl-tls-setup)).

Expected: `scansuite-ci.py: OK`, then `scansuite-ci 1.4.5` (or the version your server publishes). Or use the container image `appsec4u/scansuite-ci:1` (the latest 1.x), where the client runs as `scansuite-ci` and Git is installed; clear its entrypoint when the CI system runs its own shell in the container. At start-up the client says when the server offers a newer version.

## Pipelines by CI system

Each platform runs the same client with the same options. The client reads the repository, branch, commit and merge or pull request target from GitLab CI, GitHub Actions, Jenkins, Azure DevOps and Bitbucket Pipelines.

### GitLab CI

Include the template your ScanSuite server publishes. It adds `scansuite-merge-request` (`quick-classic` on the changed files of every merge request) and `scansuite-default-branch` (`standard-ai` on the default branch), and a hidden `.scansuite` job to extend for your own jobs.

*.gitlab-ci.yml*

```
include:
  - remote: '<YOUR_SCANSUITE_URL>/ci/scansuite.gitlab-ci.yml'
```

Variables, recipes (DAST, images, nightly, release, monorepo) and GitLab-specific troubleshooting are in [GitLab CI step by step](https://scansuite.eu/documentation/reference/gitlab-ci).

### GitHub Actions

Copy the `github/` folder of the [scansuite-ci repository](https://github.com/cepxeo/scansuite-ci) into your repository as `.github/actions/scansuite`, and add the token as the repository secret `SCANSUITE_TOKEN`. Findings then appear under Security → Code scanning.

*.github/workflows/scansuite.yml*

```
name: ScanSuite
on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write          # SARIF upload

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }  # --changed-only needs the history
      - uses: ./.github/actions/scansuite
        with:
          url: <YOUR_SCANSUITE_URL>
          team: <YOUR_TEAM>
          product: <YOUR_PRODUCT>
          token: ${{ secrets.SCANSUITE_TOKEN }}
          profile: ${{ github.event_name == 'pull_request' && 'quick-classic' || 'standard-ai' }}
          changed-only: ${{ github.event_name == 'pull_request' }}
      - uses: github/codeql-action/upload-sarif@v3
        if: always()
        with: { sarif_file: scansuite.sarif }
```

| Input | Default | What it does |
|---|---|---|
| url | required | SCANSUITE_URL. |
| team | required | SCANSUITE_TEAM. |
| token | required | The token; pass a secret. |
| product / product-id | empty | The product's exact name, or its number. |
| profile | standard-ai | The bundle (see Static analysis). |
| changed-only | false | true scans only the files the pull request changes. |
| base | the pull request base | Branch or commit changed-only compares with. |
| fail-on-severity | high | Lowest severity that fails the job. |
| max | empty | Allowed findings per severity, such as high=0,medium=10. |
| block-class | empty | Classes that fail the job at any severity. |
| min-confidence | any | reachable: only findings confirmed reachable fail the job. |
| strict-tls | false | true stops on a self-signed or unknown-CA certificate. |
| ca-bundle | empty | Path in the workspace to your CA certificate. |
| sarif | scansuite.sarif | SARIF report path, for upload-sarif. |
| summary-json | scansuite.json | JSON summary path. |

The action has no input for `--fail-on-secrets`, `--scan-type`, targets, `--create-product` or other client options. For those, and for DAST and infrastructure scans, run the image as a container job and pass any option:

*.github/workflows/scansuite.yml*

```
jobs:
  scan:
    runs-on: ubuntu-latest
    container: appsec4u/scansuite-ci:1
    env:
      SCANSUITE_URL: <YOUR_SCANSUITE_URL>
      SCANSUITE_TEAM: <YOUR_TEAM>
      SCANSUITE_PRODUCT: <YOUR_PRODUCT>
      SCANSUITE_TOKEN: ${{ secrets.SCANSUITE_TOKEN }}
    steps:
      - uses: actions/checkout@v4
        with: { fetch-depth: 0 }
      - run: scansuite-ci --profile quick-classic --changed-only --sarif scansuite.sarif
```

### Jenkins

Add the `jenkins` folder of the scansuite-ci repository as a Global Pipeline Library (Manage Jenkins → System → Global Pipeline Libraries, for example named `scansuite`), and store the token as a **Secret text** credential. The step needs the Docker Pipeline plugin. It publishes the JUnit report and archives the JSON summary.

*Jenkinsfile*

```
@Library('scansuite') _
pipeline {
  agent any
  stages {
    stage('ScanSuite') {
      steps {
        scansuiteScan(
          url: '<YOUR_SCANSUITE_URL>', team: '<YOUR_TEAM>', product: '<YOUR_PRODUCT>',
          credentialsId: 'scansuite-ci-token',
          profile: env.CHANGE_ID ? 'quick-classic' : 'standard-ai',
          changedOnly: env.CHANGE_ID != null,
          unstableOnGate: true)
      }
    }
  }
}
```

| Option | Default | What it does |
|---|---|---|
| url, team | required | SCANSUITE_URL and SCANSUITE_TEAM. |
| product / productId | empty | The product's exact name, or its number. |
| credentialsId | scansuite-ci-token | The Secret text credential that holds the token. |
| profile | standard-ai | The bundle. |
| changedOnly | false | Scan only the files the change request changes. |
| base | the change request target | Branch or commit changedOnly compares with. |
| failOnSeverity | high | Lowest severity that fails the build. |
| max | empty | Allowed findings per severity. |
| blockClass | empty | Classes that fail the build at any severity. |
| minConfidence | any | reachable: only findings confirmed reachable fail the build. |
| strictTls | false | Stop on a self-signed or unknown-CA certificate. |
| caBundle | empty | Path to your CA certificate. |
| unstableOnGate | false | true marks the build UNSTABLE instead of failed when the gate fails (exit code 1). Other codes still fail it. |
| image | appsec4u/scansuite-ci:1 | The client image. |
| args | empty | More client options, such as --fail-on-secrets none. |

Without the library, use a Docker agent with the image, clear its entrypoint (`args '--entrypoint='`), map the credential with `SCANSUITE_TOKEN = credentials('scansuite-ci-token')`, and run `scansuite-ci` in an `sh` step.

### Azure DevOps

Add `SCANSUITE_TOKEN` as a secret pipeline variable. Azure DevOps does not pass secrets to scripts on its own, so map it under `env`. The Microsoft-hosted Ubuntu agents have Python 3 and Git.

*azure-pipelines.yml*

```
trigger: [main]
pr: [main]
pool: { vmImage: ubuntu-latest }

variables:
  SCANSUITE_URL: <YOUR_SCANSUITE_URL>
  SCANSUITE_TEAM: <YOUR_TEAM>
  SCANSUITE_PRODUCT: <YOUR_PRODUCT>

steps:
  - checkout: self
    fetchDepth: 0
  - script: |
      set -e
      curl -fsSO "$SCANSUITE_URL/ci/scansuite-ci.py"
      curl -fsS "$SCANSUITE_URL/ci/scansuite-ci.py.sha256" | sha256sum -c -
      if [ "$BUILD_REASON" = "PullRequest" ]; then ARGS="--profile quick-classic --changed-only"; else ARGS="--profile standard-ai"; fi
      python3 scansuite-ci.py $ARGS --junit scansuite-junit.xml --sarif scansuite.sarif
    displayName: ScanSuite
    env:
      SCANSUITE_TOKEN: $(SCANSUITE_TOKEN)
  - task: PublishTestResults@2
    condition: always()
    inputs: { testResultsFormat: JUnit, testResultsFiles: scansuite-junit.xml, testRunTitle: ScanSuite }
```

Pull request builds (branch policy → build validation) report the target branch, and `--changed-only` compares with it. A server with a self-signed certificate also needs `curl -k` for the download.

### Bitbucket Pipelines

Add `SCANSUITE_TOKEN` as a secured repository variable and the others as plain ones. Test results under `test-results/` are picked up on their own.

*bitbucket-pipelines.yml*

```
image: python:3.12
clone:
  depth: full

pipelines:
  pull-requests:
    '**':
      - step:
          name: ScanSuite (changed files)
          script:
            - curl -fsSO "$SCANSUITE_URL/ci/scansuite-ci.py"
            - curl -fsS "$SCANSUITE_URL/ci/scansuite-ci.py.sha256" | sha256sum -c -
            - python3 scansuite-ci.py --profile quick-classic --changed-only --junit test-results/scansuite.xml
  branches:
    main:
      - step:
          name: ScanSuite
          script:
            - curl -fsSO "$SCANSUITE_URL/ci/scansuite-ci.py"
            - curl -fsS "$SCANSUITE_URL/ci/scansuite-ci.py.sha256" | sha256sum -c -
            - python3 scansuite-ci.py --profile standard-ai --junit test-results/scansuite.xml
```

### Any other CI system, a script or a laptop

Elsewhere (CircleCI, TeamCity, Drone, a cron job) give the client what it cannot detect: the base for `--changed-only`, and a unique `--idempotency-key` per run so a retried step does not pick up the previous run's scan.

```bash
docker run --rm -v "$PWD:/src" -w /src \
    -e SCANSUITE_URL -e SCANSUITE_TEAM -e SCANSUITE_TOKEN -e SCANSUITE_PRODUCT \
    appsec4u/scansuite-ci:1 \
    --profile quick-classic --changed-only --base origin/main --idempotency-key "build-$BUILD_NUMBER" \
    --junit scansuite-junit.xml
```

On a laptop, keep the token in a file readable only by you and pass `--token-file ~/.scansuite-token` instead of the variable. Without Git on the machine, the client archives the directory tree with default exclusions, and `--changed-only` falls back to a full scan.

## Check the first run

The job log starts with the team and product it reports to and the scan's reference, and ends with the verdict:

*The last lines of a run*

```
[scansuite] Quality gate passed
```

or `[scansuite] Quality gate FAILED: N blocking finding(s), M blocking secret(s)`, after one `BLOCKING` line per finding or secret that failed it, followed by `[scansuite] Exit code 1: the quality gate failed …`. The scan also appears in ScanSuite under Scan History. If the job ends with another exit code, see [Exit codes](https://scansuite.eu/documentation/reference/cicd#exit-codes).

## Products

Every scan reports into one product of your team: give its exact name with `--product-name` (`SCANSUITE_PRODUCT`) or its number with `--product-id` (`SCANSUITE_PRODUCT_ID`), not both. A name that matches no product stops the run with exit code 3.

With `--create-product` (`SCANSUITE_CREATE_PRODUCT=1`) the first run creates the product and logs `Created product …`; later runs find it. The CI pipeline preset allows it. When the team works with DefectDojo, the product is created there as well; if DefectDojo cannot be reached, it is still created in ScanSuite and the run warns. For one product per service of a monorepo, give each job its folder and product: `--source-dir services/payments --product-name payments --create-product`.

## What to run, and when

| Trigger | Scan |
|---|---|
| Merge or pull request | --profile quick-ai --changed-only, or quick-classic --changed-only without AI: the changed files, in minutes. |
| Merge to the default branch | --profile standard-ai, or standard-classic without AI. |
| Nightly or weekly | --profile full-ai, or full-classic without AI. Use the CI system's own schedule. |
| After a deployment to staging | --scan-type dast against the deployed application. |
| Release, or a change to the hosts | --scan-type infra against the servers or the image the release runs on. |

## Static analysis

Six bundles, three without AI and three with it. `--profile` or `SCANSUITE_PROFILE` picks one; `standard-ai` is the default.

| --profile | Scanners | Use it for |
|---|---|---|
| quick-classic | Rule-based Semgrep and secrets. Minutes. | Merge requests, without AI. |
| standard-classic | Semgrep with the full local ruleset, secrets and dependency checks. | The default branch, without AI. |
| full-classic | Every classic scanner: Semgrep (full, quick and the team's own rules), the native engine of each language in the repository, KICS for IaC, secrets and dependency checks. | Nightly and release, without AI. |
| quick-ai | AI Native SAST without reachability or Git history, and secrets with AI verification. | Merge requests, with AI. |
| standard-ai (default) | AI Native SAST with reachability, secrets with AI verification, and dependency checks with reachability. | The default branch. |
| full-ai | standard-ai plus security architecture analysis, the cross-file hunt and Git history. The slowest and the costliest in AI. | Nightly and release. |

The AI bundles report only the secrets AI verification confirmed; when verification could not run for some, the client warns with their number. `full-ai` analyses Git history, so fetch the full history in the pipeline (GitLab `GIT_DEPTH: 0`, GitHub `fetch-depth: 0`). The 1.x names still work: `quick` is `quick-classic`, `standard` is `standard-ai`, `deep` is `full-ai`.

### Scanners and options

`--scanners` replaces a bundle's scanners, `--add-scanners` adds to them, and `--options` replaces its options; `--no-ai-verification` drops the AI options. An option is sent only with its scanner. `--list-scanners` prints what your server offers, marks what can scan selected files, what runs alone and what is `NOT AVAILABLE` for your team, with the reason; choosing an unavailable scanner stops the run with exit code 3 before a scan starts.

| Scanner ID | In the console | Options and notes |
|---|---|---|
| mlsast | AI Native SAST | Options mlsast_reachability (Verify findings reachability), mlsast_security_architecture (Analyze security architecture), mlsast_boundary_hunt (Cross-file hunt), mlsast_git_history (Analyze Git history). Needs an AI provider. |
| secrets | Secrets Detection | Option secrets_ai (Verify secrets with AI). |
| dep_checks | Dependency Check (SCA) | Option dep_checks_reachability (Verify findings reachability). dep_checks_ai is still accepted and does nothing. |
| sast_quick | Quick (Semgrep) | Can scan selected files. |
| sast_full | Full (Semgrep with the full ruleset, plus the main language's engine) | Can scan selected files. |
| sast_custom | Custom Rules (the team's Semgrep rules) | Can scan selected files. |
| iacs_kics | IaCS Analysis (KICS) | Can scan selected files. |
| snyk | Snyk Scan (Online) | Needs a Snyk key on the Teams page, Integrations tab. |
| python, java, csharp, cpp, php, javascript, typescript, ruby, go, kotlin, swift | The native engine of one language | Usually chosen through --lang or full-classic. |

`--lang` names the main language instead of the bundle's (`multilanguage` for all detected languages), and `--git-history` adds `mlsast_git_history` (the archive then includes `.git`).

### Only what a merge request changed

`--changed-only` (`SCANSUITE_CHANGED_ONLY=1`) scans the files changed since the merge base with the target branch. Merge and pull request pipelines report the target branch themselves; elsewhere give it with `--base`. When a shallow clone lacks the target branch, the client fetches it. Only scanners that can scan selected files run (`mlsast`, `sast_quick`, `sast_full`, `sast_custom`, `iacs_kics`): secrets and dependency checks need the whole repository and are skipped, with a warning when a dependency manifest changed. When nothing changed the job passes; when no base can be found a full scan runs, with a warning. It cannot be combined with `--mode` or `--scope`.

### Upload or clone

By default (`--source auto`) the runner uploads its checkout as an archive: only the files Git tracks, from the CI workspace or `--source-dir`, minus `--exclude` patterns. The server scans exactly the commit the pipeline runs on, private repositories need no extra access, and findings link to the files in your repository at that commit.

With `--source git` (chosen automatically when `--git-url` or `--mode incremental` is given) the server clones the repository instead, at the branch head when the scan starts. Private repositories are cloned over SSH with the team's key: Teams page, Scanning tab, **Team SSH key**. Pipeline scans always use the team key, never a person's own key (see [Git repository authentication](https://scansuite.eu/documentation/usage/static-code-analysis/git-repository-authentication)). The URL must not contain credentials. `--mode incremental` scans only what changed since the previous scan of the branch and passes when nothing did.

`--mode custom-scope --scope PATTERN` (repeatable, or `--scope-file FILE`) scans only the files the patterns match; scanners that cannot be limited to files are left out, with a note.

## Web application and infrastructure scans

Give the targets with `--target` (repeatable, or separated by commas, spaces or `;`) or `SCANSUITE_TARGETS`. The team's target policy must allow each one, unless Allow is set to **Any target**; a refused target stops the run with exit code 3 before anything starts, and the message names it. The rules for targets are on [Web App Scanning](https://scansuite.eu/documentation/usage/web-app-scanning#targets). The secrets gate does not apply to these scans.

*A DAST scan of a staging deployment*

```
scansuite-ci --scan-type dast --target https://staging.example.com \
  --profile standard --header "Authorization: Bearer $STAGING_TOKEN" --junit scansuite-dast.xml
```

| --profile with --scan-type dast | Scanners |
|---|---|
| quick | dast_quick, tech_discovery |
| standard (default) | dast_quick, nuclei, tech_discovery, dirbust |
| deep | dast_balanced, nuclei, tech_discovery, dirbust, mldast |

| DAST scanner ID | In the console | Notes |
|---|---|---|
| dast_quick | Quick |  |
| dast_balanced | Balanced |  |
| zap_full | Deep Scan | Intrusive. |
| nuclei | Nuclei |  |
| nuclei_custom | Custom Rules (the team's Nuclei templates) |  |
| api_scan | API Scan |  |
| acunetix | Acunetix | Needs Acunetix on the Teams page, Scanning tab, Scanner engines. |
| nessus_web | Nessus Web | Needs Nessus in System Settings. |
| tech_discovery | Tech Discovery |  |
| dirbust | Hidden Paths |  |
| crawl | Secrets (crawls pages for leaked keys) |  |
| mldast | AI DAST | Needs an AI provider. |
| ai_pentest | Autonomous AI assessment (AI Pentest) | Needs an AI provider. Guide it with --instructions. |

To scan behind a login, pass `--header "Name: value"` (several separated by `|`) or `--cookie "key=value"` (several separated by `;`). Keep such values in CI secrets, not in the job file.

Infrastructure scans have no profiles: name the scanners with `--scanners`.

| Infrastructure scanner ID | In the console | Notes |
|---|---|---|
| openvas | OpenVAS | Needs OpenVAS on the Teams page, Scanning tab, Scanner engines. |
| nessus | Nessus | Needs Nessus in System Settings. |
| hosts_prescan | Discovery pre-scan | Finds live hosts before the vulnerability scanners. |
| nuclei | Nuclei |  |
| nuclei_custom | Custom Rules |  |
| hosts_bruter | Bruteforcer | Tries weak and leaked credentials on known services. |
| ai_pentest | Autonomous AI assessment (AI Pentest) | Needs an AI provider. Guide it with --instructions. |
| hosts_scan | Network Discovery | Runs alone. |
| hosts_patching | Local Patching Checks | Runs alone. Uses the team's server account (Teams page, Scanning tab). |
| osint | Domains OSINT | Runs alone. Option osint_brute (Bruteforce hostnames). |
| docker_image_scan | Docker Image Scan | Runs alone. The target is an image reference. |

*Infrastructure scans*

```
# Two hosts, the top 1000 TCP and UDP ports, including hosts that do not answer ping
scansuite-ci --scan-type infra --scanners openvas,nuclei \
  --target 10.0.4.10,10.0.4.11 --ports "Top 1000 TCP/UDP" --no-ping --fail-on-severity critical

# A container image
scansuite-ci --scan-type infra --scanners docker_image_scan \
  --target registry.example.com/shop/api:1.4.2
```

| --ports or SCANSUITE_PORTS | Ports scanned |
|---|---|
| All TCP | Every TCP port. The default. |
| Top 1000 TCP/UDP | The 1000 most common TCP and UDP ports. |
| All TCP/UDP (slowest) | Every TCP and UDP port. |
| PCI External Scan | The port set of a PCI external scan. |
| PCI Internal Scan | The port set of a PCI internal scan. |
| A port list, such as 22,80,443 | Exactly these ports, separated by commas, semicolons, spaces or \|. |

In GitLab, set `SCANSUITE_PORTS` rather than passing `--ports` in `SCANSUITE_EXTRA_ARGS`: that variable is split on spaces, so `Top 1000 TCP/UDP` would break apart.

## When the build fails

Findings from this scan that are Open or In Progress count. Findings AI verification marked unreachable do not, unless you add `--include-unreachable`. What the classic scanner tools report counts only when the team stores it: Teams page, Scanning tab, **Scanner findings in Vulnerabilities** (on by default).

| Option | What fails the build |
|---|---|
| --fail-on-severity high | Findings of this severity or above fail the build: critical, high (default), medium, low, info, or none to turn it off. |
| --max high=0,medium=10 | How many findings of each severity are allowed. A severity named here uses its limit instead of --fail-on-severity. |
| --block-class sql_injection,command_injection | Findings of these classes fail the build at any severity. The class names are the ones findings carry in ScanSuite and in the JSON summary. |
| --min-confidence reachable | Only findings confirmed reachable, or with a known exploit, fail the build. The default any also counts findings that were not assessed. |
| --fail-on-secrets new | Secrets this run added to the product that are verified, or were never judged, fail the build (the default). all counts every such secret of the product; none turns it off. Static analysis only. Needs credential.read. |
| --include-unreachable | Findings AI verification marked unreachable count too. |

- A target that does not answer is a warning in the scan log, not a failure: the scan ends Finished and the gate decides on what the other targets returned.
- What two scanners of one scan report for the same issue on the same service is one finding: it blocks once, at the higher severity.
- If one of the scanners did not complete, the build fails with exit code 2 even when nothing else blocks it; findings from the scanners that did complete still fail it with exit code 1.
- The first scan of a product sees every secret as new, so it fails on secrets already committed. Triage them in ScanSuite, or start with `--fail-on-secrets none`.

## Exit codes

| Code | Meaning | What to do |
|---|---|---|
| 0 | Passed, or there was nothing to scan. | — |
| 1 | The gate failed: findings or secrets over the limits. | Read the BLOCKING lines; fix, or triage in ScanSuite. |
| 2 | The scan failed, was cancelled, or was refused by the server (for example the team's scan limit), or one of its scanners did not complete. | Open the scan in ScanSuite; the log names the scanner. |
| 3 | A configuration, token, permission or target error, or a TLS certificate that failed verification. | The line before it says what to change. |
| 4 | Timed out (--timeout, 7200 seconds by default). | Raise --timeout, or the CI job's own time limit. |
| 5 | ScanSuite was unreachable or answered with an error. | Check the runner's network path to SCANSUITE_URL. |

A failed run ends with a line naming its exit code and what it means, for example `[scansuite] Exit code 3: configuration error: option, token, permission, target or certificate`. Some CI systems, GitLab among them, show every failed job as exit code 1; that line tells you which failure it was. `--soft-fail` turns codes 2, 4 and 5 into 0 with a warning, so a ScanSuite outage does not block a release; codes 1 and 3 still fail the job.

## Reports

| Option | Output |
|---|---|
| --junit FILE | JUnit XML, shown as test failures in GitLab, Jenkins and Azure DevOps. |
| --sarif FILE | SARIF 2.1.0, for GitHub code scanning, Azure DevOps and IDEs. |
| --summary-json FILE | The scan, the gate result, the findings and the secrets (never their values). |
| --report-zip FILE | The full report archive. Needs report.read. |

The job log lists every blocking finding and secret with the rule that blocked it; `--max-print N` limits how many (50 by default).

## Waiting and cancelling

The client follows the scan until it ends, checking every 20 seconds for up to two hours. When the CI job is cancelled, the client cancels its scan (the token needs **Stop scans**, which the CI pipeline preset includes). A request repeated after a network error picks up the scan already started instead of starting another, a busy server is retried, and separate CI runs never share a scan.

## TLS certificates

With a self-signed certificate, or one from a CA the runner does not know, the client warns and goes on without verifying it. Give the CA certificate with `--ca-bundle` to verify it, or add `--strict-tls` to stop with exit code 3 instead. A CA-signed certificate for another host, or an expired one, always stops the run.

## All options

Every option the client takes. Boolean variables accept `1`, `true` or `yes`. A variable is read only when the option is not given.

### Server and product

| Option | Variable | Default | What it does | When to change it |
|---|---|---|---|---|
| --url | SCANSUITE_URL | — | The ScanSuite address. | Always set. |
| --team | SCANSUITE_TEAM | — | The team name in addresses, from the Automation and API tokens card. | Always set. |
| --token-file FILE | SCANSUITE_TOKEN | the variable | Reads the token from a file instead of the variable. | On a laptop or a host where the token is a file. |
| --product-name | SCANSUITE_PRODUCT | — | The product's exact name. Cannot be combined with --product-id. | Always set one of the two. |
| --product-id | SCANSUITE_PRODUCT_ID | — | The product's number. | When names change. |
| --create-product | SCANSUITE_CREATE_PRODUCT | off | Creates the --product-name product when the team has none (and in DefectDojo when the team uses it). Needs product.write. | New repositories and monorepo services. |
| --ca-bundle FILE | SCANSUITE_CA_BUNDLE | — | Verifies the server's certificate against your CA. | A private CA. |
| --strict-tls | SCANSUITE_STRICT_TLS | off | Stops (exit 3) on a certificate it cannot verify, instead of warning. | When an unverified connection is not acceptable. |
| --insecure | — | off | Turns certificate verification off from the start. | Testing only. |
| --check | — | — | Tests the connection and the token, prints what the token may do, and exits. Starts nothing. | After issuing a token, and when a pipeline starts failing with exit code 3. |
| --list-scanners | — | — | Prints the scanners and options the server offers for each scan type, what is not available for the team and why, and the bundles; then exits. | Before choosing --scanners or --options. |
| --no-version-check | — | off | Does not ask the server whether a newer client exists. | Air-gapped runners with a pinned client. |
| --version | — | — | Prints the client version and exits. | — |

### What to scan

| Option | Variable | Default | What it does | When to change it |
|---|---|---|---|---|
| --scan-type | SCANSUITE_SCAN_TYPE | sast | sast (source code), dast (web applications) or infra (hosts, networks, container images). | DAST and infrastructure jobs. |
| --target | SCANSUITE_TARGETS | — | DAST: URLs. Infra: hosts, addresses, ranges or image references. Refused for sast. | Always for dast and infra. |
| --header | — | — | DAST: request headers, "Name: value", several separated by \|. | Scanning behind a login. |
| --cookie | — | — | DAST: cookies, "key=value", several separated by ;. | Scanning behind a login. |
| --ports | SCANSUITE_PORTS | All TCP | Infra: the ports (see the table above). | Faster or PCI scans. |
| --no-ping | — | off (ping on) | Infra: also scans hosts that do not answer ping. | Most Internet-facing hosts. |
| --instructions TEXT | — | — | DAST and infra: guidance for the AI pentest scanner (ai_pentest), such as what to focus on and what is off limits. | With ai_pentest. |

### Static analysis: source and selection

| Option | Variable | Default | What it does | When to change it |
|---|---|---|---|---|
| --profile | SCANSUITE_PROFILE | standard-ai (dast: standard) | The bundle. Not used for infra. | Per trigger, see What to run. |
| --scanners | — | the bundle's | Scanner IDs, replacing the bundle's. Required for infra. | One scanner, or infra scans. |
| --add-scanners | — | — | Scanner IDs added to the bundle's. | Adding snyk or iacs_kics. |
| --options | — | the bundle's | Option IDs, replacing the bundle's. --options "" turns them all off. | Choosing AI features one by one. |
| --no-ai-verification | — | off | Drops the AI verification and reachability options. | Saving AI cost. |
| --lang | — | the bundle's, else multilanguage | The main language. | A repository with one language the bundle does not pick. |
| --git-history | — | off | Adds Git history analysis to AI Native SAST; the archive includes .git. | Hunting secrets and risky changes in history (fetch the full history). |
| --changed-only | SCANSUITE_CHANGED_ONLY | off | Scans only the files changed against --base. | Merge and pull requests. |
| --base | SCANSUITE_BASE | the merge or pull request target | Branch or commit --changed-only compares with. | Outside merge request pipelines. |
| --mode | — | once | once, incremental (Git only: what changed since the last scan of the branch) or custom-scope. | Large repositories cloned by the server. |
| --scope / --scope-file | — | — | Patterns for --mode custom-scope; repeatable. | Scanning one folder. |
| --source | SCANSUITE_SOURCE | auto | zip uploads the checkout; git lets the server clone; auto picks git when --git-url or --mode incremental is given. | Large repositories. |
| --source-dir | — | the CI workspace | The directory to archive. | Monorepo services. |
| --exclude GLOB | — | .git, node_modules and other build folders when not in Git | Leaves paths out of the archive; repeatable. | Generated or vendored code. |
| --no-git-ls-files | — | off | Archives the directory tree instead of the files Git tracks. | Scanning untracked or generated files. |
| --max-archive-mb | — | 1024 | Refuses a larger archive (exit 3). | Very large repositories; or use --source git. |
| --git-url | SCANSUITE_GIT_URL | the CI repository | The URL the server clones (HTTPS without credentials, or SSH). | With --source git. |
| --branch | SCANSUITE_BRANCH | the pipeline branch | The branch the server clones. | Scanning another branch. |
| --repository-url | — | from the CI system | The browsable repository address used in finding links. | When the CI system reports none. |

`--changed-only`, `--mode`, `--scope` and `--git-url` are for `--scan-type sast` only, and `--changed-only` cannot be combined with `--mode` or `--scope` (exit 3).

### Waiting

| Option | Variable | Default | What it does | When to change it |
|---|---|---|---|---|
| --timeout | SCANSUITE_TIMEOUT | 7200 | Seconds to wait before exit code 4. | Long full-ai scans; also raise the CI job's own limit. |
| --poll-interval | — | 20 | Seconds between status checks. | Rarely. |
| --cancel-on-timeout | — | off | Cancels the scan when --timeout expires. | When a timed-out scan should not keep running. |
| --no-cancel-on-interrupt | — | off | Leaves the scan running when the CI job is cancelled. | When the scan should finish anyway. |
| --no-wait | — | off | Starts the scan and exits 0 without gating. | Fire-and-forget scans after a merge. |
| --idempotency-key | SCANSUITE_IDEMPOTENCY_KEY | derived from the CI job | A retried start with the same key returns the same scan. | Unknown CI systems, or Jenkins jobs recreated with the same build numbers. |

### Quality gate

| Option | Variable | Default | What it does | When to change it |
|---|---|---|---|---|
| --fail-on-severity | SCANSUITE_FAIL_ON_SEVERITY | high | Lowest blocking severity: critical, high, medium, low, info, none. | Stricter release gates, or none while rolling out. |
| --max | SCANSUITE_MAX | — | Allowed findings per severity, such as high=0,medium=10. | A budget instead of zero tolerance. |
| --block-class | SCANSUITE_BLOCK_CLASS | — | Classes that block at any severity. | Classes you never accept, such as sql_injection. |
| --min-confidence | SCANSUITE_MIN_CONFIDENCE | any | reachable: only findings confirmed reachable or with a known exploit block. | Release gates on what attackers can reach. |
| --include-unreachable | — (pass it as an option) | off | Findings marked unreachable block too. | When you do not trust the reachability verdict. |
| --fail-on-secrets | SCANSUITE_FAIL_ON_SECRETS | new | new, all or none. | all on releases; none for the first scan of an old repository. |
| --soft-fail | — | off | Codes 2, 4 and 5 become 0 with a warning. | When an outage must not block releases. |

### Output

| Option | Variable | Default | What it does | When to change it |
|---|---|---|---|---|
| --summary-json FILE | — | — | JSON with the scan, gate result, findings and secrets (no values). | Dashboards and scripts. |
| --junit FILE | — | — | JUnit XML report. | GitLab, Jenkins, Azure DevOps test views. |
| --sarif FILE | — | — | SARIF 2.1.0 report. | GitHub code scanning, Azure DevOps. |
| --report-zip FILE | — | — | Downloads the full report archive. Needs report.read. | Keeping the evidence with a release. |
| --print-logs-on-failure | — | off | Prints the end of the scan log when the scan fails. | Debugging exit code 2. |
| --max-print | — | 50 | How many blocking items the job log lists. | Very noisy first scans. |
