GitHub Actions¶
Two ready-to-paste blocks: a minimal "scan and upload to DefectDojo" job, and a fuller multi-scanner version.
Minimal: Trivy + dd-cli¶
name: Security scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy
run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \
| sh -s -- -b /usr/local/bin
trivy fs --format json -o trivy.json .
- name: Set up Python + install dd-cli
uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install dd-cli
- name: Upload to DefectDojo
env:
DD_URL: ${{ secrets.DD_URL }}
DD_API_KEY: ${{ secrets.DD_API_KEY }}
run: |
dd import findings \
--file trivy.json \
--scanner "Trivy Scan" \
--product-type "GitHub Repos" \
--product "${{ github.repository }}" \
--engagement "${{ github.ref_name }}" \
--test-name "Trivy" \
--auto-create \
--yes
With Docker (no Python install in the runner)¶
- name: Upload to DefectDojo
env:
DD_URL: ${{ secrets.DD_URL }}
DD_API_KEY: ${{ secrets.DD_API_KEY }}
run: |
docker run --rm \
-v "$PWD:/work" -w /work \
-e DD_URL -e DD_API_KEY \
ghcr.io/osamamahmood/dd-cli:2 \
import findings \
--file trivy.json --scanner "Trivy Scan" \
--product-type "GitHub Repos" \
--product "${{ github.repository }}" \
--engagement "${{ github.ref_name }}" \
--test-name "Trivy" \
--auto-create --yes
Pin to a major version (
:2) so dependabot picks up patch / minor updates but not breaking ones.
Multi-scanner: Trivy + Bandit + Semgrep¶
name: Security scans
on:
push:
branches: [main]
schedule:
- cron: "0 6 * * *" # daily at 6am UTC
env:
DD_URL: ${{ secrets.DD_URL }}
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_PRODUCT_TYPE_NAME: GitHub Repos
DD_PRODUCT_NAME: ${{ github.repository }}
DD_ENGAGEMENT_NAME: ${{ github.ref_name }}
DD_AUTO_CREATE_CONTEXT: "true"
jobs:
trivy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
trivy fs --format json -o trivy.json .
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install dd-cli
- run: dd-reimport-findings # legacy shim — DD_* env vars do everything
env:
DD_TEST_NAME: Trivy
DD_TEST_TYPE_NAME: Trivy Scan
DD_FILE_NAME: trivy.json
bandit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install bandit dd-cli
- run: bandit -r src/ -f json -o bandit.json || true
- run: dd-reimport-findings
env:
DD_TEST_NAME: Bandit
DD_TEST_TYPE_NAME: Bandit Scan
DD_FILE_NAME: bandit.json
semgrep:
runs-on: ubuntu-latest
container: returntocorp/semgrep
steps:
- uses: actions/checkout@v4
- run: semgrep --config auto --json --output semgrep.json
- uses: actions/setup-python@v5
with: { python-version: "3.12" }
- run: pip install dd-cli
- run: dd-reimport-findings
env:
DD_TEST_NAME: Semgrep
DD_TEST_TYPE_NAME: Semgrep JSON Report
DD_FILE_NAME: semgrep.json
The pattern: shared env: block at the workflow level for everything every job needs (auth + product / engagement scoping), per-job env: for the scanner-specific bits.
Branching on the new typed exit codes¶
dd import findings (the new command) exits with typed codes — useful for CI logic:
- name: Upload + branch on result
env:
DD_URL: ${{ secrets.DD_URL }}
DD_API_KEY: ${{ secrets.DD_API_KEY }}
run: |
set +e
dd import findings \
--file trivy.json --scanner "Trivy Scan" \
--product-type "Web" --product "Payments" \
--auto-create --yes
rc=$?
set -e
case $rc in
0) echo "Upload successful";;
3) echo "::error::DefectDojo authentication failed — rotate DD_API_KEY"; exit 1;;
8) echo "::warning::Network blip — retrying"; sleep 30; dd import findings ...;;
9) echo "::error::Configuration missing"; exit 1;;
*) echo "::error::Upload failed with code $rc"; exit 1;;
esac
The legacy dd-reimport-findings shim flattens all errors to exit 1 for backward compatibility — see migration.