DVSentry Documentation API referenceOpen the app

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:

  1. Register the repository in DVSentry once and note its id (Repository Security, or dvsentry.py repositories).
  2. Create a token with scans:trigger, scans:read and gates:read (ideally a service account token limited to the organization) and store it as a CI secret named DVSENTRY_TOKEN.
  3. In the pipeline, download /downloads/dvsentry.py and run scan. 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:

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

Something missing or unclear? Tell us with Feedback in the app.