Postgress connection standardization #400
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Security Scan | |
| # Standalone, independent workflow. It can go red on its own without affecting | |
| # the Unit Tests / Integration Tests / build workflows (they are separate | |
| # workflows and never depend on this one). A red result here does NOT block | |
| # merge unless this check is explicitly added to branch protection's required | |
| # status checks - by default it is not. | |
| on: | |
| pull_request: | |
| branches: [ main ] | |
| push: | |
| branches: [ main ] | |
| permissions: | |
| contents: read | |
| jobs: | |
| security-scan: | |
| runs-on: ubuntu-latest | |
| # Surface findings IN the pull request so they are reviewable before merge: | |
| # Bandit -> code scanning annotations on the PR diff + a checks entry; | |
| # pip-audit -> a job summary on the run (linked from the PR checks). | |
| # NOTE: on a public repo these PR surfaces are visible to anyone who can | |
| # see the PR, not only maintainers. | |
| permissions: | |
| security-events: write | |
| actions: read | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: '3.11' | |
| - name: Install scanners | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install "bandit[sarif]" | |
| # Gate - high severity + high confidence fails this job (red check). | |
| - name: Bandit SAST gate (fails on high-severity, high-confidence findings) | |
| run: | | |
| bandit -r . \ | |
| -x ./test,./.venv,./.venv-windows,./screenshot \ | |
| --severity-level high --confidence-level high | |
| # medium+ report -> SARIF (for PR annotations + Security tab) and JSON (summary). | |
| - name: Bandit report (SARIF + JSON, non-blocking) | |
| if: always() | |
| run: | | |
| bandit -r . \ | |
| -x ./test,./.venv,./.venv-windows,./screenshot \ | |
| --severity-level medium --confidence-level medium \ | |
| -f sarif -o bandit.sarif --exit-zero | |
| bandit -r . \ | |
| -x ./test,./.venv,./.venv-windows,./screenshot \ | |
| --severity-level medium --confidence-level medium \ | |
| -f json -o bandit.json --exit-zero | |
| # Upload on BOTH push and pull_request so code scanning annotations appear | |
| # inline on the PR diff before merge. category keeps Bandit separate from CodeQL. | |
| - name: Upload Bandit SARIF to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: bandit.sarif | |
| category: bandit | |
| # Counts on the run summary, linked from the PR checks. | |
| - name: Bandit job summary | |
| if: always() | |
| run: | | |
| python - <<'PY' | |
| import json, os, collections | |
| try: | |
| data = json.load(open("bandit.json")) | |
| except FileNotFoundError: | |
| data = {"results": []} | |
| results = data.get("results", []) | |
| sev = collections.Counter(r["issue_severity"] for r in results) | |
| conf = collections.Counter(r["issue_confidence"] for r in results) | |
| lines = ["## Bandit SAST (medium+ report)", "", | |
| "Total findings: %d" % len(results), "", | |
| "| Level | Severity count | Confidence count |", "|---|---|---|"] | |
| for lvl in ("HIGH", "MEDIUM", "LOW"): | |
| lines.append("| %s | %d | %d |" % (lvl, sev.get(lvl, 0), conf.get(lvl, 0))) | |
| lines += ["", "Inline findings annotate the PR diff; full list: Security tab -> Code scanning -> bandit."] | |
| with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as f: | |
| f.write("\n".join(lines) + "\n") | |
| PY | |
| # Downloadable full reports. | |
| - name: Upload Bandit reports as artifacts | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: bandit-reports | |
| path: | | |
| bandit.sarif | |
| bandit.json | |
| retention-days: 7 | |
| # Dependency CVEs. The official action renders a job summary (visible from | |
| # the PR checks) and FAILS this job on any CVE (red check). It does NOT block | |
| # merge unless this check is added to branch protection's required checks. | |
| # Single accepted-CVE allowlist: one id per line, the text after '#' is the | |
| # reason. Here '#' is a real shell comment, so the reason is stripped and only | |
| # the bare id reaches pip-audit (the action's ignore-vulns is whitespace-split | |
| # and cannot carry comments itself). Edit THIS list to accept/remove a CVE. | |
| - name: Assemble pip-audit CVE allowlist | |
| id: cve_allowlist | |
| if: always() | |
| run: | | |
| ids="" | |
| accept() { ids="$ids $1"; } | |
| accept PYSEC-2025-217 # transformers X-CLIP checkpoint-conversion RCE - unreachable (no X-CLIP / checkpoint conversion) | |
| accept CVE-2026-1839 # transformers Trainer torch.load RCE - unreachable (no Trainer); fix only in 5.0.0rc3 (major RC) | |
| accept CVE-2026-4372 # transformers config _attn_implementation_internal RCE - unreachable (no AutoModelForCausalLM, only trusted bundled models, optional 'kernels' pkg not installed); fix only in 5.x major | |
| accept PYSEC-2026-113 # pyarrow C++ Use-After-Free - Python binding is NOT vulnerable per the advisory | |
| accept PYSEC-2026-169 # distributed (Dask) dashboard XSS - needs Jupyter Lab + jupyter-server-proxy + a Dask cluster; none run here | |
| accept PYSEC-2026-2290 # transformers LightGlue trust_remote_code override RCE - unreachable (no LightGlue, no untrusted model repos, only trusted bundled models); fix only in 5.x major | |
| echo "ids=$(echo $ids | xargs)" >> "$GITHUB_OUTPUT" | |
| # Shared deps - full resolution, so transitive CVEs are caught too. | |
| - name: pip-audit dependency CVEs (common) | |
| if: always() | |
| uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0 | |
| with: | |
| inputs: requirements/common.txt | |
| ignore-vulns: ${{ steps.cve_allowlist.outputs.ids }} | |
| # CPU + NVIDIA GPU extras (onnxruntime / onnxruntime-gpu / cupy / cuml plus | |
| # the deps cuml/RAPIDS resolves in). The noavx2 variant is intentionally NOT audited. | |
| - name: pip-audit dependency CVEs (cpu + nvidia gpu) | |
| if: always() | |
| uses: pypa/gh-action-pip-audit@1220774d901786e6f652ae159f7b6bc8fea6d266 # v1.1.0 | |
| with: | |
| no-deps: true | |
| inputs: | | |
| requirements/cpu.txt | |
| requirements/gpu.txt | |
| ignore-vulns: ${{ steps.cve_allowlist.outputs.ids }} | |
| # Image / supply-chain layer: Trivy scans the container DEFINITION (Dockerfile | |
| # + deployment manifests) for misconfigurations and the working tree for leaked | |
| # secrets -- coverage pip-audit (Python CVEs) and Bandit (Python SAST) do not | |
| # provide. The config scan (Dockerfile/manifest misconfig) is report-only | |
| # (exit-code 0); the filesystem scan GATES (exit-code 1) so HIGH/CRITICAL | |
| # dependency CVEs or leaked secrets turn this check red, matching pip-audit's | |
| # fail-on-CVE behaviour. A red check does NOT block merge unless this workflow | |
| # is added to branch protection's required checks. Findings are uploaded as | |
| # SARIF to code scanning so the detailed table lives in the private Security | |
| # tab (write/maintain/admin only) instead of the public Actions run log. A | |
| # full built-image OS scan can later be hooked into the image-publish pipeline. | |
| trivy: | |
| runs-on: ubuntu-latest | |
| # security-events:write lets the SARIF upload publish to the Security tab. | |
| permissions: | |
| contents: read | |
| security-events: write | |
| actions: read | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install Trivy | |
| run: | | |
| curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh \ | |
| | sh -s -- -b /usr/local/bin | |
| # Each scan emits native JSON once: it feeds both a numeric recap on the run | |
| # summary (0 shown when clean) and, via `trivy convert`, the SARIF uploaded | |
| # to the Security tab -- one scan, no second pass over the vuln DB. | |
| # Accepted / not-applicable findings are listed in .trivyignore (the Trivy | |
| # counterpart of the pip-audit allowlist above). Config scan is report-only. | |
| - name: Trivy config scan (Dockerfiles + deployment manifests) | |
| if: always() | |
| run: | | |
| trivy config --severity HIGH,CRITICAL --exit-code 0 \ | |
| --ignorefile .trivyignore \ | |
| --format json --output trivy-config.json . | |
| - name: Convert Trivy config report to SARIF | |
| if: always() | |
| run: trivy convert --format sarif --output trivy-config.sarif trivy-config.json | |
| - name: Upload Trivy config SARIF to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: trivy-config.sarif | |
| category: trivy-config | |
| # Filesystem scan GATES on HIGH/CRITICAL. --file-patterns adds the shipped | |
| # requirements/*.txt to the pip vuln scan (Trivy's pip scanner otherwise only | |
| # reads files named exactly requirements.txt); the -noavx2 legacy stacks and | |
| # the dev-only test/requirements.txt are skipped. The detailed finding table | |
| # reaches the Security tab via the converted SARIF (kept out of the public run | |
| # log); only the counts are public. The step still exits non-zero on findings | |
| # so the check goes red. | |
| - name: Trivy filesystem scan (dependency CVEs + leaked secrets, fails on HIGH/CRITICAL) | |
| run: | | |
| trivy fs --scanners vuln,secret --severity HIGH,CRITICAL --exit-code 1 \ | |
| --ignorefile .trivyignore \ | |
| --file-patterns "pip:requirements/.*\.txt" \ | |
| --skip-files requirements/common-noavx2.txt,requirements/cpu-noavx2.txt,test/requirements.txt \ | |
| --format json --output trivy-fs.json \ | |
| --skip-dirs .venv,.venv-windows,native-build,test/songs,model,dist . | |
| - name: Convert Trivy filesystem report to SARIF | |
| if: always() | |
| run: trivy convert --format sarif --output trivy-fs.sarif trivy-fs.json | |
| - name: Upload Trivy filesystem SARIF to code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: trivy-fs.sarif | |
| category: trivy-fs | |
| # Counts on the run summary (0 when clean), mirroring the Bandit summary. | |
| - name: Trivy job summary | |
| if: always() | |
| run: | | |
| python3 - <<'PY' | |
| import json, os | |
| def load(path): | |
| try: | |
| with open(path) as fh: | |
| return json.load(fh) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| return {} | |
| def tally(report, key): | |
| total = high = crit = 0 | |
| for res in (report.get("Results") or []): | |
| for item in (res.get(key) or []): | |
| total += 1 | |
| sev = (item.get("Severity") or "").upper() | |
| if sev == "HIGH": | |
| high += 1 | |
| elif sev == "CRITICAL": | |
| crit += 1 | |
| return total, high, crit | |
| cfg = load("trivy-config.json") | |
| fs = load("trivy-fs.json") | |
| rows = [ | |
| ("Config misconfig (Dockerfiles / manifests)",) + tally(cfg, "Misconfigurations"), | |
| ("Dependency CVEs (filesystem)",) + tally(fs, "Vulnerabilities"), | |
| ("Leaked secrets (filesystem)",) + tally(fs, "Secrets"), | |
| ] | |
| total = sum(r[1] for r in rows) | |
| lines = ["## Trivy (HIGH / CRITICAL)", "", | |
| "Total findings: %d" % total, "", | |
| "| Scan | Findings | HIGH | CRITICAL |", "|---|---|---|---|"] | |
| for name, t, h, c in rows: | |
| lines.append("| %s | %d | %d | %d |" % (name, t, h, c)) | |
| lines += ["", "Detail for any non-zero row: Security tab -> Code scanning -> trivy-config / trivy-fs."] | |
| with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as fh: | |
| fh.write("\n".join(lines) + "\n") | |
| PY |