Zion Boggan zionboggan.com ↗

security pipeline: lint/sast/secrets/deps/test + soc notifier

9795a86   Zion Boggan committed on Apr 19, 2026 (2 months ago)
.github/workflows/security.yml +93 -0
@@ -0,0 +1,93 @@
+name: security-pipeline
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ workflow_dispatch:
+
+permissions:
+ contents: read
+ security-events: write
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install ruff==0.6.9
+ - run: ruff check .
+
+ sast:
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v4
+ - uses: returntocorp/semgrep-action@v1
+ with:
+ config: >-
+ p/default
+ p/python
+ p/flask
+ .semgrep/rules.yml
+ generateSarif: "1"
+ - uses: github/codeql-action/upload-sarif@v3
+ if: always()
+ with:
+ sarif_file: semgrep.sarif
+ category: semgrep
+
+ secrets:
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: gitleaks/gitleaks-action@v2
+ env:
+ GITLEAKS_CONFIG: .gitleaks.toml
+
+ dependencies:
+ runs-on: ubuntu-latest
+ needs: lint
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install pip-audit==2.7.3
+ - run: pip-audit -r requirements.txt --strict --desc
+
+ test:
+ runs-on: ubuntu-latest
+ needs: [sast, secrets, dependencies]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: pip install -r requirements-dev.txt
+ - run: pytest --cov=app --cov-report=term-missing
+
+ notify-soc:
+ runs-on: ubuntu-latest
+ needs: [test]
+ if: always()
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-python@v5
+ with:
+ python-version: "3.11"
+ - run: python scripts/notify_soc.py
+ env:
+ SHUFFLE_WEBHOOK_URL: ${{ secrets.SHUFFLE_WEBHOOK_URL }}
+ PIPELINE_STATUS: ${{ needs.test.result }}
+ REPO: ${{ github.repository }}
+ RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
+ COMMIT: ${{ github.sha }}
+ ACTOR: ${{ github.actor }}
Makefile +21 -0
@@ -0,0 +1,21 @@
+.PHONY: install lint sast secrets deps test all
+
+install:
+ pip install -r requirements-dev.txt
+
+lint:
+ ruff check .
+
+sast:
+ semgrep --config p/default --config p/python --config p/flask --config .semgrep/rules.yml --error
+
+secrets:
+ gitleaks detect --config .gitleaks.toml --no-banner
+
+deps:
+ pip-audit -r requirements.txt --strict --desc
+
+test:
+ pytest --cov=app --cov-report=term-missing
+
+all: lint sast secrets deps test
requirements-dev.txt +6 -0
@@ -0,0 +1,6 @@
+-r requirements.txt
+pytest==8.3.3
+pytest-cov==5.0.0
+bandit==1.7.10
+pip-audit==2.7.3
+ruff==0.6.9
scripts/notify_soc.py +47 -0
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+import json
+import os
+import sys
+from urllib import error, request
+
+TIMEOUT = 10
+
+
+def build_event():
+ status = os.environ.get("PIPELINE_STATUS", "unknown")
+ return {
+ "source": "github-actions",
+ "pipeline": "security-pipeline",
+ "status": status,
+ "outcome": "success" if status == "success" else "failure",
+ "repository": os.environ.get("REPO"),
+ "commit": os.environ.get("COMMIT"),
+ "actor": os.environ.get("ACTOR"),
+ "run_url": os.environ.get("RUN_URL"),
+ }
+
+
+def main():
+ hook = os.environ.get("SHUFFLE_WEBHOOK_URL")
+ if not hook:
+ print("SHUFFLE_WEBHOOK_URL not set, skipping SOC notification")
+ return 0
+ if not hook.lower().startswith(("https://", "http://")):
+ print("SHUFFLE_WEBHOOK_URL must be an http(s) URL", file=sys.stderr)
+ return 1
+ event = build_event()
+ body = json.dumps(event).encode("utf-8")
+ req = request.Request(
+ hook, data=body, headers={"Content-Type": "application/json"}, method="POST"
+ )
+ try:
+ with request.urlopen(req, timeout=TIMEOUT) as resp:
+ print(f"SOC webhook responded {resp.status}")
+ except (error.URLError, error.HTTPError) as exc:
+ print(f"failed to reach SOC webhook: {exc}", file=sys.stderr)
+ return 0
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())