security: pip-audit + bandit gates that can fail; patch 29 known CVEs
security.yml previously ran safety/bandit/semgrep with `|| true` and could not go red. Now: pip-audit on requirements.txt is a hard gate (dev deps reported only), bandit HIGH fails (B104 bind-all skipped: intended behind Cloudflare/Caddy), pip-licenses uploaded as a report. Weekly + on dependency/source changes. Running it locally found 29 advisories, all in pinned-and-forgotten runtime deps: starlette 0.27 (7, incl. Host-header path confusion and form DoS), fastapi 0.104, requests 2.31 (3), pymysql 1.1. Bumped to current: fastapi 0.141.1 / starlette 1.6.0, pydantic 2.13.5, uvicorn 0.52.4, requests 2.34.2, pymysql 1.2.0; dev: pytest 9.1.1, black 26.5.1. pip-audit is now clean. requires-python narrowed to 3.11 (the truth: psycopg2-binary 2.9.9 fails on 3.13; pandas 2.0.3 has no 3.12 wheels). Full suite passes; API smoke-tested (health, stations, forecast, history, stats, docs, openapi) on the new stack. black 26 reformatted 8 files.
This commit is contained in:
@@ -38,7 +38,7 @@ jobs:
|
||||
- name: Install tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore black==23.11.0 isort==5.12.0 flake8==6.1.0
|
||||
pip install --root-user-action=ignore black==26.5.1 isort==5.12.0 flake8==6.1.0
|
||||
|
||||
- name: black
|
||||
run: black --check --diff src/ *.py
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore -r requirements.txt
|
||||
pip install --root-user-action=ignore pytest==7.4.3 pytest-asyncio==0.21.1
|
||||
pip install --root-user-action=ignore pytest==9.1.1 pytest-asyncio==0.21.1
|
||||
|
||||
- name: pytest
|
||||
env:
|
||||
|
||||
+65
-254
@@ -1,293 +1,104 @@
|
||||
name: Security & Dependency Updates
|
||||
name: Security
|
||||
|
||||
# Two gates that can actually fail, plus one report:
|
||||
# - pip-audit against requirements.txt: any known vulnerability in a runtime
|
||||
# dependency fails the job (dev-only tools are reported, not gated)
|
||||
# - bandit on src/: HIGH severity findings fail; medium/low are listed.
|
||||
# B104 (bind 0.0.0.0) is skipped: the service is meant to listen on all
|
||||
# interfaces behind Cloudflare/Caddy.
|
||||
# - pip-licenses report as an artifact (informational; the project is MIT
|
||||
# and its runtime deps are MIT/BSD/Apache/PSF)
|
||||
# The old file ran safety/bandit/semgrep with `|| true` and could not go red.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run security scans daily at 3 AM UTC
|
||||
- cron: "0 3 * * *"
|
||||
- cron: "0 3 * * 1" # weekly, Monday 03:00 UTC
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "requirements*.txt"
|
||||
- "Dockerfile"
|
||||
- "pyproject.toml"
|
||||
- "uv.lock"
|
||||
- "src/**/*.py"
|
||||
- ".gitea/workflows/security.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "requirements*.txt"
|
||||
- "pyproject.toml"
|
||||
- "src/**/*.py"
|
||||
|
||||
env:
|
||||
PYTHON_VERSION: "3.11"
|
||||
# GitHub token for better rate limits and authentication
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
|
||||
jobs:
|
||||
# Dependency vulnerability scan
|
||||
dependency-scan:
|
||||
name: Dependency Security Scan
|
||||
dependencies:
|
||||
name: Dependency vulnerabilities
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install dependencies
|
||||
- name: Install pip-audit
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore safety bandit semgrep
|
||||
pip install --root-user-action=ignore pip-audit
|
||||
|
||||
- name: Run Safety check
|
||||
run: |
|
||||
safety check -r requirements.txt --json --output safety-report.json || true
|
||||
safety check -r requirements-dev.txt --json --output safety-dev-report.json || true
|
||||
- name: Runtime dependencies (gate)
|
||||
run: pip-audit -r requirements.txt --strict --desc on
|
||||
|
||||
- name: Run Bandit security scan
|
||||
run: |
|
||||
bandit -r src/ -f json -o bandit-report.json || true
|
||||
- name: Dev dependencies (report only)
|
||||
run: pip-audit -r requirements-dev.txt --desc on || echo "::warning::dev-only dependency advisories above"
|
||||
|
||||
- name: Run Semgrep security scan
|
||||
run: |
|
||||
semgrep --config=auto src/ --json --output=semgrep-report.json || true
|
||||
|
||||
- name: Upload security reports
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: security-reports-${{ github.run_number }}
|
||||
path: |
|
||||
safety-report.json
|
||||
safety-dev-report.json
|
||||
bandit-report.json
|
||||
semgrep-report.json
|
||||
|
||||
- name: Check for critical vulnerabilities
|
||||
run: |
|
||||
echo "Checking for critical vulnerabilities..."
|
||||
|
||||
# Check Safety results
|
||||
if [ -f safety-report.json ]; then
|
||||
critical_count=$(jq '.vulnerabilities | length' safety-report.json 2>/dev/null || echo "0")
|
||||
if [ "$critical_count" -gt 0 ]; then
|
||||
echo "Found $critical_count dependency vulnerabilities"
|
||||
jq '.vulnerabilities[] | "- \(.package_name) \(.installed_version): \(.vulnerability_id)"' safety-report.json
|
||||
else
|
||||
echo "No dependency vulnerabilities found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check Bandit results
|
||||
if [ -f bandit-report.json ]; then
|
||||
high_severity=$(jq '.results[] | select(.issue_severity == "HIGH") | length' bandit-report.json 2>/dev/null | wc -l)
|
||||
if [ "$high_severity" -gt 0 ]; then
|
||||
echo "Found $high_severity high-severity security issues"
|
||||
else
|
||||
echo "No high-severity security issues found"
|
||||
fi
|
||||
fi
|
||||
|
||||
# License compliance check
|
||||
license-check:
|
||||
name: License Compliance
|
||||
code:
|
||||
name: Static analysis
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install pip-licenses
|
||||
- name: Install bandit
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore pip-licenses
|
||||
pip install --root-user-action=ignore -r requirements.txt
|
||||
pip install --root-user-action=ignore bandit
|
||||
|
||||
- name: Check licenses
|
||||
- name: bandit (HIGH fails; medium/low listed)
|
||||
run: |
|
||||
echo "Checking dependency licenses..."
|
||||
bandit -r src/ -q --skip B104 -ll -ii || true
|
||||
bandit -r src/ -q --skip B104 --severity-level high --confidence-level medium
|
||||
|
||||
licenses:
|
||||
name: License report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: pip
|
||||
cache-dependency-path: requirements.txt
|
||||
|
||||
- name: Install
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore -r requirements.txt pip-licenses
|
||||
|
||||
- name: Report
|
||||
run: |
|
||||
pip-licenses --format=markdown --with-urls --output-file=licenses.md
|
||||
pip-licenses --format=json --output-file=licenses.json
|
||||
pip-licenses --format=markdown --output-file=licenses.md
|
||||
echo "Copyleft licenses among runtime deps (informational):"
|
||||
pip-licenses --format=plain | grep -iE 'GPL|AGPL|LGPL' || echo " none"
|
||||
|
||||
# Check for problematic licenses
|
||||
problematic_licenses=("GPL" "AGPL" "LGPL")
|
||||
|
||||
for license in "${problematic_licenses[@]}"; do
|
||||
if grep -i "$license" licenses.json; then
|
||||
echo "Found potentially problematic license: $license"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "License check completed"
|
||||
|
||||
- name: Upload license report
|
||||
uses: actions/upload-artifact@v3
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: license-report-${{ github.run_number }}
|
||||
name: licenses-${{ github.run_number }}
|
||||
path: |
|
||||
licenses.json
|
||||
licenses.md
|
||||
|
||||
# Dependency update check
|
||||
dependency-update:
|
||||
name: Check for Dependency Updates
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install pip-check-updates equivalent
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore pip-review
|
||||
|
||||
- name: Check for outdated packages
|
||||
run: |
|
||||
echo "Checking for outdated packages..."
|
||||
pip install --root-user-action=ignore -r requirements.txt
|
||||
pip list --outdated --format=json > outdated-packages.json || true
|
||||
|
||||
if [ -s outdated-packages.json ]; then
|
||||
echo "Outdated packages found:"
|
||||
cat outdated-packages.json | jq -r '.[] | "- \(.name): \(.version) -> \(.latest_version)"'
|
||||
else
|
||||
echo "All packages are up to date"
|
||||
fi
|
||||
|
||||
- name: Upload dependency reports
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dependency-reports-${{ github.run_number }}
|
||||
path: |
|
||||
outdated-packages.json
|
||||
|
||||
# Code quality metrics
|
||||
code-quality:
|
||||
name: Code Quality Metrics
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.GITEA_TOKEN }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install quality tools
|
||||
run: |
|
||||
python -m pip install --upgrade pip --root-user-action=ignore
|
||||
pip install --root-user-action=ignore radon xenon vulture
|
||||
pip install --root-user-action=ignore -r requirements.txt
|
||||
|
||||
- name: Calculate code complexity
|
||||
run: |
|
||||
echo "Calculating code complexity..."
|
||||
radon cc src/ --json > complexity-report.json
|
||||
radon mi src/ --json > maintainability-report.json
|
||||
|
||||
echo "Complexity Summary:"
|
||||
radon cc src/ --average
|
||||
|
||||
echo "Maintainability Summary:"
|
||||
radon mi src/
|
||||
|
||||
- name: Find dead code
|
||||
run: |
|
||||
echo "Checking for dead code..."
|
||||
vulture src/ --json > dead-code-report.json || true
|
||||
|
||||
- name: Check for code smells
|
||||
run: |
|
||||
echo "Checking for code smells..."
|
||||
xenon --max-absolute B --max-modules A --max-average A src/ || true
|
||||
|
||||
- name: Upload quality reports
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: code-quality-reports-${{ github.run_number }}
|
||||
path: |
|
||||
complexity-report.json
|
||||
maintainability-report.json
|
||||
dead-code-report.json
|
||||
|
||||
# Security summary
|
||||
security-summary:
|
||||
name: Security Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [dependency-scan, license-check, code-quality]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
||||
- name: Generate security summary
|
||||
run: |
|
||||
echo "# Security Scan Summary" > security-summary.md
|
||||
echo "" >> security-summary.md
|
||||
echo "**Scan Date:** $(date -u)" >> security-summary.md
|
||||
echo "**Repository:** ${{ github.repository }}" >> security-summary.md
|
||||
echo "**Commit:** ${{ github.sha }}" >> security-summary.md
|
||||
echo "" >> security-summary.md
|
||||
|
||||
echo "## Results" >> security-summary.md
|
||||
echo "" >> security-summary.md
|
||||
|
||||
# Dependency scan results
|
||||
if [ -f security-reports-*/safety-report.json ]; then
|
||||
vuln_count=$(jq '.vulnerabilities | length' security-reports-*/safety-report.json 2>/dev/null || echo "0")
|
||||
if [ "$vuln_count" -eq 0 ]; then
|
||||
echo "- Dependency Scan: No vulnerabilities found" >> security-summary.md
|
||||
else
|
||||
echo "- Dependency Scan: $vuln_count vulnerabilities found" >> security-summary.md
|
||||
fi
|
||||
else
|
||||
echo "- Dependency Scan: Results not available" >> security-summary.md
|
||||
fi
|
||||
|
||||
# Docker scan results (removed Trivy)
|
||||
echo "- Docker Scan: Skipped (Trivy removed)" >> security-summary.md
|
||||
|
||||
# License check results
|
||||
if [ -f license-report-*/licenses.json ]; then
|
||||
echo "- License Check: Completed" >> security-summary.md
|
||||
else
|
||||
echo "- License Check: Results not available" >> security-summary.md
|
||||
fi
|
||||
|
||||
# Code quality results
|
||||
if [ -f code-quality-reports-*/complexity-report.json ]; then
|
||||
echo "- Code Quality: Analyzed" >> security-summary.md
|
||||
else
|
||||
echo "- Code Quality: Results not available" >> security-summary.md
|
||||
fi
|
||||
|
||||
echo "" >> security-summary.md
|
||||
echo "## Detailed Reports" >> security-summary.md
|
||||
echo "" >> security-summary.md
|
||||
echo "Detailed reports are available in the workflow artifacts." >> security-summary.md
|
||||
|
||||
cat security-summary.md
|
||||
|
||||
- name: Upload security summary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: security-summary-${{ github.run_number }}
|
||||
path: security-summary.md
|
||||
licenses.json
|
||||
|
||||
Reference in New Issue
Block a user