A GitHub Action that runs bandit (static code analysis) and pip-audit (dependency vulnerability scanning) on a Python repository, then surfaces findings as inline PR annotations, a workflow step summary, and a downloadable artifact.
Running bandit and pip-audit directly β or using the official focused actions (PyCQA/bandit-action and pypa/gh-action-pip-audit) β is a reasonable and common approach. Those tools and actions are fine on their own.
This action exists for workflows where you want both scanners behind one step and one place to read the outcome. It is a thin wrapper around the same tools, not a different kind of analysis. The things it adds on top of running the tools individually:
- Single step, unified report β one action replaces two, with no need to coordinate SARIF uploads or chain step outputs between jobs.
- Inline PR annotations β bandit findings appear as inline annotations on the "Files changed" tab, pointing directly to the affected file and line. pip-audit findings appear as summary-level annotations. Annotations generate no email notifications, so they don't add to developer fatigue on active PRs.
- Workflow step summary β the full report is written to the "Summary" tab of the workflow run.
- Optional PR comment β set
comment_on: blockingorcomment_on: alwaysto post a unified PR comment as well. The comment is created once and updated in place on every push, so the PR thread stays clean. Disabled by default to avoid notification noise. - Block on fixable-only vulnerabilities β
pip_audit_block_on: fixable(the default) fails CI only when a patched version exists, so you can act on it immediately; unfixable CVEs are reported but don't block. The official pip-audit action does not have this mode. - Automatic requirements export β pass
package_manager: uv|poetry|pipenvand the action runs the appropriate export command before invoking pip-audit. With the official pip-audit action, you must add a separate step to export first.
Using the two official actions (uv project, bandit + pip-audit):
jobs:
security:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
# Static analysis
- uses: PyCQA/bandit-action@v1
with:
targets: src/
# Export dependencies before pip-audit (required for uv projects)
- name: Export requirements
run: uv export --no-dev --format requirements-txt > requirements.txt
# Dependency audit
- uses: pypa/gh-action-pip-audit@v1
with:
inputs: requirements.txt
# Note: no built-in "fixable-only" blocking mode
# Note: no unified report, no inline PR annotationsUsing this action (equivalent result, one step):
jobs:
security:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: uv # export handled automatically
bandit_scan_dirs: 'src/'
pip_audit_block_on: fixable # only block when a fix exists
# Inline PR annotations and step summary are written automaticallyBandit findings are emitted as inline workflow annotations that appear directly on the affected file and line in the PR "Files changed" tab:
::error file=src/app.py,line=2::[B404] Consider possible security implications associated with subprocess module.
::warning file=src/app.py,line=5::[B602] subprocess call with shell=True identified, security issue.
pip-audit findings appear as summary-level annotations (no file/line available):
::warning::pip-audit: requests@2.25.0 β GHSA-j8r2-6x86-q33q (fix: 2.31.0)
Annotation severity maps to bandit severity: HIGH β error, MEDIUM β warning, LOW β notice. Annotations are always emitted and generate no email notifications.
The full report is written to the workflow run "Summary" tab on every run.
Set comment_on: blocking or comment_on: always to also post a PR comment. When issues are found, the comment looks like this:
# Security Audit Report
## Bandit β Static Security Analysis
| Severity | Confidence | File | Line | Issue |
|---|---|---|---|---|
| π΄ HIGH | HIGH | `src/app.py` | 2 | [B404] Consider possible security implications associated with subprocess module. |
| π‘ MEDIUM | MEDIUM | `src/app.py` | 5 | [B602] subprocess call with shell=True identified, security issue. |
_2 issue(s) found, 1 at or above HIGH threshold._
## pip-audit β Dependency Vulnerabilities
| Package | Version | ID | Fix Versions | Description |
|---|---|---|---|---|
| requests | 2.25.0 | GHSA-j8r2-6x86-q33q | 2.31.0 | Unintended leak of Proxy-Authorization header ... |
_1 vulnerability/vulnerabilities found (1 fixable) across 1 package(s)._
---
**Result: β Blocking issues found β see details above.**
The comment is idempotent β created once and updated in place on every push, so the PR thread stays clean. Each section includes a direct link to the repository's GitHub Code Scanning page (Bandit) and Dependabot security alerts page (pip-audit).
Add this to your workflow (e.g. .github/workflows/security.yml):
name: Security Audit
on:
pull_request:
push:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0This runs both bandit and pip-audit with sensible defaults: blocks the job on HIGH-severity code issues and on dependency vulnerabilities that have a fix available.
The default configuration (annotations + step summary, no PR comment) only needs:
permissions:
contents: read
security-events: write # upload bandit SARIF to GitHub Code ScanningWhen comment_on is set to blocking or always, add:
permissions:
contents: read
pull-requests: write # post/update the PR comment
security-events: writeIf you don't need Code Scanning integration, contents: read alone is sufficient.
Pass package_manager to match how your project manages dependencies. The action exports a requirements list before invoking pip-audit, so no extra step is needed.
uv:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: uv
bandit_scan_dirs: 'src/'Poetry:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: poetry
bandit_scan_dirs: 'src/'Pipenv:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: pipenv
bandit_scan_dirs: 'src/'Plain requirements file (default):
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
requirements_file: requirements/prod.txt
bandit_scan_dirs: 'src/'When your source code spans more than one directory, pass a comma-separated list to bandit_scan_dirs:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: uv
bandit_scan_dirs: 'src/,scripts/'Set working_directory to the project root within the repo. All relative paths (scan dirs, requirements file) are resolved from there:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
working_directory: services/api
package_manager: uv
bandit_scan_dirs: 'src/'Useful when you manage dependencies externally or run pip-audit in a separate job:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
tools: bandit
bandit_scan_dirs: 'src/'Useful when you already run bandit separately or only care about known CVEs in dependencies:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
tools: pip-audit
package_manager: uvBlock on any bandit finding at MEDIUM or above, and on all known vulnerabilities regardless of whether a fix exists. Suitable for high-assurance services or regulated environments:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: poetry
bandit_severity_threshold: medium
pip_audit_block_on: allAdd the action first as an observer: findings appear as inline annotations and in the step summary without ever failing the job. Tighten the thresholds once your team has addressed the backlog:
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: uv
bandit_severity_threshold: low # report everything
pip_audit_block_on: none # never block
comment_on: always # optionally post findings to the PR comment tooRun a weekly audit against main in addition to PR checks, so vulnerabilities introduced through dependency updates are caught promptly:
name: Weekly Security Audit
on:
schedule:
- cron: '0 8 * * 1' # every Monday at 08:00 UTC
push:
branches: [main]
jobs:
security:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- uses: developmentseed/action-python-security-auditing@8ebea22ea75dfba2244ed9883c2aa6cb4df8d9a9 # v0.6.0
with:
package_manager: uv
# comment_on defaults to never β no PR comment is posted for scheduled runsIf you run this action from more than one workflow on the same PR with comment_on: blocking or comment_on: always, each workflow automatically gets its own PR comment. No extra configuration is needed β the comment is keyed on the workflow name, so the two comments stay independent and update in place separately.
The job fails (non-zero exit) when either tool finds issues above its configured threshold.
Bandit threshold (bandit_severity_threshold): findings at or above the threshold block the job.
bandit_severity_threshold |
Blocks on |
|---|---|
high (default) |
π΄ HIGH only |
medium |
π‘ MEDIUM and π΄ HIGH |
low |
π’ LOW, π‘ MEDIUM, and π΄ HIGH |
pip-audit threshold (pip_audit_block_on):
pip_audit_block_on |
Blocks on |
|---|---|
fixable (default) |
Vulnerabilities with a fix available β you can act on these immediately |
all |
All known vulnerabilities, including those with no fix yet |
none |
Never blocks β audit runs but CI stays green |
| Input | Default | Description |
|---|---|---|
tools |
bandit,pip-audit |
Comma-separated tools to run |
bandit_scan_dirs |
. |
Comma-separated directories for bandit to scan (relative to working_directory) |
bandit_severity_threshold |
high |
Minimum severity that blocks the job: high, medium, or low |
pip_audit_block_on |
fixable |
When pip-audit findings block the job: fixable, all, or none |
package_manager |
requirements |
How to resolve deps for pip-audit: uv, pip, poetry, pipenv, requirements |
requirements_file |
requirements.txt |
Path to requirements file when package_manager=requirements |
working_directory |
. |
Directory to run the audit from (useful for monorepos) |
comment_on |
never |
When to post a PR comment: never, blocking (only when issues block the job), or always |
github_token |
${{ github.token }} |
Token used for posting PR comments (only needed when comment_on is not never) |
artifact_name |
security-audit-reports |
Name of the uploaded artifact |
debug |
false |
Enable verbose debug logging; also activates automatically when re-running a workflow with "Enable debug logging" |
- Annotations β always emitted. Bandit findings appear as inline annotations on the PR "Files changed" tab (keyed to file and line). pip-audit findings appear as summary-level annotations. No email notifications are generated.
- Step summary β the full report is written to the workflow run summary, visible under the "Summary" tab.
- PR comment β opt-in via
comment_on: blockingorcomment_on: always. Created on first run, updated in place on every subsequent run. The comment is keyed on a hidden<!-- security-scan-results::{workflow-name} -->marker, so multiple workflows on the same PR each maintain their own separate comment. - Artifact β
pip-audit-report.jsonandresults.sarifuploaded under the name set byartifact_name(default:security-audit-reports) for download or downstream steps. Theresults.sariffile is the bandit SARIF report; it is also uploaded to GitHub Code Scanning automatically by the underlyinglhoupert/bandit-actionstep, making findings visible in the repository's Security tab when the job hassecurity-events: writepermission. - Exit code β non-zero when blocking issues are found, so the job fails and branch protections can enforce it.
uv pip install -e ".[dev]"
uv run pytest
pre-commit run --all-files