Fortify CI/CD Integration Guide
Who can Use this ?Developers & Software Engineers, AI/ML Professionals, DevOPs & Infrastructure teams, QA and Test Engineers are the ideal users.
This section provides a step-to-step guide to implement Fortify CI/CD integration into your pipeline, using a connector script, a configuration file, and hardcoded deployment steps. This guide is perfect for users who want to Integrate automated security scanning into CI/CD pipelines for continuous protection.
Introduction
Overview
CI/CD integration for Fortify introduces a new feature enabling automated red teaming scans within the Software Development Lifecycle (SDLC). Using an asynchronous session-based architecture, CI/CD pipelines can trigger Fortify scans on remote connector instances, allowing automated security validation as part of build and deployment workflows.
Key Capabilities
- CI/CD Scan Triggering – Pipelines can initiate Fortify scan sessions via API.
- Async Session Architecture – CI/CD sends a request; the Target connector polls and executes the scan.
- Session-Based Tracking – Each scan generates a unique Session ID for status monitoring and results.
- Flexible Pipeline Behavior – Pipelines can either:
- Fire-and-Forget (trigger scan and continue), or
- Wait/Block (poll for scan completion and results).
Prerequisites
- Connector Script: A connector script must be built to handle logic such as hitting the bots API, authentication, and fetching the conversation ID.
- Fortify User and Target: You must be a valid user and create your target in the Fortify UI instance to generate the necessary API key for authentication.
- Supported Session Types: The current CI/CD integration only supports the following session types:
-
Essential
-
Comprehensive
-
Target specific Note: Attack library functionality is not supported in the current version.
-
Step-by-Step Implementation
Step 1: Create and Configure fortifyconfig.yml
This YAML file holds the session parameters and must be added to your codebase.
Example of a configuration file:
api_key: FORTIFY_API_KEY # Name of the environment variable containing your key
base_url: https://fortify-dev.fuelix.ai # Optional - Custom Fortify API base URL
wait_for_completion: false # false = fire-and-forget; true = waits until done
session:
integration_type: "api" # "api" | "http"
session_type: "essential" # "essential" | "comprehensive" | "target_specific"
attacker_type: "mturn" # "sturn" (single-turn) | "mturn" (multi-turn)
rerun_count: 0 # [1, 5, 10, 15, 20] for sturn; 0 for mturn
lang: "en-US" # language code
tags: [ci] # Labels for filtering
# fortifyconfig.yml
authentication:
api_key: "<YOUR_FORTIFY_API_KEY>"
base_url: "https://fortify.fax.ai"
session_parameters:
wait_for_completion: false # Set to 'true' to halt deployment
integration_type: API
session_type: essential
attacker_type: singleton
rerun_count: 5 # Must be 1-20 for 'singleton', 0 for 'multi-turn'
language: enus
- Define Authentication Parameters: Include your Fortify API key and the base URL (e.g., fortify.fax.ai).
- Configure wait for completion:
- Set to true to halt the build/deployment until Fortify returns scan results.
- Set to false for "fire and forget," where the session triggers in the background, but the build proceeds without waiting for results (default).
- Define Session Parameters: These mirror the fields found in the Fortify UI for creating a new session.
| Parameters | Options/Details |
|---|---|
| integration type | API or HTTP. |
| session type | Choose one: essential, comprehensive, or target specific. Using any other value will result in an error. |
| attacker type | Choose multi-turn or singleton. |
| rerun count | If multi-turn is chosen: Must be 0. If singleton is chosen: Must be between 1 and 20. 0 is not supported. |
| language | Supported languages are enus and ca. |
Step 2: Add Commands to Your Deployment File
You must add three hardcoded commands to your existing deployment YAML file, typically located in your GitHub workflows folder.
- Install Python Version
- Add the command to install a Python version. This is required regardless of your bot's primary language.
- Install Fortify SDK
- Add the command to install the Fortify SDK via a public URL. This initiates the connection logic.
- Run Security Scan
- Add the command to run the security scan, ensuring it references the fortifyconfig.yml file to utilize the defined session parameters.
Step 3: Post-Scan Management (Optional but Recommended)
For better visibility in your CI/CD process, consider adding steps to display scan results on Pull Requests (PRs).
- Name Scan Session on PR: Include a command to name the scan session on a PR. Fortify will then provide a link to the running session and display the results upon completion.
- GitHub Action Integration: This allows Fortify, through a GitHub action, to comment directly on the PR with vulnerability scan results and metrics.
- Conditional Build Failure: You can implement simple if/else logic in your deployment file to enforce build failure based on metrics (e.g., if the vulnerability rate is greater than 10%) or if the scan status is failed.
Step 4: Scheduling and Triggering (Optional)
Control when the CI/CD session is triggered.
- Define Trigger Branches: Specify which branches or pull requests should trigger the CI/CD session.
- Recommended Trigger: Configure the scan to run when a Pull Request is opened. This allows reviewers to examine Fortify results before approving the code merge and deployment.
- Scheduled Reruns: Use schedule with cron syntax (e.g., in UTC timing) to automatically trigger Fortify sessions on a recurring basis (e.g., nightly, weekly, or monthly) directly from your codebase.
CI/CD Pipeline Integration
Three steps to add Fortify to any pipeline:
Step 1 → Commit fortifyconfig.yml to your repo
Step 2 → Store FORTIFY_API_KEY as a CI secret
Step 3 → Add the fortify run step
Step 1: Create the Configuration File
Place fortifyconfig.yml in your repository root. The API key is resolved from the environment at runtime — never hardcode it.
api_key: FORTIFY_API_KEY
wait_for_completion: true
session:
integration_type: "http"
session_type: "comprehensive"
attacker_type: "sturn"
rerun_count: 5
lang: "en-US"
tags: [ci]
http_integration_opts:
endpoint: "https://your-bot.example.com/chat"
headers:
Authorization: "Bearer YOUR_TOKEN"
Validate locally before committing:
export FORTIFY_API_KEY="your-key"
fortify -c fortifyconfig.yaml validate
Step 2: Store the API Key as a CI Secret
⚠️ Never commit API keys to source control.
| CI Provider | Where |
|---|---|
| GitHub Actions | Settings → Secrets and variables → Actions → New repository secret |
| GitLab CI | Settings → CI/CD → Variables → Add variable (enable Masked) |
| Jenkins | Manage Jenkins → Credentials → Global → Add Credentials → Secret text |
Secret name: FORTIFY_API_KEY
Step 3: Add the CI Job
GitHub Actions - Default Workflow:
# .github/workflows/fortify-security-test.yml
name: Fortify Security Scan
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
schedule:
- cron: '0 2 * * *'
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install FortifySDK
run: pip install https://storage.googleapis.com/fortify-sdk/releases/fortifysdk-1.1.0-py3-none-any.whl
- name: Validate configuration
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
run: fortify -c fortifyconfig.yaml validate
- name: Run security scan
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
run: fortify -c fortifyconfig.yaml run --verbose --output results.json
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: fortify-results
path: results.json
retention-days: 30
- name: Enforce vulnerability threshold
run: |
VULN_RATE=$(jq -r '.vulnerability_rate' results.json | tr -d '%')
if [ "$VULN_RATE" -gt "10" ]; then
echo "❌ Vulnerability rate ${VULN_RATE}% exceeds threshold."
exit 1
fi
echo "✅ Vulnerability rate ${VULN_RATE}% is within threshold."
GitHub Actions — api integration (two-process):
# .github/workflows/fortify-api-integration.yml
name: Fortify Security Scan (API Integration)
on:
push:
branches: [main]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install https://storage.googleapis.com/fortify-sdk/releases/fortifysdk-1.1.0-py3-none-any.whl
pip install -r requirements.txt
- name: Start executor in background
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
BOT_API_KEY: ${{ secrets.BOT_API_KEY }}
BOT_BASE_URL: ${{ secrets.BOT_BASE_URL }}
run: |
python executor.py &
echo $! > executor.pid
sleep 5
- name: Run security scan
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
run: fortify -c fortifyconfig.yaml run --verbose --output results.json
- name: Stop executor
if: always()
run: kill $(cat executor.pid) || true
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: fortify-results
path: results.json
retention-days: 30
GitHub Actions — wait_for_completion: false + polling loop:
Use this when you want the session to be created quickly (false), but still need results in the same pipeline run. The trick: loop fortify status until the session completes.
# .github/workflows/fortify-poll-loop.yml
name: Fortify Scan (Poll Loop)
on:
push:
branches: [main]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install FortifySDK
run: pip install https://storage.googleapis.com/fortify-sdk/releases/fortifysdk-1.1.0-py3-none-any.whl
- name: Create session (fire-and-forget)
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
run: |
fortify -c fortifyconfig.yaml run --output session.json
echo "SESSION_ID=$(jq -r '.session_id' session.json)" >> "$GITHUB_ENV"
- name: Poll until session completes
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
run: |
echo "Polling session: $SESSION_ID"
while true; do
fortify -c fortifyconfig.yaml status -s "$SESSION_ID" --output results.json
STATUS=$(jq -r '.status' results.json)
echo "Status: $STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
break
fi
sleep 30
done
- name: Enforce vulnerability threshold
run: |
VULN_RATE=$(jq -r '.vulnerability_rate' results.json | tr -d '%')
if [ "$VULN_RATE" -gt "10" ]; then
echo "❌ Vulnerability rate ${VULN_RATE}% exceeds threshold."
exit 1
fi
echo "✅ Vulnerability rate ${VULN_RATE}% is within threshold."
- name: Upload results
uses: actions/upload-artifact@v4
if: always()
with:
name: fortify-results
path: results.json
The fortifyconfig.yaml for this workflow:
api_key: FORTIFY_API_KEY
wait_for_completion: false # ← CLI exits immediately after session creation
session:
integration_type: "http"
session_type: "comprehensive"
attacker_type: "sturn"
rerun_count: 5
lang: "en-US"
tags: [ci]
http_integration_opts:
endpoint: "https://your-bot.example.com/chat"
headers:
Authorization: "Bearer YOUR_TOKEN"
GitHub Actions — Post scan results as a PR comment:
# .github/workflows/fortify-pr-comment.yml
name: Fortify Scan + PR Comment
on:
pull_request:
branches: [main]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install FortifySDK
run: pip install https://storage.googleapis.com/fortify-sdk/releases/fortifysdk-1.1.0-py3-none-any.whl
- name: Run security scan
env:
FORTIFY_API_KEY: ${{ secrets.FORTIFY_API_KEY }}
run: fortify -c fortifyconfig.yaml run --verbose --output results.json
- name: Comment results on PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const results = JSON.parse(fs.readFileSync('results.json', 'utf8'));
const rate = results.vulnerability_rate || 'N/A';
const status = results.status || 'unknown';
const total = results.total_conversations || 0;
const vuln = results.vulnerable_conversations || 0;
const icon = parseInt(rate) > 10 ? '🔴' : '🟢';
const body = `## ${icon} Fortify Security Scan Results
| Metric | Value |
|---|---|
| **Status** | \`${status}\` |
| **Total Conversations** | ${total} |
| **Vulnerable** | ${vuln} |
| **Vulnerability Rate** | **${rate}** |
> Session: \`${results.session_id}\``;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: body
});
GitLab CI:
# .gitlab-ci.yml
fortify-security-scan:
stage: test
image: python:3.11-slim
before_script:
- pip install https://storage.googleapis.com/fortify-sdk/releases/fortifysdk-1.1.0-py3-none-any.whl
- fortify -c fortifyconfig.yaml validate
script:
- fortify -c fortifyconfig.yaml run --verbose --output results.json
artifacts:
paths: [results.json]
expire_in: 30 days
when: always
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
Secret: Settings → CI/CD → Variables → Add variable. Key: FORTIFY_API_KEY. Enable Masked and Protected.
Jenkins:
// Jenkinsfile
pipeline {
agent any
environment {
FORTIFY_API_KEY = credentials('fortify-api-key')
}
stages {
stage('Install') { steps { sh 'pip install https://storage.googleapis.com/fortify-sdk/releases/fortifysdk-1.1.0-py3-none-any.whl' } }
stage('Validate') { steps { sh 'fortify -c fortifyconfig.yaml validate' } }
stage('Security Scan') { steps { sh 'fortify -c fortifyconfig.yaml run --verbose --output results.json' } }
}
post {
always { archiveArtifacts artifacts: 'results.json', allowEmptyArchive: true }
failure { echo 'Security scan failed. Review results.json.' }
}
}
Secret: Manage Jenkins → Credentials → Global → Add Credentials → Secret text. ID: fortify-api-key.
Configuration Reference
The fully annotated fortifyconfig.yaml — single source of truth for all settings:
────────────────────────────────────────────────
# AUTHENTICATION
────────────────────────────────────────────────
api_key: FORTIFY_API_KEY
# Name of the environment variable holding your API key.
# The SDK reads the key from the environment at runtime — not from this file.
base_url: https://api.fortify.fuelix.ai
# Optional. Override the default Fortify API base URL.
────────────────────────────────────────────────
# EXECUTION MODE
────────────────────────────────────────────────
wait_for_completion: true
# true → Block until the session finishes and return full results.
# false → Create the session and exit immediately (fire-and-forget).
# See "Understanding wait_for_completion" for detailed behaviour.
────────────────────────────────────────────────
# SESSION SETTINGS
────────────────────────────────────────────────
session:
integration_type: "http"
# "http" → Fortify calls your bot's HTTP endpoint directly.
# "api" → You run an SDK executor that polls and forwards prompts.
# See "Integration Types" for when to use each.
session_type: "essential"
# "essential" → Fast scan with core attack vectors.
# "comprehensive" → Deep scan with extended attack surface.
# "target_specific" → Custom, targeted attack scenarios.
attacker_type: "sturn"
# "sturn" → Single-turn: one prompt, one response per conversation.
# "mturn" → Multi-turn: maintains conversation history across turns.
rerun_count: 5
# For sturn: 1–20.
# For mturn: must be 0.
lang: "en-US"
# BCP-47 language code for generated attack prompts.
tags: [ci, production]
# Labels for filtering sessions on the dashboard.
# ── HTTP Integration (required when integration_type is "http") ──
http_integration_opts:
endpoint: "https://your-bot.example.com/chat"
headers:
Authorization: "Bearer YOUR_TOKEN"
# ── Metadata (optional) ──
meta:
model: "gpt-4"
environment: "staging"
────────────────────────────────────────────────
# POLLING (only applies when wait_for_completion is true)
────────────────────────────────────────────────
polling:
interval: 30
# Seconds between status checks. Minimum: 10. Default: 10.
Field Reference
| Field | Required | Default | Description |
|---|---|---|---|
| api_key | ✅ | — | API key string or name of an env var containing it |
| base_url | ❌ | Platform default | Custom Fortify API base URL |
| wait_for_completion | ❌ | false | Block until the session finishes |
| session.integration_type | ✅ | — | "http" or "api" |
| session.session_type | ✅ | — | "essential" · "comprehensive" · "target_specific" |
| session.attacker_type | ✅ | — | "sturn" (single-turn) · "mturn" (multi-turn) |
| session.rerun_count | ✅ | — | 1–20 for sturn; 0 for mturn |
| session.lang | ✅ | — | BCP-47 language code (e.g., "en-US") |
| session.tags | ✅ | — | List of categorization tags |
| session.meta | ❌ | Arbitrary key-value metadata | |
| session.http_integration_opts.endpoint | ✅(http) | — | Your bot's HTTP URL |
| session.http_integration_opts.headers | ❌ | Headers sent with every attack request | |
| polling.interval | ❌ | 10 | Seconds between polling cycles (min: 10) |
Updated 4 months ago
