Skip to navigation

CI/CD Integration

Overview

Pensar provides a CLI tool and API for integrating automated pentesting directly into your CI/CD pipeline. Run pentests on every commit or deployment to catch vulnerabilities before they reach production.

CI-triggered pentests consume credits from your workspace balance. See Billing & Usage for details on credit management and usage tracking.

Installation

Install the Pensar CI package globally:

npm install -g @pensar/ci

Or add it as a dev dependency:

npm install --save-dev @pensar/ci

View the source code and additional examples on GitHub.

Configuration

Required Environment Variables

VariableDescription
PENSAR_API_KEYYour Pensar API key

Optional Environment Variables

VariableDescriptionDefault
PENSAR_ENVIRONMENTdev, staging, or productionproduction

You can configure CI/CD integration directly from the Pensar Console. Navigate to Settings → Automation → CI/CD in your workspace for guided setup and one-click workflow creation.

CLI Usage

Run a Pentest

pensar pentest [options]

Options:

OptionDescription
-b, --branch <branch>Branch name to associate with the scan
-e, --environment <env>Environment: dev, staging, production
--commit <sha>Exact commit to test; pass the deployed SHA for post-deploy runs
--no-waitDispatch the scan without waiting for completion

Examples:

# Basic pentest using environment variables
pensar pentest
# Pentest a specific branch
pensar pentest --branch main
# Pentest without waiting for completion
pensar pentest --no-wait
# Pentest with explicit environment
pensar pentest --branch develop --environment staging

How the CI diff is calculated

By default:

  • Compare: Your requested commit (--commit) is compared with the latest qualifying completed pentest for the same repository in your workspace. This baseline is shared across branches and environments; it is not the previous deployment.
  • Filter: Files such as package.json and lockfiles are excluded. If no changes remain, testing is skipped; large changes can trigger broader coverage.
  • Test: The agent uses the diff to create security objectives for the affected applications.
  • Results: A launched scan returns an ID and status, with findings after testing completes. Buffered runs can defer launch; a queued scan is not a passing result.

Without a baseline, or if a force-push removed it, Pensar runs without diff narrowing. Other comparison errors fail the run. For an explicit comparison, the API accepts baseSha with commitSha; both commits must exist.

Compare two explicit commits

To test exactly what changed between two deployed versions, pass the previously deployed commit as --base (or PENSAR_BASE_SHA) along with the new commit. Requires @pensar/ci 2.7.0 or later.

pensar pentest \
--commit 2222222222222222222222222222222222222222 \
--base 1111111111111111111111111111111111111111

In GitHub Actions the repository is detected automatically; elsewhere, pass --repo-id. The CLI sends these as baseSha and commitSha to POST /ci/dispatch, which you can also call directly.

  • Both values must be full 40-character SHAs, and the repository must be known (--repo-id, or repoId / repositoryId on the API).
  • Both commits must exist in that repository. A missing commit returns 400; Pensar does not fall back to the last pentest or a full scan.
  • The two commit trees are compared directly, so rollbacks and diverged histories work. The commit buffer is bypassed.
  • If the two commits are the same, there is nothing to test and the run is skipped.
  • The range is kept if the scan is queued, deduplicated, or retried.

Run Only on Labelled Changes

When only some changes are worth pentesting, label the pull request and gate the workflow on that label. This is done in the workflow, not in the CLI.

On a pull-request-triggered workflow the labels are in the event payload, so a job-level condition is all you need and nothing extra runs:

jobs:
pentest:
if: contains(github.event.pull_request.labels.*.name, 'pentest')
runs-on: ubuntu-latest
# ...

On a push or a post-deploy run there is no pull request in the payload, so the pull request the commit came from has to be looked up. Add a small job that does the lookup and make the pentest job depend on it:

permissions:
contents: read
pull-requests: read # lets the gate read the label off the pull request
jobs:
gate:
runs-on: ubuntu-latest
outputs:
labelled: ${{ steps.check.outputs.labelled }}
steps:
- id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LABEL: "pentest"
run: |
if gh api "repos/${{ github.repository }}/commits/${{ github.sha }}/pulls" \
--jq '.[].labels[].name' | grep -Fqx -- "$LABEL"; then
echo 'labelled=true' >> "$GITHUB_OUTPUT"
else
echo "No $LABEL label on the pull request for this commit. Skipping."
echo 'labelled=false' >> "$GITHUB_OUTPUT"
fi
pentest:
needs: gate
if: needs.gate.outputs.labelled == 'true'
runs-on: ubuntu-latest
# ...

