From 4edf7e8a4648bfda222738a16a2318a79b6a676b Mon Sep 17 00:00:00 2001 From: DragonSlayer_14 Date: Sun, 13 Sep 2026 00:31:49 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Feat:=20F=C3=BCgt=20Security-Scan-Skripte,?= =?UTF-8?q?=20Renovate-Konfiguration=20und=20CI/CD-Workflows=20hinzu;=20Ve?= =?UTF-8?q?rsion=20auf=201.0.5=20erh=C3=B6ht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/code-quality.yaml | 54 +++++ .gitea/workflows/main.yaml | 13 +- .gitea/workflows/renovate.yaml | 26 +++ .gitea/workflows/security-scan.yaml | 95 ++++++++ .gitea/workflows/testing.yaml | 13 +- .gitea/workflows/trufflehog-scan.yaml | 59 +++++ .gitea/workflows/unit-tests.yaml | 38 ++++ .gitignore | 2 + .idea/jsonSchemas.xml | 43 ++++ Cargo.lock | 2 +- Cargo.toml | 2 +- qodana.yaml | 50 +++++ renovate.json | 102 +++++++++ scripts/report-security-issue.py | 312 ++++++++++++++++++++++++++ 14 files changed, 807 insertions(+), 4 deletions(-) create mode 100644 .gitea/workflows/code-quality.yaml create mode 100644 .gitea/workflows/renovate.yaml create mode 100644 .gitea/workflows/security-scan.yaml create mode 100644 .gitea/workflows/trufflehog-scan.yaml create mode 100644 .gitea/workflows/unit-tests.yaml create mode 100644 .idea/jsonSchemas.xml create mode 100644 qodana.yaml create mode 100644 renovate.json create mode 100644 scripts/report-security-issue.py diff --git a/.gitea/workflows/code-quality.yaml b/.gitea/workflows/code-quality.yaml new file mode 100644 index 0000000..8c35d11 --- /dev/null +++ b/.gitea/workflows/code-quality.yaml @@ -0,0 +1,54 @@ +name: Code Quality (Auto-Format & Clippy-Fix) + +on: + push: + branches: + - dev + workflow_dispatch: + +jobs: + fix: + name: Formatierung & Clippy automatisch beheben + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + ref: ${{ gitea.ref_name || github.ref_name }} + token: ${{ secrets.PACKAGE_TOKEN || secrets.RELEASE_TOKEN || secrets.PUBLISH_TOKEN || secrets.API_TOKEN || secrets.PAT_TOKEN || secrets.CUSTOM_TOKEN || secrets.GITEA_TOKEN || secrets.GITHUB_TOKEN || github.token }} + + - name: Install Rust Toolchain + uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + components: clippy, rustfmt + cache: false + + - name: Cache Cargo-Abhängigkeiten & Build-Artefakte + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}- + + - name: Formatierung automatisch beheben + run: cargo fmt + + - name: Clippy-Fixes automatisch anwenden + run: cargo clippy --fix --allow-dirty --allow-staged --all-targets + + - name: Änderungen committen & pushen + run: | + if [ -n "$(git status --porcelain)" ]; then + git config user.name "Gitea-Bot" + git config user.email "no-reply@creativedragonslayer.de" + git add -A + git commit -m "Style: Automatische Formatierung & Clippy-Fixes" + git push origin HEAD:${{ gitea.ref_name || github.ref_name }} + else + echo "Keine Formatierungs- oder Clippy-Änderungen." + fi diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml index edfce51..7e39e6b 100644 --- a/.gitea/workflows/main.yaml +++ b/.gitea/workflows/main.yaml @@ -14,11 +14,22 @@ jobs: uses: actions/checkout@v7 - name: Install Rust Toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@v2 with: toolchain: stable cache: false + - name: Cache Cargo-Abhängigkeiten & Build-Artefakte + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}- + - name: Run Tests run: | cargo test diff --git a/.gitea/workflows/renovate.yaml b/.gitea/workflows/renovate.yaml new file mode 100644 index 0000000..cd3e060 --- /dev/null +++ b/.gitea/workflows/renovate.yaml @@ -0,0 +1,26 @@ +name: Renovate + +on: + schedule: + - cron: "0 * * * *" + workflow_dispatch: + +jobs: + renovate: + name: Dependency-Updates prüfen & Pull Requests erstellen + runs-on: ubuntu-latest + container: ghcr.io/renovatebot/renovate:44.82.0 + steps: + - name: Renovate ausführen + run: renovate + env: + RENOVATE_PLATFORM: gitea + RENOVATE_ENDPOINT: ${{ gitea.server_url || github.server_url }}/api/v1/ + RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} + RENOVATE_REPOSITORIES: ${{ gitea.repository || github.repository }} + RENOVATE_AUTODISCOVER: "false" + RENOVATE_GIT_AUTHOR: "Renovate Bot " + RENOVATE_HOST_RULES: >- + [{"hostType":"cargo","matchHost":"${{ gitea.server_url || github.server_url }}","token":"${{ secrets.RENOVATE_TOKEN }}"}] + GITHUB_COM_TOKEN: ${{ secrets.GH_RENOVATE_TOKEN }} + LOG_LEVEL: info diff --git a/.gitea/workflows/security-scan.yaml b/.gitea/workflows/security-scan.yaml new file mode 100644 index 0000000..75df4b3 --- /dev/null +++ b/.gitea/workflows/security-scan.yaml @@ -0,0 +1,95 @@ +name: Security Scans + +on: + push: + branches: + - main + - testing + - dev + pull_request: + schedule: + - cron: "0 5 * * 1" + workflow_dispatch: + +jobs: + security-scan: + name: Trivy & OSV-Scanner + runs-on: ubuntu-latest + env: + TRIVY_VERSION: "0.74.0" + OSV_SCANNER_VERSION: "2.5.1" + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Lokales bin-Verzeichnis zum PATH hinzufügen + run: | + mkdir -p "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Cache Trivy-Binary + id: cache-trivy + uses: actions/cache@v6 + with: + path: ~/.local/bin/trivy + key: trivy-bin-${{ runner.os }}-${{ env.TRIVY_VERSION }} + + - name: Install Trivy + if: steps.cache-trivy.outputs.cache-hit != 'true' + run: | + curl -fsSL -o trivy.tar.gz \ + "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" + tar -xzf trivy.tar.gz trivy + chmod +x trivy + mv trivy "$HOME/.local/bin/trivy" + rm -f trivy.tar.gz + + - name: Ermittle Cache-Datum für Trivy-DB + run: echo "CACHE_DATE=$(date -u +%Y-%m-%d)" >> "$GITHUB_ENV" + + - name: Cache Trivy-Schwachstellen-Datenbank + uses: actions/cache@v6 + with: + path: ~/.cache/trivy + key: trivy-db-${{ runner.os }}-${{ env.CACHE_DATE }} + restore-keys: | + trivy-db-${{ runner.os }}- + + - name: Run Trivy Scanner + run: | + trivy fs \ + --scanners vuln,secret,misconfig \ + --severity CRITICAL,HIGH \ + --format json \ + --output trivy-results.json \ + --exit-code 0 \ + . + + - name: Cache OSV-Scanner-Binary + id: cache-osv-scanner + uses: actions/cache@v6 + with: + path: ~/.local/bin/osv-scanner + key: osv-scanner-bin-${{ runner.os }}-${{ env.OSV_SCANNER_VERSION }} + + - name: Install OSV-Scanner + if: steps.cache-osv-scanner.outputs.cache-hit != 'true' + run: | + curl -fsSL -o "$HOME/.local/bin/osv-scanner" \ + "https://github.com/google/osv-scanner/releases/download/v${OSV_SCANNER_VERSION}/osv-scanner_linux_amd64" + chmod +x "$HOME/.local/bin/osv-scanner" + + - name: Run OSV-Scanner + run: | + set +e + osv-scanner scan source --recursive --format json --output-file osv-results.json . + echo "OSV_EXIT=$?" >> "$GITHUB_ENV" + + - name: Ergebnisse & Gitea-Issue erstellen/aktualisieren + env: + GITEA_URL: ${{ gitea.server_url || github.server_url }} + REPO: ${{ gitea.repository || github.repository }} + TOKEN: ${{ secrets.SECURITY_TOKEN }} + RUN_URL: ${{ gitea.server_url || github.server_url }}/${{ gitea.repository || github.repository }}/actions/runs/${{ gitea.run_id || github.run_id }} + run: | + python3 scripts/report-security-issue.py trivy-results.json osv-results.json diff --git a/.gitea/workflows/testing.yaml b/.gitea/workflows/testing.yaml index 674f41d..328ca4f 100644 --- a/.gitea/workflows/testing.yaml +++ b/.gitea/workflows/testing.yaml @@ -14,11 +14,22 @@ jobs: uses: actions/checkout@v7 - name: Install Rust Toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@v2 with: toolchain: stable cache: false + - name: Cache Cargo-Abhängigkeiten & Build-Artefakte + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}- + - name: Run Tests run: | cargo test diff --git a/.gitea/workflows/trufflehog-scan.yaml b/.gitea/workflows/trufflehog-scan.yaml new file mode 100644 index 0000000..5e4fcb6 --- /dev/null +++ b/.gitea/workflows/trufflehog-scan.yaml @@ -0,0 +1,59 @@ +name: TruffleHog Secret Scan + +on: + push: + pull_request: + schedule: + - cron: "0 6 * * 1" + workflow_dispatch: + +jobs: + trufflehog-scan: + name: TruffleHog + runs-on: ubuntu-latest + env: + TRUFFLEHOG_VERSION: "3.97.4" + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Lokales bin-Verzeichnis zum PATH hinzufügen + run: | + mkdir -p "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + - name: Cache TruffleHog-Binary + id: cache-trufflehog + uses: actions/cache@v6 + with: + path: ~/.local/bin/trufflehog + key: trufflehog-bin-${{ runner.os }}-${{ env.TRUFFLEHOG_VERSION }} + + - name: Install TruffleHog + if: steps.cache-trufflehog.outputs.cache-hit != 'true' + run: | + curl -fsSL -o trufflehog.tar.gz \ + "https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}/trufflehog_${TRUFFLEHOG_VERSION}_linux_amd64.tar.gz" + tar -xzf trufflehog.tar.gz trufflehog + chmod +x trufflehog + mv trufflehog "$HOME/.local/bin/trufflehog" + rm trufflehog.tar.gz + + - name: Run TruffleHog Scanner + run: | + set +e + trufflehog git file://. --results=verified,unknown --fail --json > trufflehog-results.json + echo "TRUFFLEHOG_EXIT=$?" >> "$GITHUB_ENV" + + - name: Ergebnisse & Gitea-Issue erstellen/aktualisieren + env: + GITEA_URL: ${{ gitea.server_url || github.server_url }} + REPO: ${{ gitea.repository || github.repository }} + TOKEN: ${{ secrets.SECURITY_TOKEN }} + RUN_URL: ${{ gitea.server_url || github.server_url }}/${{ gitea.repository || github.repository }}/actions/runs/${{ gitea.run_id || github.run_id }} + ISSUE_TITLE: "Security-Scan: TruffleHog Secrets" + ISSUE_LABEL: "security-scan-trufflehog" + run: | + python3 scripts/report-security-issue.py "" "" trufflehog-results.json diff --git a/.gitea/workflows/unit-tests.yaml b/.gitea/workflows/unit-tests.yaml new file mode 100644 index 0000000..7703f7f --- /dev/null +++ b/.gitea/workflows/unit-tests.yaml @@ -0,0 +1,38 @@ +name: Unit-Tests + +on: + pull_request: + types: + - opened + - synchronize + - reopened + branches: + - testing + +jobs: + test: + name: Unit-Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v7 + + - name: Install Rust Toolchain + uses: actions-rust-lang/setup-rust-toolchain@v2 + with: + toolchain: stable + cache: false + + - name: Cache Cargo-Abhängigkeiten & Build-Artefakte + uses: actions/cache@v6 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }} + restore-keys: | + cargo-${{ runner.os }}- + + - name: Run Tests + run: cargo test diff --git a/.gitignore b/.gitignore index 85c7d47..c5aa50f 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,5 @@ fabric.properties # Built Visual Studio Code Extensions *.vsix + +.junie/plans diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml new file mode 100644 index 0000000..181b816 --- /dev/null +++ b/.idea/jsonSchemas.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e1467bd..7a6ea67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,7 +10,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "config-ctdra" -version = "1.0.4" +version = "1.0.5" dependencies = [ "confy", "program-ctdra", diff --git a/Cargo.toml b/Cargo.toml index 77f3512..6bd07f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "config-ctdra" -version = "1.0.4" +version = "1.0.5" edition = "2024" authors = ['DragonSlayer_14'] readme = "README.md" diff --git a/qodana.yaml b/qodana.yaml new file mode 100644 index 0000000..d5f217e --- /dev/null +++ b/qodana.yaml @@ -0,0 +1,50 @@ +#-------------------------------------------------------------------------------# +# Qodana analysis is configured by qodana.yaml file # +# https://www.jetbrains.com/help/qodana/qodana-yaml.html # +#-------------------------------------------------------------------------------# + +################################################################################# +# WARNING: Do not store sensitive information in this file, # +# as its contents will be included in the Qodana report. # +################################################################################# +version: "1.0" + +#Specify inspection profile for code analysis +profile: + name: qodana.starter + +#Enable inspections +#include: +# - name: + +#Disable inspections +#exclude: +# - name: +# paths: +# - + +#Execute shell command before Qodana execution (Applied in CI/CD pipeline) +#bootstrap: sh ./prepare-qodana.sh + +#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline) +#plugins: +# - id: #(plugin id can be found at https://plugins.jetbrains.com) + +# Quality gate. Will fail the CI/CD pipeline if any condition is not met +# severityThresholds - configures maximum thresholds for different problem severities +# testCoverageThresholds - configures minimum code coverage on a whole project and newly added code +# dependencyLicenses - fails the run on prohibited or unknown dependency licenses +# Code Coverage is available in Ultimate and Ultimate Plus plans +#failureConditions: +# severityThresholds: +# any: 15 +# critical: 5 +# testCoverageThresholds: +# fresh: 70 +# total: 50 +# dependencyLicenses: +# failOnProhibited: true +# failOnUnknown: false + +#Specify Qodana linter for analysis (Applied in CI/CD pipeline) +linter: jetbrains/qodana-:2026.2 diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..9cc3c16 --- /dev/null +++ b/renovate.json @@ -0,0 +1,102 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "timezone": "Europe/Berlin", + "schedule": ["before 6am on monday"], + "allowCustomCrateRegistries": true, + "baseBranchPatterns": [ + "dev" + ], + "packageRules": [ + { + "matchFileNames": [".gitea/workflows/**"], + "groupName": "Gitea Actions", + "separateMajorMinor": false, + "separateMinorPatch": false + }, + { + "matchManagers": ["cargo"], + "groupName": "Cargo Dependencies", + "separateMajorMinor": false, + "separateMinorPatch": false + }, + { + "matchManagers": ["dockerfile", "docker-compose"], + "groupName": "Docker-Images", + "separateMajorMinor": false, + "separateMinorPatch": false + } + ], + "customManagers": [ + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.gitea/workflows/.+\\.ya?ml$/" + ], + "matchStrings": [ + "TRIVY_VERSION:\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "aquasecurity/trivy", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.*)$" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.gitea/workflows/.+\\.ya?ml$/" + ], + "matchStrings": [ + "OSV_SCANNER_VERSION:\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "google/osv-scanner", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.*)$" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.gitea/workflows/.+\\.ya?ml$/" + ], + "matchStrings": [ + "TRUFFLEHOG_VERSION:\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "trufflesecurity/trufflehog", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.*)$" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.gitea/workflows/.+\\.ya?ml$/" + ], + "matchStrings": [ + "CARGO_BINSTALL_VERSION:\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "cargo-bins/cargo-binstall", + "datasourceTemplate": "github-releases", + "extractVersionTemplate": "^v(?.*)$" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.gitea/workflows/.+\\.ya?ml$/" + ], + "matchStrings": [ + "CARGO_DEB_VERSION:\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "cargo-deb", + "datasourceTemplate": "crate" + }, + { + "customType": "regex", + "managerFilePatterns": [ + "/^\\.gitea/workflows/.+\\.ya?ml$/" + ], + "matchStrings": [ + "CARGO_GENERATE_RPM_VERSION:\\s*\"(?[^\"]+)\"" + ], + "depNameTemplate": "cargo-generate-rpm", + "datasourceTemplate": "crate" + } + ] +} diff --git a/scripts/report-security-issue.py b/scripts/report-security-issue.py new file mode 100644 index 0000000..1518b86 --- /dev/null +++ b/scripts/report-security-issue.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python3 +"""Erstellt oder kommentiert ein Gitea-Issue mit den Ergebnissen der Security-Scans. + +Sucht ein offenes Issue mit dem Label ISSUE_LABEL (Standard: "security-scan"). +Existiert eines, wird der aktuelle Scan-Stand als neuer Kommentar angehängt +(die Historie bleibt erhalten). Existiert keines (z.B. weil das letzte +geschlossen wurde), wird ein neues Issue erstellt. Gibt es keine Funde mehr, +wird ein offenes Issue nur kommentiert, nicht geschlossen. + +Titel und Label lassen sich per Umgebungsvariable ISSUE_TITLE / ISSUE_LABEL +überschreiben, damit z.B. TruffleHog-Funde in ein eigenes Issue laufen statt +in das gemeinsame Trivy/OSV-Issue. +""" + +import json +import os +import sys +import urllib.error +import urllib.request + +LABEL_NAME = os.environ.get("ISSUE_LABEL", "security-scan") +LABEL_COLOR = "#b60205" +ISSUE_TITLE = os.environ.get("ISSUE_TITLE", "Security-Scan: Offene Schwachstellen") + +SEVERITY_ORDER = { + "VERIFIED": -1, + "CRITICAL": 0, + "HIGH": 1, + "MEDIUM": 2, + "LOW": 3, + "UNKNOWN": 4, + "UNVERIFIED": 6, +} + + +def api(method, path, token, gitea_url, data=None): + url = f"{gitea_url}/api/v1{path}" + body = json.dumps(data).encode() if data is not None else None + req = urllib.request.Request(url, data=body, method=method) + req.add_header("Authorization", f"token {token}") + req.add_header("Content-Type", "application/json") + try: + with urllib.request.urlopen(req, timeout=10) as resp: + raw = resp.read() + return json.loads(raw) if raw else None + except urllib.error.HTTPError as e: + print(f"Gitea API Fehler ({method} {path}): {e.code} {e.read().decode()}", file=sys.stderr) + raise + + +def make_finding(source, id, severity, package, installed="-", fixed="-", target="-"): + return { + "source": source, + "id": id, + "severity": severity, + "package": package, + "installed": installed, + "fixed": fixed, + "target": target, + } + + +def cvss_score_to_severity(score): + try: + score = float(score) + except (TypeError, ValueError): + return "UNKNOWN" + if score >= 9.0: + return "CRITICAL" + if score >= 7.0: + return "HIGH" + if score >= 4.0: + return "MEDIUM" + if score > 0.0: + return "LOW" + return "UNKNOWN" + + +def load_trivy(path): + findings = [] + if not path or not os.path.isfile(path): + return findings + with open(path) as f: + data = json.load(f) + for result in data.get("Results", []) or []: + target = result.get("Target", "?") + for vuln in result.get("Vulnerabilities", []) or []: + findings.append(make_finding( + "Trivy", + vuln.get("VulnerabilityID", "?"), + vuln.get("Severity", "UNKNOWN"), + vuln.get("PkgName", "?"), + installed=vuln.get("InstalledVersion", "?"), + fixed=vuln.get("FixedVersion") or "-", + target=target, + )) + for misc in result.get("Misconfigurations", []) or []: + findings.append(make_finding( + "Trivy (Misconfig)", + misc.get("ID", "?"), + misc.get("Severity", "UNKNOWN"), + misc.get("Title", "?"), + target=target, + )) + for secret in result.get("Secrets", []) or []: + findings.append(make_finding( + "Trivy (Secret)", + secret.get("RuleID", "?"), + secret.get("Severity", "UNKNOWN"), + secret.get("Title", "?"), + target=target, + )) + return findings + + +def load_osv(path): + findings = [] + if not path or not os.path.isfile(path): + return findings + with open(path) as f: + data = json.load(f) + for result in data.get("results", []) or []: + source = (result.get("source") or {}).get("path", "?") + for pkg in result.get("packages", []) or []: + info = pkg.get("package", {}) + pkg_name = f"{info.get('name', '?')} ({info.get('ecosystem', '?')})" + severity_by_id = {} + for group in pkg.get("groups", []) or []: + label = cvss_score_to_severity(group.get("max_severity")) + for vuln_id in group.get("ids", []) or []: + severity_by_id[vuln_id] = label + for vuln in pkg.get("vulnerabilities", []) or []: + vuln_id = vuln.get("id", "?") + findings.append(make_finding( + "OSV-Scanner", + vuln_id, + severity_by_id.get(vuln_id, "UNKNOWN"), + pkg_name, + installed=info.get("version", "?"), + target=source, + )) + return findings + + +def load_trufflehog(path): + findings = [] + if not path or not os.path.isfile(path): + return findings + with open(path) as f: + for line in f: + line = line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + git_meta = ((entry.get("SourceMetadata") or {}).get("Data") or {}).get("Git") or {} + findings.append(make_finding( + "TruffleHog", + entry.get("DetectorName", "?"), + "VERIFIED" if entry.get("Verified") else "UNVERIFIED", + git_meta.get("file", "?"), + target=git_meta.get("commit", "-"), + )) + return findings + + +def sort_findings(findings): + return sorted(findings, key=lambda f: (SEVERITY_ORDER.get(f["severity"], 9), f["id"])) + + +def print_summary(findings): + if not findings: + print("Keine Funde.") + return + widths = { + key: max(len(key), *(len(str(f[key])) for f in findings)) + for key in ("source", "id", "severity", "package", "installed", "fixed", "target") + } + header = ("source", "id", "severity", "package", "installed", "fixed", "target") + row_fmt = " ".join(f"{{:{widths[k]}}}" for k in header) + print(row_fmt.format(*header)) + print(row_fmt.format(*("-" * widths[k] for k in header))) + for f in findings: + print(row_fmt.format(*(str(f[k]) for k in header))) + + +TRUFFLEHOG_GUIDANCE = """### Vorgehen bei gefundenen Secrets + +1. **Sofort rotieren/widerrufen**: Das betroffene Secret (Token, Passwort, Schlüssel) beim jeweiligen Dienst ungültig machen und durch ein neues ersetzen. Ein einmal committetes Secret gilt als kompromittiert, auch wenn es später aus der Historie entfernt wird. +2. **Ursache beheben**: Neues Secret nur noch über Umgebungsvariablen/Secrets-Store einbinden, nicht erneut hart codieren. +3. **Historie bereinigen (optional, manuell, erst nach Schritt 1)**: Mit `git filter-repo` oder BFG Repo-Cleaner den Commit-Inhalt entfernen, danach `git push --force` in Absprache mit allen Mitwirkenden – bestehende Clones/Forks werden dadurch ungültig. +4. **Issue schließen**, sobald rotiert wurde. TruffleHog findet das alte Secret ggf. weiterhin in der Historie – nach der Rotation ist das unkritisch. + +> Der Wert des Secrets selbst wird hier bewusst nicht ausgegeben, auch nicht gekürzt – nur Detector, Datei und Commit. Fund lässt sich über "Ziel" (Commit-Hash) und "Paket" (Dateipfad) lokalisieren.""" + + +def escape_md_cell(value): + return str(value).replace("|", "\\|").replace("\r", " ").replace("\n", " ") + + +def build_report(findings, run_url): + lines = [ + "Automatisch erstellt vom Security-Scan-Workflow.", + f"Lauf: {run_url}" if run_url else "", + "", + "| Quelle | ID | Schweregrad | Paket | Installiert | Fix | Ziel |", + "|---|---|---|---|---|---|---|", + ] + for f in findings: + cells = (f["source"], f["id"], f["severity"], f["package"], f["installed"], f["fixed"], f["target"]) + lines.append("| " + " | ".join(escape_md_cell(c) for c in cells) + " |") + if any(f["source"] == "TruffleHog" for f in findings): + lines.append("") + lines.append(TRUFFLEHOG_GUIDANCE) + return "\n".join(lines) + + +def ensure_label(token, gitea_url, repo): + page = 1 + while True: + labels = api("GET", f"/repos/{repo}/labels?limit=50&page={page}", token, gitea_url) or [] + for label in labels: + if label.get("name") == LABEL_NAME: + return label["id"] + if len(labels) < 50: + break + page += 1 + created = api("POST", f"/repos/{repo}/labels", token, gitea_url, { + "name": LABEL_NAME, + "color": LABEL_COLOR, + "description": "Automatisch verwaltet vom Security-Scan-Workflow", + }) + return created["id"] + + +def find_open_issue(token, gitea_url, repo): + issues = api( + "GET", + f"/repos/{repo}/issues?state=open&type=issues&labels={LABEL_NAME}", + token, + gitea_url, + ) or [] + for issue in issues: + if issue.get("title") == ISSUE_TITLE: + return issue + return None + + +def main(): + trivy_path = sys.argv[1] if len(sys.argv) > 1 else None + osv_path = sys.argv[2] if len(sys.argv) > 2 else None + trufflehog_path = sys.argv[3] if len(sys.argv) > 3 else None + + gitea_url = os.environ.get("GITEA_URL", "").strip().rstrip("/") + repo = os.environ.get("REPO", "").strip() + token = os.environ.get("TOKEN", "").strip() + run_url = os.environ.get("RUN_URL", "") + osv_exit = int(os.environ.get("OSV_EXIT", "0")) + trufflehog_exit = int(os.environ.get("TRUFFLEHOG_EXIT", "0")) + + findings = sort_findings(load_trivy(trivy_path) + load_osv(osv_path) + load_trufflehog(trufflehog_path)) + print_summary(findings) + + if not token or not gitea_url or not repo: + missing = [name for name, val in [("TOKEN", token), ("GITEA_URL", gitea_url), ("REPO", repo)] if not val] + print(f"{', '.join(missing)} nicht gesetzt oder leer – überspringe Gitea-Issue-Synchronisation.") + else: + open_issue = find_open_issue(token, gitea_url, repo) + + if findings: + report = build_report(findings, run_url) + if open_issue: + print(f"Kommentiere bestehendes Issue #{open_issue['number']} mit {len(findings)} Fund(en).") + api("POST", f"/repos/{repo}/issues/{open_issue['number']}/comments", token, gitea_url, {"body": report}) + else: + label_id = ensure_label(token, gitea_url, repo) + print(f"Erstelle neues Issue mit {len(findings)} Fund(en).") + api("POST", f"/repos/{repo}/issues", token, gitea_url, { + "title": ISSUE_TITLE, + "body": report, + "labels": [label_id], + }) + elif open_issue: + print(f"Keine aktuellen Funde mehr. Kommentiere Issue #{open_issue['number']}.") + api("POST", f"/repos/{repo}/issues/{open_issue['number']}/comments", token, gitea_url, { + "body": f"Aktueller Scan hat keine offenen Schwachstellen mehr gefunden.\n\n{run_url}".strip(), + }) + else: + print("Keine Funde und kein offenes Issue vorhanden.") + + osv_ok_exits = {0, 1} + trufflehog_ok_exits = {0, 183} + if osv_exit not in osv_ok_exits: + print(f"WARNUNG: osv-scanner beendete sich mit unerwartetem Exit-Code {osv_exit} - Scan evtl. unvollständig.", file=sys.stderr) + if trufflehog_exit not in trufflehog_ok_exits: + print(f"WARNUNG: trufflehog beendete sich mit unerwartetem Exit-Code {trufflehog_exit} - Scan evtl. unvollständig.", file=sys.stderr) + + has_trivy_findings = any(f["source"].startswith("Trivy") for f in findings) + if ( + has_trivy_findings + or osv_exit == 1 + or trufflehog_exit == 183 + or osv_exit not in osv_ok_exits + or trufflehog_exit not in trufflehog_ok_exits + ): + sys.exit(1) + + +if __name__ == "__main__": + main() From 29e41e8ea2ed77ad4cf106d92e0a343e519de323 Mon Sep 17 00:00:00 2001 From: Gitea-Bot Date: Sat, 12 Sep 2026 22:32:35 +0000 Subject: [PATCH 2/3] Style: Automatische Formatierung & Clippy-Fixes --- src/lib.rs | 33 +++++++++++++-------------------- tests/integration_tests.rs | 22 +++++++++++----------- 2 files changed, 24 insertions(+), 31 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 60f6b3f..9e87599 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -33,8 +33,8 @@ use std::sync::{OnceLock, RwLock}; pub use confy::ConfyError; use program_ctdra::try_program_name; -use serde::de::DeserializeOwned; use serde::Serialize; +use serde::de::DeserializeOwned; use sudo_ctdra::is_run_as_root; /// Optionaler benutzerdefinierter Konfigurationsdateiname (Standard: "config"). @@ -72,13 +72,11 @@ pub fn set_config_name(name: impl Into) { /// Liefert den konfigurierten Namen der Konfigurationsdatei (Standard: `"config"`). pub fn get_config_name() -> String { - if let Some(lock) = CONFIG_NAME.get() { - if let Ok(guard) = lock.read() { - if let Some(name) = guard.as_ref() { + if let Some(lock) = CONFIG_NAME.get() + && let Ok(guard) = lock.read() + && let Some(name) = guard.as_ref() { return name.clone(); } - } - } DEFAULT_CONFIG_NAME.to_string() } @@ -100,11 +98,10 @@ pub fn clear_custom_dir() { /// Liefert das aktuell gesetzte benutzerdefinierte Konfigurationsverzeichnis, falls vorhanden. pub fn get_custom_dir() -> Option { - if let Some(lock) = CUSTOM_CONFIG_DIR.get() { - if let Ok(guard) = lock.read() { + if let Some(lock) = CUSTOM_CONFIG_DIR.get() + && let Ok(guard) = lock.read() { return guard.clone(); } - } None } @@ -126,11 +123,10 @@ pub fn clear_custom_path() { /// Liefert den aktuell gesetzten expliziten Pfad zur Konfigurationsdatei, falls vorhanden. pub fn get_custom_path() -> Option { - if let Some(lock) = CUSTOM_CONFIG_PATH.get() { - if let Ok(guard) = lock.read() { + if let Some(lock) = CUSTOM_CONFIG_PATH.get() + && let Ok(guard) = lock.read() { return guard.clone(); } - } None } @@ -263,20 +259,17 @@ where { let map_lock = GLOBAL_CONFIGS.get_or_init(|| RwLock::new(HashMap::new())); - if let Ok(guard) = map_lock.read() { - if let Some(entry) = guard.get(&TypeId::of::()) { - if let Some(val) = entry.downcast_ref::() { + if let Ok(guard) = map_lock.read() + && let Some(entry) = guard.get(&TypeId::of::()) + && let Some(val) = entry.downcast_ref::() { return val; } - } - } let mut guard = map_lock.write().unwrap(); - if let Some(entry) = guard.get(&TypeId::of::()) { - if let Some(val) = entry.downcast_ref::() { + if let Some(entry) = guard.get(&TypeId::of::()) + && let Some(val) = entry.downcast_ref::() { return val; } - } let loaded: T = load_config::(); let boxed: &'static T = Box::leak(Box::new(loaded)); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index 2886996..e396141 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -1,7 +1,7 @@ use config_ctdra::{ - clear_custom_dir, clear_custom_path, get_config, get_config_name, get_config_path, + ConfyError, clear_custom_dir, clear_custom_path, get_config, get_config_name, get_config_path, get_custom_dir, get_custom_path, get_program_name, load, load_config, modify, modify_config, - save_config, set_config_name, set_custom_dir, set_custom_path, store, ConfyError, + save_config, set_config_name, set_custom_dir, set_custom_path, store, }; use serde::{Deserialize, Serialize}; use std::env; @@ -17,17 +17,11 @@ fn lock_test() -> std::sync::MutexGuard<'static, ()> { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Default)] struct DummyAppConfig { general: DummyGeneral, } -impl Default for DummyAppConfig { - fn default() -> Self { - Self { - general: DummyGeneral::default(), - } - } -} #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] struct DummyGeneral { @@ -199,7 +193,10 @@ fn test_load_config_fallback_and_invalid_toml() { // load::() sollte bei ungültigem TOML fehlschlagen let res: Result = load(); - assert!(res.is_err(), "Laden von korruptem TOML sollte mit ConfyError fehlschlagen"); + assert!( + res.is_err(), + "Laden von korruptem TOML sollte mit ConfyError fehlschlagen" + ); // load_config::() fällt im Fehlerfall auf Default zurück let loaded_default: CustomServerConfig = load_config(); @@ -346,7 +343,10 @@ fn test_get_config_singleton_cache() { // Zweiter Aufruf: liefert dieselbe statische Referenz let ref2: &'static DatabaseConfig = get_config(); - assert!(std::ptr::eq(ref1, ref2), "get_config muss dieselbe Referenz zurückgeben"); + assert!( + std::ptr::eq(ref1, ref2), + "get_config muss dieselbe Referenz zurückgeben" + ); cleanup_temp_file(&temp_file); clear_custom_path(); From 6a48840ec08162162be19d5f16fdf9ba8950f2b1 Mon Sep 17 00:00:00 2001 From: Gitea-Bot Date: Sat, 12 Sep 2026 22:33:04 +0000 Subject: [PATCH 3/3] Style: Automatische Formatierung & Clippy-Fixes --- src/lib.rs | 35 ++++++++++++++++++++--------------- tests/integration_tests.rs | 4 +--- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 9e87599..7f067b0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,9 +74,10 @@ pub fn set_config_name(name: impl Into) { pub fn get_config_name() -> String { if let Some(lock) = CONFIG_NAME.get() && let Ok(guard) = lock.read() - && let Some(name) = guard.as_ref() { - return name.clone(); - } + && let Some(name) = guard.as_ref() + { + return name.clone(); + } DEFAULT_CONFIG_NAME.to_string() } @@ -99,9 +100,10 @@ pub fn clear_custom_dir() { /// Liefert das aktuell gesetzte benutzerdefinierte Konfigurationsverzeichnis, falls vorhanden. pub fn get_custom_dir() -> Option { if let Some(lock) = CUSTOM_CONFIG_DIR.get() - && let Ok(guard) = lock.read() { - return guard.clone(); - } + && let Ok(guard) = lock.read() + { + return guard.clone(); + } None } @@ -124,9 +126,10 @@ pub fn clear_custom_path() { /// Liefert den aktuell gesetzten expliziten Pfad zur Konfigurationsdatei, falls vorhanden. pub fn get_custom_path() -> Option { if let Some(lock) = CUSTOM_CONFIG_PATH.get() - && let Ok(guard) = lock.read() { - return guard.clone(); - } + && let Ok(guard) = lock.read() + { + return guard.clone(); + } None } @@ -261,15 +264,17 @@ where if let Ok(guard) = map_lock.read() && let Some(entry) = guard.get(&TypeId::of::()) - && let Some(val) = entry.downcast_ref::() { - return val; - } + && let Some(val) = entry.downcast_ref::() + { + return val; + } let mut guard = map_lock.write().unwrap(); if let Some(entry) = guard.get(&TypeId::of::()) - && let Some(val) = entry.downcast_ref::() { - return val; - } + && let Some(val) = entry.downcast_ref::() + { + return val; + } let loaded: T = load_config::(); let boxed: &'static T = Box::leak(Box::new(loaded)); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index e396141..3a705b5 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -16,13 +16,11 @@ fn lock_test() -> std::sync::MutexGuard<'static, ()> { TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner()) } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] -#[derive(Default)] +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)] struct DummyAppConfig { general: DummyGeneral, } - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] struct DummyGeneral { log_level: String,