A GitHub Action that runs bandit (static code analysis) and pip-audit (dependency vulnerability scanning) on Python repos, then consolidates results into a single PR comment, workflow step summary, and downloadable artifact.
It is a composite action (action.yml) backed by a small Python package (src/python_security_auditing/).
action.yml ← GitHub Action entry point (composite steps)
src/python_security_auditing/
__main__.py ← Orchestrator: settings → runners → report → comment → exit
settings.py ← Pydantic-based config from env vars (GitHub Action inputs)
runners.py ← Tool invocation: SARIF parsing, pip-audit, package manager adapters
report.py ← Markdown report builder and threshold checker
pr_comment.py ← Upsert PR comment via `gh` CLI
Flow: Settings loads env vars → runners invokes tools and parses output → report builds markdown and checks thresholds → pr_comment posts/updates the PR comment → __main__ exits 0 or 1.
Key boundaries:
settings.py— input/config boundary (reads env vars, validates via Pydantic)runners.py— external tool boundary (subprocess calls to bandit SARIF, pip-audit, package managers)report.py— pure logic (markdown generation, threshold checking — no I/O except step summary)pr_comment.py— GitHub API boundary (subprocess calls toghCLI)
- Build system: Hatch (
hatchling) - Python: ≥ 3.13
- Dependencies:
pydantic-settings,pip-audit - Dev deps:
pytest,pytest-mock,mypy(strict),ruff
# Install in dev mode
uv pip install -e ".[dev]"
# Run tests
uv run pytest
# Type checking (strict mode)
uv run mypy src/
# Lint and format
uv run ruff check src/ tests/
uv run ruff format src/ tests/- Tests live in
tests/and mirror module names:test_settings.py,test_runners.py,test_report.py. - Test fixtures (JSON/SARIF samples) are in
tests/fixtures/. - External tool calls (
subprocess.run) are mocked in tests — never invoke realbandit,pip-audit, orghin unit tests. - Settings are configured via
monkeypatch.setenv()sinceSettingsreads from env vars. - Run the full suite after any change:
pytest.
- Formatter/linter: Ruff (line-length 100, rules: E, F, I, UP)
- Type checking: mypy strict mode
- Imports:
from __future__ import annotationsin every module - Type annotations: use
dict[str, Any],list[...],int | None(modern syntax, noOptional/Dict/List) - Match existing patterns — don't refactor surrounding code when making a change.
- SARIF input for bandit: Bandit runs in a separate composite step (
lhoupert/bandit-action). This package only reads the SARIF output file — it does not invoke bandit directly. - PR comment is idempotent: Uses a hidden HTML marker (
<!-- security-scan-results -->) to find and update the same comment on subsequent pushes. - Threshold logic:
check_thresholds()inreport.pyreturns a boolean; the orchestrator translates that tosys.exit(1). - Package manager adapters:
generate_requirements()normalizes all package managers to arequirements.txtfile before passing topip-audit.