Because the pentest job depends on the gate, an unlabelled change never starts it: no runner, no checkout, no install.

Keep the label in LABEL rather than inlining it into the command. A label is free text, so an apostrophe would break the quoting and a . or * would make grep match a different label; -F compares it literally.

After a deploy, use ${{ github.event.workflow_run.head_sha }} rather than ${{ github.sha }}. On a workflow_run event github.sha is the tip of the default branch, not the commit that was deployed, so the gate would check whichever change landed most recently.

The Console generates both of these for you: enter a label under Settings → CI/CD and the workflow snippets on that page include the gate, with the right SHA for each trigger.

Check Scan Status

pensar status <scanId>

Example:

pensar status abc123-def456-789

CI/CD Examples

GitHub Actions

Run a pentest when commits are pushed to main branches:

name: Pensar Pentest
on:
push:
branches: [main, master, develop]
jobs:
pentest:
name: Pensar Pentest
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install Pensar CI
run: npm install -g @pensar/ci
- name: Run Pensar Pentest
env:
PENSAR_API_KEY: ${{ secrets.PENSAR_API_KEY }}
run: pensar pentest --branch ${{ github.ref_name }}

GitLab CI

Add to your .gitlab-ci.yml:

stages:
- security
pensar-pentest:
stage: security
image: node:22
rules:
- if: $CI_COMMIT_BRANCH == "main"
before_script:
- npm install -g @pensar/ci
script:
- pensar pentest --branch $CI_COMMIT_REF_NAME
variables:
PENSAR_API_KEY: $PENSAR_API_KEY

Bitbucket Pipelines

Add to your bitbucket-pipelines.yml:

pipelines:
branches:
main:
- step:
name: Pensar Pentest
image: node:22
script:
- npm install -g @pensar/ci
- pensar pentest --branch main

Exit Codes

CodeMeaning
0Scan completed with no issues
1Scan found security issues or failed

By default, the CLI will exit with code 1 if any security issues are found. Use this to block deployments when vulnerabilities are detected.

API Reference

Dispatch Scan

POST /ci/dispatch

Headers:

  • Authorization: Bearer <api_key> or x-api-key: <api_key>
  • Content-Type: application/json

Request Body:

Provide a repository so the pentest is scoped to that repository’s applications. Supply either repoId (GitHub’s numeric repository id — the CLI auto-detects this from GITHUB_REPOSITORY_ID) or repositoryId (the internal repository UUID). When neither is supplied, application scope remains workspace-wide. Exactly one connected repository still supplies attribution and a baseline; multiple repositories leave the scan unattributed without diff narrowing.

{
"repoId": 123456789,
"repositoryId": "string (uuid, optional alternative to repoId)",
"branch": "string (optional)",
"scanLevel": "priority | full (optional)",
"commitSha": "string (optional)",
"baseSha": "string (optional; requires commitSha and a selected repository)",
"testType": "custom | endpoint-selection (optional, defaults to custom)"
}

The repository must already be connected to the workspace. If it isn’t found, the API responds 404; if it belongs to another workspace, 403; if it has no discovered applications yet (run reconnaissance first), 422.

With baseSha, CI compares exactly that commit to commitSha in the selected repository and bypasses the commit buffer. Both commits must exist there; a missing commit returns 400 instead of recovering to a full scan. Omit baseSha to keep the default last-pentest baseline.

testType: custom derives application-level objectives from the diff; endpoint-selection selects affected discovered endpoints. Both use the same repository baseline and recovery rules above. A supplied branch must match the repository’s configured target branch; it does not select a different branch for one run.

Response:

{
"scanId": "string",
"label": "string",
"status": "queued"
}

Get Scan Status

GET /ci/status/{scanId}

Headers:

  • Authorization: Bearer <api_key> or x-api-key: <api_key>

Response:

{
"scanId": "string",
"label": "string",
"status": "queued | running | completed | failed | paused",
"startedAt": "string | null",
"completedAt": "string | null",
"errorMessage": "string | null",
"issuesCount": 0,
"issueCountsBySeverity": {
"critical": 0,
"high": 0,
"medium": 0,
"low": 0
},
"reportReady": false
}

Next Steps