Scanning from CI pipelines
Every CI system can start a DVSentry scan and fail the build on your policy result. The pattern is always the same:
- Register the repository in DVSentry once and note its id (Repository Security, or
dvsentry.py repositories). - Create a token with
scans:trigger,scans:readandgates:read(ideally a service account token limited to the organization) and store it as a CI secret namedDVSENTRY_TOKEN. - In the pipeline, download
/downloads/dvsentry.pyand runscan. Its exit code is the gate: 0 pass, 1 policy failed, 2 scan incomplete, 3 usage error, 4 API error.
DVSentry clones the repository itself at the commit you pass (--ref), so the commit must be pushed to the remote DVSentry registered. The scan runs on DVSentry, not on your runner.
On GitHub, the GitHub App is the simpler path: pushes to the default branch (including merges) and pull requests are scanned automatically and get a check. GitLab, Bitbucket and Azure Repos have no webhook receiver in DVSentry, so a CI step is the way there.
Private repositories
POST /v1/security-reports accepts a clone credential in one of two forms; the field names are the same for every host:
github_pat: a read-only token sent with this one request (any host, despite the name).organization_secret_id: the id of a token an Admin saved once in Settings → Secret Vault (recommended for CI: the token never passes through the pipeline).
The CLI does not send a clone credential yet, so for a private repository not linked to the GitHub App use the curl form at the end of this page with organization_secret_id.
GitHub Actions
# .github/workflows/dvsentry.yml
name: DVSentry
on:
pull_request:
push:
branches: [main]
jobs:
scan:
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: DVSentry scan and policy gate
env:
DVSENTRY_URL: https://app.dvsentry.com
DVSENTRY_TOKEN: ${{ secrets.DVSENTRY_TOKEN }}
DVSENTRY_REPOSITORY_ID: ${{ vars.DVSENTRY_REPOSITORY_ID }}
run: |
curl -fsSo dvsentry.py "$DVSENTRY_URL/downloads/dvsentry.py"
python3 dvsentry.py scan --repository-id "$DVSENTRY_REPOSITORY_ID" \
--scanner semgrep-local --scanner gitleaks --scanner osv-dependencies --scanner trivy-filesystem \
--ref "${{ github.event.pull_request.head.sha || github.sha }}" \
--strategy incremental --idempotency-key "gh-${{ github.run_id }}-${{ github.run_attempt }}" \
--timeout 2400
Optional: also upload the findings to GitHub code scanning (the token also needs reports:export). Replace the scan step's last command with these lines so the report id is kept:
run: |
curl -fsSo dvsentry.py "$DVSENTRY_URL/downloads/dvsentry.py"
set +e
python3 dvsentry.py scan --repository-id "$DVSENTRY_REPOSITORY_ID" \
--scanner semgrep-local --scanner gitleaks --scanner osv-dependencies \
--ref "${{ github.event.pull_request.head.sha || github.sha }}" | tee scan.log
GATE=${PIPESTATUS[0]}
REPORT_ID=$(awk '/^queued report/ {print $3}' scan.log)
curl -fsS -H "Authorization: Bearer $DVSENTRY_TOKEN" -o dvsentry.sarif \
"$DVSENTRY_URL/v1/security-reports/$REPORT_ID/export?format=sarif"
exit $GATE
- uses: github/codeql-action/upload-sarif@v3
if: always()
with: { sarif_file: dvsentry.sarif }
GitLab CI
# .gitlab-ci.yml
dvsentry:
stage: test
image: python:3.12-slim
variables:
DVSENTRY_URL: https://app.dvsentry.com
# DVSENTRY_TOKEN and DVSENTRY_REPOSITORY_ID are masked CI/CD variables
script:
- python3 -c "import urllib.request; urllib.request.urlretrieve('$DVSENTRY_URL/downloads/dvsentry.py', 'dvsentry.py')"
- python3 dvsentry.py scan --repository-id "$DVSENTRY_REPOSITORY_ID"
--scanner semgrep-local --scanner gitleaks --scanner osv-dependencies
--ref "$CI_COMMIT_SHA" --idempotency-key "gl-$CI_PIPELINE_ID-$CI_JOB_ID"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
GitLab can also show DVSentry results in its security widgets: download format=gitlab-sast and declare it as artifacts: reports: sast: gl-sast-report.json.
Jenkins (declarative pipeline)
pipeline {
agent any
environment {
DVSENTRY_URL = 'https://app.dvsentry.com'
DVSENTRY_REPOSITORY_ID = '3f0c5a1e-...'
}
stages {
stage('DVSentry') {
steps {
withCredentials([string(credentialsId: 'dvsentry-token', variable: 'DVSENTRY_TOKEN')]) {
sh '''
curl -fsSo dvsentry.py "$DVSENTRY_URL/downloads/dvsentry.py"
python3 dvsentry.py scan --repository-id "$DVSENTRY_REPOSITORY_ID" \
--scanner semgrep-local --scanner gitleaks --scanner osv-dependencies \
--ref "$GIT_COMMIT" --idempotency-key "jenkins-$BUILD_TAG"
'''
}
}
}
}
}
Use single quotes around the sh block so Groovy does not interpolate the token.
Bitbucket Pipelines
# bitbucket-pipelines.yml
pipelines:
default:
- step:
name: DVSentry
image: python:3.12-slim
script:
- python3 -c "import urllib.request; urllib.request.urlretrieve('https://app.dvsentry.com/downloads/dvsentry.py', 'dvsentry.py')"
- export DVSENTRY_URL=https://app.dvsentry.com
- python3 dvsentry.py scan --repository-id "$DVSENTRY_REPOSITORY_ID" --scanner semgrep-local
--scanner gitleaks --scanner osv-dependencies --ref "$BITBUCKET_COMMIT"
--idempotency-key "bb-$BITBUCKET_BUILD_NUMBER"
Azure Pipelines
# azure-pipelines.yml
trigger: [main]
pool: { vmImage: ubuntu-latest }
steps:
- script: |
curl -fsSo dvsentry.py "$(DVSENTRY_URL)/downloads/dvsentry.py"
python3 dvsentry.py scan --repository-id "$(DVSENTRY_REPOSITORY_ID)" \
--scanner semgrep-local --scanner gitleaks --scanner osv-dependencies \
--ref "$(Build.SourceVersion)" --idempotency-key "ado-$(Build.BuildId)"
displayName: DVSentry scan and policy gate
env:
DVSENTRY_URL: https://app.dvsentry.com
DVSENTRY_TOKEN: $(DVSENTRY_TOKEN) # secret pipeline variable
CircleCI and others
Any runner with Python 3.9+ works the same way: download the file, set the three variables, run scan.
Without the CLI (curl only, private repository)
set -euo pipefail
API="$DVSENTRY_URL/v1"; AUTH="Authorization: Bearer $DVSENTRY_TOKEN"
REPORT_ID=$(curl -fsS -X POST "$API/security-reports" -H "$AUTH" -H "Content-Type: application/json" \
-H "Idempotency-Key: ci-$BUILD_ID" \
-d "{\"repository_id\":\"$DVSENTRY_REPOSITORY_ID\",\"git_ref\":\"$COMMIT_SHA\",
\"scanners\":[\"semgrep-local\",\"gitleaks\",\"osv-dependencies\"],
\"organization_secret_id\":\"$DVSENTRY_CLONE_SECRET_ID\",\"environment\":\"production\"}" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["id"])')
while :; do
STATUS=$(curl -fsS "$API/security-reports/$REPORT_ID" -H "$AUTH" | python3 -c 'import json,sys; print(json.load(sys.stdin)["status"])')
case "$STATUS" in completed|partial|failed|cancelled|timed_out) break;; esac
sleep 15
done
curl -fsS "$API/security-reports/$REPORT_ID/ci-result" -H "$AUTH" | tee ci-result.json
exit "$(python3 -c 'import json; print(json.load(open("ci-result.json"))["exit_code"])')"
Tips
- Pick scanners for speed.
semgrep-local,gitleaksandosv-dependenciesgive code, secrets and dependencies quickly. Addtrivy-filesystemfor IaC,syft-sbomfor an SBOM,gitleaks-historyfor secrets in history (slower). - Incremental pull requests. Set a repository baseline and use
--strategy incremental: only scanners the change can affect run, and the gate judges what the change introduced. - Timeouts. A large repository can take longer than a CI job allows. Use
--no-waitto queue the scan and gate it in a later step or pipeline withdvsentry.py wait <report id>. - Retries. Pin
--idempotency-keyto the build id so a retried step returns the same report instead of a second scan. - Quota. Each CI scan counts as a scan on your plan (GitHub App scans do not).
Something missing or unclear? Tell us with Feedback in the app.