CI/CD integration

May 10, 2026 · View on GitHub

ptai ships with first-class CI/CD support: SARIF output, severity gates, deterministic exit codes, and PR comments. Drop one of the templates below into your repo and every push or pull request gets an authenticated security scan.

What --ci mode changes

  • Non-interactive: never prompts (set PENTEST_AI_AUP_ACCEPTED=1 so first-run AUP is auto-accepted).
  • Quieter output (no progress spinners; structured logs to stderr).
  • Exits non-zero when a finding meets --fail-on <severity>.
  • Always emits SARIF when --sarif <path> is set, even on failure.
  • Always writes JUnit XML when --junit <path> is set (great for test-result panels).
  • Caches tool installs to ~/.cache/ptai/ between jobs when the runner persists that path.

Common flags:

ptai start <target> \
  --ci \
  --fail-on high \
  --sarif pentest.sarif \
  --junit pentest.junit.xml \
  --auth-profile <profile>            # optional, for authenticated scans
  --scope ./pentest-scope.yml         # optional, restricts the engagement

Severity levels for --fail-on: critical, high, medium, low, info. high is the conventional gate for staging; critical for production. Set to none to never fail the build (useful for trend-only runs).


GitHub Actions

# .github/workflows/security.yml
name: Security scan
on:
  pull_request:
    branches: [main]
  schedule:
    - cron: '0 6 * * 1'     # Mondays at 06:00 UTC

jobs:
  ptai:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    permissions:
      contents: read
      pull-requests: write       # for the PR comment
      security-events: write     # for SARIF upload to Code Scanning
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: pip

      - run: pip install ptai

      - name: Run pentest-ai
        env:
          ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
          PENTEST_AI_AUP_ACCEPTED: '1'
        run: |
          ptai start ${{ vars.STAGING_URL }} \
            --ci \
            --fail-on high \
            --sarif pentest.sarif \
            --junit pentest.junit.xml

      - name: Upload SARIF to Code Scanning
        if: always()
        uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: pentest.sarif

      - name: Publish JUnit results
        if: always()
        uses: dorny/test-reporter@v1
        with:
          name: pentest-ai
          path: pentest.junit.xml
          reporter: java-junit

PR-comment summary: ptai writes the human-readable findings table to $GITHUB_STEP_SUMMARY automatically when running on GitHub Actions, so it appears in the PR's Checks tab. To also post as a PR comment, add the mshick/add-pr-comment action and read from pentest.summary.md:

      - uses: mshick/add-pr-comment@v2
        if: always()
        with:
          message-path: pentest.summary.md

Authenticated scans in CI

Store target credentials in GitHub Secrets and reference them in an auth profile:

      - name: Configure auth profile
        env:
          STAGING_PASSWORD: ${{ secrets.STAGING_APP_PASSWORD }}
        run: |
          ptai auth profile add staging \
            --login-url '${{ vars.STAGING_URL }}/login' \
            --username admin \
            --password-env STAGING_PASSWORD
      - run: ptai start ${{ vars.STAGING_URL }} --auth-profile staging --ci --fail-on high

The password is read from the env var at scan time. It never enters argv, the findings DB, the LLM context, or the SARIF output.


GitLab CI

# .gitlab-ci.yml
stages: [security]

pentest-ai:
  stage: security
  image: python:3.12-slim
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_PIPELINE_SOURCE == "schedule"
  variables:
    PENTEST_AI_AUP_ACCEPTED: '1'
  before_script:
    - apt-get update && apt-get install -y --no-install-recommends curl ca-certificates
    - pip install ptai
  script:
    - |
      ptai start "$STAGING_URL" \
        --ci \
        --fail-on high \
        --sarif pentest.sarif \
        --junit pentest.junit.xml
  artifacts:
    when: always
    expire_in: 30 days
    reports:
      junit: pentest.junit.xml
      sast: pentest.sarif      # GitLab renders SARIF in the Security & Compliance tab
    paths:
      - pentest.sarif
      - pentest.junit.xml
      - pentest.summary.md

Set the secrets in the project's CI/CD > Variables pane:

VariableValue
ANTHROPIC_API_KEYyour Claude key (masked, protected)
STAGING_URLhttps://staging.example.com
STAGING_APP_PASSWORDthe auth-profile password (masked)

Findings show up in Security & Compliance > Vulnerability Report via the SARIF upload, and the JUnit report integrates with the merge-request widget.


Jenkins (declarative pipeline)

// Jenkinsfile
pipeline {
  agent { docker { image 'python:3.12-slim' } }

  triggers {
    cron('H 6 * * 1')        // Mondays around 06:00
  }

  environment {
    ANTHROPIC_API_KEY        = credentials('anthropic-api-key')
    STAGING_APP_PASSWORD     = credentials('staging-app-password')
    STAGING_URL              = 'https://staging.example.com'
    PENTEST_AI_AUP_ACCEPTED  = '1'
  }

  stages {
    stage('Install') {
      steps { sh 'pip install ptai' }
    }

    stage('Pentest') {
      steps {
        sh '''
          ptai auth profile add staging \
            --login-url "${STAGING_URL}/login" \
            --username admin \
            --password-env STAGING_APP_PASSWORD

          ptai start "${STAGING_URL}" \
            --auth-profile staging \
            --ci \
            --fail-on high \
            --sarif pentest.sarif \
            --junit pentest.junit.xml
        '''
      }
    }
  }

  post {
    always {
      junit 'pentest.junit.xml'
      archiveArtifacts artifacts: 'pentest.sarif, pentest.summary.md', fingerprint: true
      // Optional: surface SARIF inside Jenkins via the Warnings Next Generation plugin
      recordIssues tool: sarif(pattern: 'pentest.sarif')
    }
  }
}

The recordIssues step needs the Warnings Next Generation plugin. Without it, drop that line. The SARIF is still archived as a build artifact.


Tips for any CI

  • First run is slow (60-120s) because ptai installs the underlying tools (nmap, nuclei, ffuf, sqlmap, gobuster, etc.) on demand. Persist ~/.cache/ptai/ and ~/.local/share/ptai/ between runs to keep subsequent jobs fast.
  • Pin the version with pip install 'ptai==0.11.0' to avoid surprise behavior changes mid-pipeline.
  • Scope files (--scope <yml>) are strongly recommended for CI. Out-of-scope hosts are rejected at tool-invocation time, but the scope file is a cleaner signal of intent than relying on URL parsing.
  • Cost control: set a hard budget with --max-cost 5.00 (USD). The orchestrator aborts gracefully if the running token cost passes the limit.
  • Rate-limited targets: add --stealth to back off on 429s and randomize User-Agent / source-IP-port for distributed runners.

Need something else?

ptai exits with a stable code on every CI failure mode:

Exit codeMeaning
0Run completed; no finding above --fail-on threshold
1Run completed; at least one finding ≥ --fail-on
2Engagement aborted (out-of-scope target, AUP rejected, scope file invalid)
3Tool dependency missing and auto-install failed
4LLM provider error (API key invalid, rate-limited, network)
5--max-cost budget exceeded

Map those into your CI's notification logic to distinguish "real findings" from infra failures.