Skip to content

[Security hardening] Add automated security audit workflow #12

[Security hardening] Add automated security audit workflow

[Security hardening] Add automated security audit workflow #12

Workflow file for this run

name: Security Audit
permissions:
contents: read
on:
push:
branches: ["main"]
pull_request:
schedule:
- cron: "17 4 * * 1"
workflow_dispatch:
jobs:
dependency-audit:
name: Dependency audit (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.11", "3.12", "3.13"]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 2
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: ${{ matrix.python-version }}
- name: Compile scheduled audit requirements
if: ${{ github.event_name == 'schedule' }}
run: |
uv pip compile pyproject.toml --extra test --python-version "${{ matrix.python-version }}" --generate-hashes --quiet --output-file "${{ runner.temp }}/spec-kit-audit-requirements.txt"
- name: Run pip-audit (scheduled live resolution)
if: ${{ github.event_name == 'schedule' }}
run: uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r "${{ runner.temp }}/spec-kit-audit-requirements.txt" --progress-spinner off
- name: Check committed audit requirements are current
if: ${{ github.event_name != 'schedule' }}
env:
DEPENDENCY_DIFF_BASE: ${{ github.event.pull_request.base.sha || github.event.before || '' }}
DEPENDENCY_DIFF_HEAD: ${{ github.sha }}
GENERATED_REQUIREMENTS: ${{ runner.temp }}/security-audit-requirements.txt
run: python .github/scripts/check_security_requirements.py
- name: Run pip-audit (committed requirements)
if: ${{ github.event_name != 'schedule' }}
run: uvx --from pip-audit==2.10.0 pip-audit --disable-pip --require-hashes -r .github/security-audit-requirements.txt --progress-spinner off
static-analysis:
name: Static analysis
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
# Need the PR base to compare baseline growth.
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
# Blocking: HIGH severity only, with baseline. Real regressions fail CI.
- name: Run Bandit (HIGH, baseline-gated)
run: uvx --from bandit==1.9.4 bandit -r src -lll --baseline .github/bandit-baseline.json
# Informative: MEDIUM severity, no baseline. Surfaces lower-severity
# findings in the job summary without breaking CI, so reviewers see
# them before they accumulate.
- name: Run Bandit (MEDIUM, informational)
continue-on-error: true
run: uvx --from bandit==1.9.4 bandit -r src -ll
# Prevent silent whitelisting: if the baseline grew, the PR must carry
# the 'security-baseline-change' label to acknowledge it.
- name: Check Bandit baseline growth
if: ${{ github.event_name == 'pull_request' }}
env:
BANDIT_BASELINE_BASE: ${{ github.event.pull_request.base.sha }}
BANDIT_BASELINE_HEAD: ${{ github.event.pull_request.head.sha }}
BANDIT_BASELINE_LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }}
run: python .github/scripts/check_bandit_baseline.py
secret-scan:
name: Secret scan
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
with:
python-version: "3.13"
# detect-secrets is a Python tool (consistent with bandit / pip-audit
# install pattern) and detects entropy-based and provider-specific
# secrets. Baseline at .secrets.baseline is honored as a whitelist;
# any drift fails the check.
- name: Run detect-secrets
run: |
uvx --from detect-secrets==1.5.0 detect-secrets scan \
--baseline .secrets.baseline \
--exclude-files '\.secrets\.baseline$' \
--exclude-files 'uv\.lock$' \
--exclude-files '\.github/security-audit-requirements\.txt$'
- name: Verify baseline is in sync
run: |
if ! git diff --exit-code .secrets.baseline; then
echo "::error::detect-secrets found new candidates. Audit them, then update .secrets.baseline with: uvx --from detect-secrets==1.5.0 detect-secrets scan --baseline .secrets.baseline" >&2
exit 1
fi