diga: consolidate compliance matrix to single canonical artefact #19
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 | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| schedule: | |
| # Wöchentlicher Lauf, Montag 06:00 UTC — fängt zwischen-Release-Drift in | |
| # Geheimnissen, TLS-Posture und HTTP-Headern auf. | |
| - cron: "0 6 * * 1" | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| security-events: write # für SARIF-Uploads (gitleaks) | |
| pull-requests: read | |
| jobs: | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 1. Geheimnis-Scan — Quellcode + gesamte Git-History | |
| # Schließt: TR-03161 O.Cryp_1, O.Source_8 | |
| # ────────────────────────────────────────────────────────────────────── | |
| secrets: | |
| name: Secret Scanning (gitleaks) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # vollständige History scannen | |
| - name: Run gitleaks | |
| uses: gitleaks/gitleaks-action@v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 2. Markdown-Lint — Konsistenz der diga/-Dokumente | |
| # Verhindert die Lint-Warnungen, die der IDE-Linter heute gemeldet hat. | |
| # ────────────────────────────────────────────────────────────────────── | |
| markdown-lint: | |
| name: Markdown Lint (diga/) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run markdownlint-cli2 | |
| uses: DavidAnson/markdownlint-cli2-action@v18 | |
| with: | |
| globs: | | |
| diga/**/*.md | |
| !diga/regulations/markdown/** | |
| config: '.markdownlint.json' | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 3. Link-Check über alle Markdown-Dokumente in diga/ | |
| # Stellt sicher, dass Quellenverzeichnisse (BSI/BfArM/Gesetze) | |
| # erreichbar bleiben. | |
| # ────────────────────────────────────────────────────────────────────── | |
| link-check: | |
| name: Link Check (lychee) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Run lychee | |
| uses: lycheeverse/lychee-action@v2 | |
| with: | |
| args: >- | |
| --verbose | |
| --no-progress | |
| --max-concurrency 4 | |
| --timeout 30 | |
| --retry-wait-time 5 | |
| --max-retries 2 | |
| --exclude-path diga/regulations/markdown | |
| --exclude '^https?://(www\.)?linkedin\.com' | |
| --exclude '^https?://localhost' | |
| --exclude '^https?://twobreath\.com' | |
| 'diga/**/*.md' | |
| 'README.md' | |
| fail: false # Linkfehler blockieren keinen Build, sind nur Warnungen | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 4. Dependency Review — sobald in einem PR ein Lock-File geändert wird | |
| # Hinweis: dieser Job läuft NUR auf pull_request-Events. Auf push und | |
| # workflow_dispatch wird er korrekt als "skipped" angezeigt — die | |
| # Action benötigt eine Base-/Head-Diff aus dem PR-Kontext und kann | |
| # außerhalb dessen nicht funktionieren. Für Push/Cron-Scans siehe | |
| # den `osv-scan` Job darunter. | |
| # ────────────────────────────────────────────────────────────────────── | |
| dependency-review: | |
| name: Dependency Review | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Dependency review | |
| uses: actions/dependency-review-action@v4 | |
| with: | |
| fail-on-severity: high | |
| comment-summary-in-pr: on-failure | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 4b. OSV-Scanner — Manifest-basierter CVE-Scan auf push + PR + cron | |
| # Schließt: TR-03161 O.TrdP_3 (regelmäßiger Schwachstellen-Check | |
| # auf eingesetzte Drittanbieter-Software). | |
| # Aktuell: dieses Repo enthält noch kein package.json / Lockfile; | |
| # der Scan produziert eine leere Baseline-SARIF. Sobald Manifeste | |
| # einziehen (Build-Toolchain, MCP-Server, etc.), schlägt der Scan | |
| # ohne weiteren Konfigurationsaufwand an. | |
| # ────────────────────────────────────────────────────────────────────── | |
| osv-scan: | |
| name: OSV Scanner (manifest-CVE) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install osv-scanner from upstream GitHub release | |
| env: | |
| OSV_VERSION: "2.3.6" # pin; bump in PR after upstream release | |
| run: | | |
| set -euo pipefail | |
| url="https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/osv-scanner_linux_amd64" | |
| echo "Downloading osv-scanner v${OSV_VERSION}…" | |
| curl -sSLf -o osv-scanner "$url" | |
| chmod +x osv-scanner | |
| sudo mv osv-scanner /usr/local/bin/ | |
| osv-scanner --version | |
| - name: Run osv-scanner (recursive, manifest-based) | |
| run: | | |
| set -uo pipefail | |
| # exit-codes: 0 = no vulnerable, 1 = vulnerable found, 127 = misuse, | |
| # 128 = generic error. Bei leerer Manifest-Lage exit 128 ohne Treffer — | |
| # wir wollen den Job nicht hart auf 0 zwingen, sondern nur dann scharf | |
| # stellen, wenn echte Vulns gefunden werden. Heute (mit nur einem | |
| # devDependency) liefert der Scan eine reale, saubere SARIF. | |
| # v2-CLI: explizit `scan source` als Subkommando. | |
| # `--skip-git` aus v1 entfällt — v2 scannt Manifeste per default, | |
| # nicht den Git-Index. | |
| osv-scanner scan source \ | |
| --recursive \ | |
| --format=sarif \ | |
| --output=osv.sarif \ | |
| ./ || rc=$? | |
| rc=${rc:-0} | |
| echo "osv-scanner exit code: $rc" | |
| if [ -f osv.sarif ]; then | |
| ls -la osv.sarif | |
| # Anzahl gefundener Treffer in den Job-Log | |
| jq -r '.runs[]?.results | length' osv.sarif | head -3 | xargs -I{} echo "SARIF results count: {}" | |
| else | |
| echo "::warning::osv-scanner produced no SARIF (likely no manifests detected — that's OK for a first run)" | |
| fi | |
| # Job nur dann hart fail, wenn echte Vulns gefunden (exit 1) | |
| if [ "$rc" = "1" ]; then exit 1; fi | |
| - name: Upload SARIF to GitHub code scanning | |
| if: always() | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: osv.sarif | |
| category: osv-scanner | |
| continue-on-error: true # falls keine SARIF erzeugt — soll nicht hart failen | |
| - name: Upload SARIF as artefact | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: osv-scanner-report | |
| path: osv.sarif | |
| retention-days: 90 | |
| if-no-files-found: ignore | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 4c. Cross-Repo OSV-Scanner — privates ma3u/TwoBreath-app | |
| # Voraussetzung: Repository-Secret `APP_REPO_TOKEN` mit fine-grained | |
| # PAT, Read-Berechtigung auf `ma3u/TwoBreath-app/Contents`. Ohne | |
| # Secret skippt der Job ohne Fehler (Soft-Skip via Step-Output). | |
| # | |
| # Privacy-Hinweis: SARIF-Upload landet im PUBLIC Code-Scanning-Tab | |
| # dieses Repos — Vulnerability-Findings über die App werden damit | |
| # öffentlich sichtbar. Das ist im Rahmen des öffentlichen DiGA- | |
| # Audit-Trail-Charakters dieses Repos akzeptiert. | |
| # ────────────────────────────────────────────────────────────────────── | |
| osv-scan-app: | |
| name: OSV Scanner (private app repo) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| security-events: write | |
| if: github.event_name != 'pull_request' # cross-repo nur auf push/cron/dispatch | |
| steps: | |
| - name: Check token availability | |
| id: have-token | |
| run: | | |
| if [ -n "${{ secrets.APP_REPO_TOKEN }}" ]; then | |
| echo "ok=true" >> "$GITHUB_OUTPUT" | |
| echo "Token present — proceeding." | |
| else | |
| echo "ok=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::APP_REPO_TOKEN secret not set — cross-repo scan skipped. Add a fine-grained PAT with Contents:Read on ma3u/TwoBreath-app to enable." | |
| fi | |
| - name: Checkout private TwoBreath-app | |
| if: steps.have-token.outputs.ok == 'true' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: ma3u/TwoBreath-app | |
| token: ${{ secrets.APP_REPO_TOKEN }} | |
| path: app-src | |
| fetch-depth: 1 | |
| - name: Install osv-scanner | |
| if: steps.have-token.outputs.ok == 'true' | |
| env: | |
| OSV_VERSION: "2.3.6" | |
| run: | | |
| set -euo pipefail | |
| curl -sSLf -o osv-scanner \ | |
| "https://github.com/google/osv-scanner/releases/download/v${OSV_VERSION}/osv-scanner_linux_amd64" | |
| chmod +x osv-scanner | |
| sudo mv osv-scanner /usr/local/bin/ | |
| osv-scanner --version | |
| - name: Run osv-scanner against private app | |
| if: steps.have-token.outputs.ok == 'true' | |
| run: | | |
| set -uo pipefail | |
| osv-scanner scan source \ | |
| --recursive \ | |
| --format=sarif \ | |
| --output=osv-app.sarif \ | |
| ./app-src || rc=$? | |
| rc=${rc:-0} | |
| echo "osv-scanner exit code: $rc" | |
| if [ -f osv-app.sarif ]; then | |
| jq -r '.runs[]?.results | length' osv-app.sarif | head -3 | xargs -I{} echo "App SARIF results count: {}" | |
| fi | |
| # Hart fail nur bei echten Vulns (exit 1) | |
| if [ "$rc" = "1" ]; then exit 1; fi | |
| - name: Upload App SARIF to GitHub code scanning | |
| if: always() && steps.have-token.outputs.ok == 'true' | |
| uses: github/codeql-action/upload-sarif@v3 | |
| with: | |
| sarif_file: osv-app.sarif | |
| category: osv-scanner-app | |
| continue-on-error: true | |
| - name: Upload App SARIF as artefact | |
| if: always() && steps.have-token.outputs.ok == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: osv-scanner-app-report | |
| path: osv-app.sarif | |
| retention-days: 90 | |
| if-no-files-found: ignore | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 5. TLS-Posture-Check — twobreath.com (testssl.sh) | |
| # Schließt: TR-03161 O.Ntwk_2, O.Ntwk_7 (Marketing-Site) | |
| # Nur scheduled / manuell — vermeidet jeden-Push-Last gegen den Server. | |
| # ────────────────────────────────────────────────────────────────────── | |
| tls-posture: | |
| name: TLS Posture (testssl.sh) | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Run testssl.sh against www.twobreath.com | |
| run: | | |
| set -eu | |
| mkdir -p reports | |
| chmod 777 reports | |
| # `--user` bindet die Container-UID an den Runner-User, damit | |
| # die JSON-Ausgabe in das gemountete Volume geschrieben werden | |
| # kann. Ohne diesen Schritt läuft der Scan, aber die Datei | |
| # wird nicht persistiert (Permission denied). | |
| docker run --rm \ | |
| --user "$(id -u):$(id -g)" \ | |
| -v "$PWD/reports:/data" \ | |
| drwetter/testssl.sh:latest \ | |
| --jsonfile-pretty /data/testssl.json \ | |
| --severity LOW \ | |
| --quiet --color 0 \ | |
| https://www.twobreath.com || true | |
| if [ -f reports/testssl.json ]; then | |
| echo "=== testssl.sh summary ===" | |
| jq '.scanResult[0] | {target, ip, service, severity_summary: ( | |
| [.protocols[]?, .vulnerabilities[]?, .cipherTests[]?] | |
| | group_by(.severity) | map({(.[0].severity): length}) | add | |
| )}' reports/testssl.json 2>/dev/null || cat reports/testssl.json | |
| else | |
| echo "::warning::testssl.json was not produced — see step log for raw output." | |
| fi | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: testssl-report | |
| path: reports/testssl.json | |
| if-no-files-found: warn | |
| retention-days: 90 | |
| # ────────────────────────────────────────────────────────────────────── | |
| # 6. HTTP-Security-Header — twobreath.com (Mozilla HTTP Observatory) | |
| # Schließt: TR-03161 O.Ntwk_2 (Marketing-Site) | |
| # ────────────────────────────────────────────────────────────────────── | |
| http-headers: | |
| name: HTTP Security Headers (Observatory) | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| steps: | |
| - name: Trigger Observatory scan | |
| run: | | |
| set -eu | |
| mkdir -p reports | |
| curl -sS -X POST \ | |
| "https://observatory-api.mdn.mozilla.net/api/v2/scan?host=www.twobreath.com" \ | |
| -H "Content-Type: application/json" \ | |
| -d '{}' \ | |
| | tee reports/observatory.json | |
| echo | |
| echo "=== Observatory grade ===" | |
| jq -r '.grade // "n/a"' reports/observatory.json || true | |
| - uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: observatory-report | |
| path: reports/observatory.json | |
| if-no-files-found: warn | |
| retention-days: 90 |