From 938067560d7265650ff7e81827e41701213eae26 Mon Sep 17 00:00:00 2001 From: Florian Date: Wed, 21 Oct 2026 12:11:30 +0200 Subject: [PATCH] Added security scanning and mail delivery --- .gitea/workflows/pip-audit.yaml | 78 +++++++++++++++++++++++++++++++++ scripts/filter_requirements.py | 37 ++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 .gitea/workflows/pip-audit.yaml create mode 100644 scripts/filter_requirements.py diff --git a/.gitea/workflows/pip-audit.yaml b/.gitea/workflows/pip-audit.yaml new file mode 100644 index 0000000..b131169 --- /dev/null +++ b/.gitea/workflows/pip-audit.yaml @@ -0,0 +1,78 @@ +name: pip-audit +on: + schedule: + - cron: "35 1 * * *" + push: + branches: [main] + paths: [requirements.txt, scripts/filter_requirements.py] + workflow_dispatch: + +jobs: + audit: + runs-on: ubuntu-latest + outputs: + report_b64: ${{ steps.audit.outputs.report_b64 }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Audit dependencies + id: audit + shell: bash + run: | + pip install pip-audit + python3 scripts/filter_requirements.py requirements.txt requirements.audit.txt + + # Don't let a non-zero exit abort the step, so the report is saved first + set +e + pip-audit -r requirements.audit.txt --no-deps --disable-pip > audit.txt 2>&1 + echo $? > audit.exit + set -e + + cat audit.txt + echo "report_b64=$(head -c 100000 audit.txt | base64 -w0)" >> "$GITHUB_OUTPUT" + + - name: Fail if the audit failed + shell: bash + run: exit "$(cat audit.exit)" + + notify: + needs: audit + if: ${{ always() && needs.audit.result == 'failure' }} + runs-on: host + steps: + - name: Send mail via local postfix + env: + MAIL_FROM: ${{ vars.MAIL_FROM }} + MAIL_TO: ${{ vars.MAIL_TO }} + REPO: ${{ gitea.repository }} + RUN_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_number }} + REPORT_B64: ${{ needs.audit.outputs.report_b64 }} + run: | + python3 - <<'EOF' + import base64, os, smtplib + from email.message import EmailMessage + + b64 = os.environ.get("REPORT_B64", "") + report = ( + base64.b64decode(b64).decode("utf-8", errors="replace") + if b64 else + "(no audit output, the job failed before pip-audit finished)" + ) + + msg = EmailMessage() + msg["Subject"] = f"[pip-audit] Problem in {os.environ['REPO']}" + msg["From"] = os.environ["MAIL_FROM"] + msg["To"] = os.environ["MAIL_TO"] + msg.set_content( + f"pip-audit result for {os.environ['REPO']}:\n\n" + f"{report}\n\n" + f"Run: {os.environ['RUN_URL']}\n" + ) + + with smtplib.SMTP("127.0.0.1", 25, timeout=30) as s: + s.send_message(msg) + EOF diff --git a/scripts/filter_requirements.py b/scripts/filter_requirements.py new file mode 100644 index 0000000..3ed444f --- /dev/null +++ b/scripts/filter_requirements.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +"""Write a copy of requirements.txt without the self-hosted packages. + +Usage: filter_requirements.py +""" +import re +import sys + +# Self-hosted packages that are not on PyPI. One per line. +SKIP = { + "lib-db-module", + "lib-secret-manager", + "lib-send-notifications", + "simple-logger-handler", +} + +NAME_RE = re.compile(r"^\s*([A-Za-z0-9][A-Za-z0-9._-]*)") + + +def main(src: str, dst: str) -> None: + skipped = [] + + with open(src) as fin, open(dst, "w") as fout: + for line in fin: + match = NAME_RE.match(line) + if match and match.group(1) in SKIP: + skipped.append(match.group(1)) + continue + fout.write(line) + + print(f"Skipped {len(skipped)} self-hosted package(s): {', '.join(skipped) or '-'}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + sys.exit(__doc__) + main(sys.argv[1], sys.argv[2]) \ No newline at end of file