Compare commits
35
Commits
v1.0.2
...
v1.0.6-preview
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f774ac4e8
|
||
|
|
d85826757b | ||
|
|
6937d3142e
|
||
|
|
bd3f05d09e
|
||
|
|
9e3a568b22
|
||
|
|
bb3d5dc212
|
||
|
|
3871ffa3b0
|
||
|
|
6ec1eda53d | ||
|
|
e1f8d70a61 | ||
|
|
7821364733
|
||
|
|
1add336e12
|
||
|
|
6e7fcf459f
|
||
|
|
9d22a77d0e
|
||
|
|
45d571c487
|
||
|
|
6a48840ec0 | ||
|
|
29e41e8ea2 | ||
|
|
4edf7e8a46
|
||
|
|
4972318aff
|
||
|
|
f029efe315
|
||
|
|
dcf2efbf98
|
||
|
|
2e7d2f98fd
|
||
|
|
469e699074
|
||
|
|
d87ece69da
|
||
|
|
f73ba4b1d6
|
||
|
|
d7845349e8
|
||
|
|
7bea78f194
|
||
|
|
87c1a1c09c
|
||
|
|
d781f3f97b
|
||
|
|
a4f27ce091
|
||
|
|
61b016775e
|
||
|
|
439ccd60d4
|
||
|
|
1c99387d6f
|
||
|
|
bf32d82ae7
|
||
|
|
34f61bba5f
|
||
|
|
d351a8b6be
|
@@ -0,0 +1,9 @@
|
||||
[registry]
|
||||
default = "gitea"
|
||||
|
||||
[registries.gitea]
|
||||
index = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/" # Sparse index
|
||||
# index = "https://gitea.creative-dragonslayer.de/Rust-Crates/_cargo-index.git" # Git
|
||||
|
||||
[net]
|
||||
git-fetch-with-cli = true
|
||||
@@ -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
|
||||
@@ -6,19 +6,65 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Erkenne relevante Code-Änderungen
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
version_exists: ${{ steps.check-version.outputs.exists }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Prüfe auf Änderungen am Programmcode
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'src/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'scripts/**'
|
||||
- '.cargo/**'
|
||||
- '.gitea/workflows/main.yaml'
|
||||
|
||||
- name: Prüfe ob Version bereits in der Registry existiert
|
||||
id: check-version
|
||||
if: steps.filter.outputs.code == 'true'
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO_OWNER: ${{ gitea.repository_owner || github.repository_owner }}
|
||||
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 }}
|
||||
run: |
|
||||
python3 scripts/check-version-published.py
|
||||
|
||||
release-and-publish:
|
||||
name: Build, Publish Crate to Gitea Registry & Create Release
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true' && needs.detect-changes.outputs.version_exists != 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
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
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
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.83.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_ALLOW_CUSTOM_CRATE_REGISTRIES: "true"
|
||||
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate-bot@creativedragonslayer.de>"
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,111 @@
|
||||
name: Auto-PR (Testing → Main)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
create-pr:
|
||||
name: Erstelle automatisch PR von testing nach main
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prüfe auf bereits offenen PR nach main
|
||||
id: check_pr
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
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 }}
|
||||
run: |
|
||||
OPEN_PRS=$(curl -s -H "Authorization: token ${TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO}/pulls?state=open&limit=50")
|
||||
EXISTS=$(echo "$OPEN_PRS" | jq -r '[.[] | select(.base.ref == "main" and .head.ref == "testing")] | length')
|
||||
echo "Bereits offene testing→main PRs: ${EXISTS}"
|
||||
echo "exists=${EXISTS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ermittle Versionen auf main & testing
|
||||
id: versions
|
||||
if: steps.check_pr.outputs.exists == '0'
|
||||
run: |
|
||||
git fetch origin main
|
||||
MAIN_VERSION="$(git show origin/main:Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -n1)"
|
||||
TESTING_VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
echo "Version auf main: ${MAIN_VERSION} / Version auf testing: ${TESTING_VERSION}"
|
||||
echo "main_version=${MAIN_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "testing_version=${TESTING_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Ermittle geänderte Kategorien (main...testing)
|
||||
id: categories
|
||||
if: steps.check_pr.outputs.exists == '0' && steps.versions.outputs.main_version == steps.versions.outputs.testing_version
|
||||
run: |
|
||||
CHANGED_FILES="$(git diff --name-only origin/main...HEAD)"
|
||||
echo "Geänderte Dateien main...testing:"
|
||||
echo "$CHANGED_FILES"
|
||||
|
||||
WORKFLOWS="false"
|
||||
CONFIG="false"
|
||||
DOCS="false"
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q '^\.gitea/workflows/'; then
|
||||
WORKFLOWS="true"
|
||||
fi
|
||||
if echo "$CHANGED_FILES" | grep -qE '^(renovate\.json|qodana\.yaml|Cargo\.toml|Cargo\.lock|\.cargo/)'; then
|
||||
CONFIG="true"
|
||||
fi
|
||||
if echo "$CHANGED_FILES" | grep -qE '(^|/)[^/]+\.md$|^LICENSE$'; then
|
||||
DOCS="true"
|
||||
fi
|
||||
|
||||
echo "workflows=${WORKFLOWS}" >> "$GITHUB_OUTPUT"
|
||||
echo "config=${CONFIG}" >> "$GITHUB_OUTPUT"
|
||||
echo "docs=${DOCS}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Bestimme PR-Titel
|
||||
id: title
|
||||
if: steps.check_pr.outputs.exists == '0'
|
||||
run: |
|
||||
if [ "${{ steps.versions.outputs.main_version }}" != "${{ steps.versions.outputs.testing_version }}" ]; then
|
||||
TITLE="Merge testing in main: Release ${{ steps.versions.outputs.testing_version }}"
|
||||
else
|
||||
PARTS=()
|
||||
[ "${{ steps.categories.outputs.workflows }}" = "true" ] && PARTS+=("Workflows")
|
||||
[ "${{ steps.categories.outputs.config }}" = "true" ] && PARTS+=("Konfigurationen")
|
||||
[ "${{ steps.categories.outputs.docs }}" = "true" ] && PARTS+=("Dokumentation")
|
||||
|
||||
if [ ${#PARTS[@]} -eq 0 ]; then
|
||||
TITLE="Merge testing in main"
|
||||
else
|
||||
JOINED=""
|
||||
for PART in "${PARTS[@]}"; do
|
||||
if [ -z "$JOINED" ]; then
|
||||
JOINED="$PART"
|
||||
else
|
||||
JOINED="${JOINED} & ${PART}"
|
||||
fi
|
||||
done
|
||||
TITLE="Merge testing in main: ${JOINED} aktualisiert"
|
||||
fi
|
||||
fi
|
||||
echo "Ermittelter PR-Titel: ${TITLE}"
|
||||
echo "title=${TITLE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Erstelle PR (testing -> main)
|
||||
if: steps.check_pr.outputs.exists == '0'
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
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 }}
|
||||
TITLE: ${{ steps.title.outputs.title }}
|
||||
run: |
|
||||
PAYLOAD=$(jq -n --arg title "$TITLE" --arg head "testing" --arg base "main" \
|
||||
'{title: $title, head: $head, base: $base}')
|
||||
curl -f -s -S -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/pulls"
|
||||
echo "PR erstellt: ${TITLE}"
|
||||
@@ -6,19 +6,54 @@ on:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Erkenne relevante Code-Änderungen
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Prüfe auf Änderungen am Programmcode
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
code:
|
||||
- 'src/**'
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'scripts/**'
|
||||
- '.cargo/**'
|
||||
- '.gitea/workflows/testing.yaml'
|
||||
|
||||
build-and-preview:
|
||||
name: Build, Check & Create Preview Release
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,84 @@
|
||||
name: Auto Patch-Version-Bump (Dev → Testing PR)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
branches:
|
||||
- testing
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
name: Erkenne relevante Änderungen im PR
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ (gitea.head_ref || github.head_ref) == 'dev' }}
|
||||
outputs:
|
||||
code_changed: ${{ steps.filter.outputs.code }}
|
||||
steps:
|
||||
- name: Checkout Dev-Branch (PR-Head)
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ gitea.head_ref || github.head_ref }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prüfe auf Änderungen an Cargo.toml, Cargo.lock oder src/
|
||||
uses: dorny/paths-filter@v4
|
||||
id: filter
|
||||
with:
|
||||
base: ${{ gitea.base_ref || github.base_ref }}
|
||||
filters: |
|
||||
code:
|
||||
- 'Cargo.toml'
|
||||
- 'Cargo.lock'
|
||||
- 'src/**'
|
||||
|
||||
bump-version:
|
||||
name: Patch-Version erhöhen & auf Dev pushen
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Dev-Branch (PR-Head)
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ gitea.head_ref || github.head_ref }}
|
||||
fetch-depth: 0
|
||||
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: Ermittle Cargo-Version auf testing & dev
|
||||
id: versions
|
||||
run: |
|
||||
git fetch origin testing --depth=1
|
||||
TESTING_VERSION="$(git show origin/testing:Cargo.toml | sed -n 's/^version = "\(.*\)"/\1/p' | head -n1)"
|
||||
DEV_VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
echo "Version auf testing: ${TESTING_VERSION} / Version auf dev: ${DEV_VERSION}"
|
||||
echo "testing_version=${TESTING_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
echo "dev_version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Patch-Version um 1 erhöhen (Cargo.toml & Cargo.lock)
|
||||
if: steps.versions.outputs.testing_version == steps.versions.outputs.dev_version
|
||||
run: |
|
||||
VERSION="${{ steps.versions.outputs.dev_version }}"
|
||||
MAJOR="$(echo "$VERSION" | cut -d. -f1)"
|
||||
MINOR="$(echo "$VERSION" | cut -d. -f2)"
|
||||
PATCH="$(echo "$VERSION" | cut -d. -f3)"
|
||||
NEW_VERSION="${MAJOR}.${MINOR}.$((PATCH + 1))"
|
||||
echo "Erhöhe Version: ${VERSION} -> ${NEW_VERSION}"
|
||||
|
||||
sed -i "0,/^version = \"${VERSION}\"/s//version = \"${NEW_VERSION}\"/" Cargo.toml
|
||||
|
||||
PACKAGE_NAME="$(sed -n 's/^name = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
awk -v new="$NEW_VERSION" -v pkg="$PACKAGE_NAME" '
|
||||
found_name && /^version = "/ {
|
||||
print "version = \"" new "\""
|
||||
found_name = 0
|
||||
next
|
||||
}
|
||||
$0 == "name = \"" pkg "\"" { found_name = 1 }
|
||||
{ print }
|
||||
' Cargo.lock > Cargo.lock.tmp && mv Cargo.lock.tmp Cargo.lock
|
||||
|
||||
git config user.name "Gitea-Bot"
|
||||
git config user.email "no-reply@creativedragonslayer.de"
|
||||
git add Cargo.toml Cargo.lock
|
||||
git commit -m "Chore: Erhöht Patch-Version auf ${NEW_VERSION} für Promotion nach testing"
|
||||
git push origin HEAD:${{ gitea.head_ref || github.head_ref }}
|
||||
@@ -108,3 +108,5 @@ fabric.properties
|
||||
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
.junie/plans
|
||||
|
||||
Generated
+1
@@ -3,6 +3,7 @@
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
|
||||
Generated
+43
@@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JsonSchemaMappingsProjectConfiguration">
|
||||
<state>
|
||||
<map>
|
||||
<entry key="GitHub Workflow">
|
||||
<value>
|
||||
<SchemaInfo>
|
||||
<option name="name" value="GitHub Workflow" />
|
||||
<option name="relativePathToSchema" value="https://www.schemastore.org/github-workflow.json" />
|
||||
<option name="applicationDefined" value="true" />
|
||||
<option name="patterns">
|
||||
<list>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/code-quality.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/main.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/renovate.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/security-scan.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/testing.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/trufflehog-scan.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/unit-tests.yaml" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</state>
|
||||
</component>
|
||||
</project>
|
||||
@@ -31,8 +31,10 @@ Config/
|
||||
├── LICENSE # GPL-3.0 Lizenztext
|
||||
├── README.md # Projektdokumentation & Nutzungsbeispiele
|
||||
├── AGENTS.md # Entwickler- und Agenten-Richtlinien
|
||||
└── src/
|
||||
└── lib.rs # Hauptimplementierung (load, store, modify, get_config, Pfadauflösung, Tests)
|
||||
├── src/
|
||||
│ └── lib.rs # Hauptimplementierung (load, store, modify, get_config, Pfadauflösung)
|
||||
└── tests/
|
||||
└── integration_tests.rs # Vollständige Integrationstests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Generated
+18
-2
@@ -10,11 +10,12 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "config-ctdra"
|
||||
version = "1.0.2"
|
||||
version = "1.0.6"
|
||||
dependencies = [
|
||||
"confy",
|
||||
"libc",
|
||||
"program-ctdra",
|
||||
"serde",
|
||||
"sudo-ctdra",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -93,6 +94,12 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "program-ctdra"
|
||||
version = "1.0.1"
|
||||
source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
|
||||
checksum = "528d5771916f74bdffb77ad60ea00c0ae4bd85f9a84e920cd65730a082133d67"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
@@ -141,6 +148,15 @@ dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sudo-ctdra"
|
||||
version = "1.0.1"
|
||||
source = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
|
||||
checksum = "20e576be60eb2050d475d0fbe46f0d09462ba9985ab55c7833e0c2e6d116d341"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.4"
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "config-ctdra"
|
||||
version = "1.0.2"
|
||||
version = "1.0.6"
|
||||
edition = "2024"
|
||||
authors = ['DragonSlayer_14']
|
||||
readme = "README.md"
|
||||
@@ -10,8 +10,9 @@ description = "Einfache, threadsichere Konfigurationsverwaltung für Rust mit au
|
||||
|
||||
[dependencies]
|
||||
confy = "2.0.0"
|
||||
program-ctdra = { version = "1.0.0", registry = "gitea" }
|
||||
serde = { version = "1.0.229", features = ["derive"] }
|
||||
libc = "1.0.0-alpha.4"
|
||||
sudo-ctdra = { version = "1.0.0", registry = "gitea" }
|
||||
|
||||
[profile.release]
|
||||
debug = "none"
|
||||
|
||||
@@ -208,8 +208,8 @@ If you develop a new program, and you want it to be of the greatest possible use
|
||||
|
||||
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
|
||||
|
||||
DockerUpdater
|
||||
Copyright (C) 2026 Linuxapps
|
||||
config
|
||||
Copyright (C) 2026 Rust-Crates
|
||||
|
||||
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||
|
||||
@@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
|
||||
|
||||
DockerUpdater Copyright (C) 2026 Linuxapps
|
||||
config Copyright (C) 2026 Rust-Crates
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
- **Einheitliche Pfadauflösung**:
|
||||
- **Root-Modus** (Linux/Unix UID 0): Konfigurationsdateien werden unter `/etc/<programmname>/<config_name>.toml` abgelegt.
|
||||
- **Benutzermodus**: Standardmäßiges Benutzerverzeichnis (z. B. `~/.config/<programmname>/<config_name>.toml` via `confy`).
|
||||
- **Individuell anpassbar**: Programmnamen (`set_program_name`), Dateinamen (`set_config_name`), Verzeichnisse (`set_custom_dir`) oder explizite Dateipfade (`set_custom_path`).
|
||||
- **Individuell anpassbar**: Dateinamen (`set_config_name`), Verzeichnisse (`set_custom_dir`) oder explizite Dateipfade (`set_custom_path`). Der Programmname wird automatisch über `program-ctdra` ermittelt.
|
||||
- **Globales Caching (`get_config::<T>()`)**: Einmaliges Laden und threadsicheres Zwischenspeichern statischer Referenzen pro Konfigurationstyp.
|
||||
- **Atomare Operationen**: `load`, `store`, `modify`, `modify_config` für konsistentes Laden, Bearbeiten und Speichern.
|
||||
|
||||
@@ -97,11 +97,9 @@ impl Default for Database {
|
||||
### 2. Globales Caching mit `get_config`
|
||||
|
||||
```rust
|
||||
use config_ctdra::{get_config, set_program_name};
|
||||
use config_ctdra::get_config;
|
||||
|
||||
fn main() {
|
||||
set_program_name("my-service");
|
||||
|
||||
let cfg = get_config::<AppConfig>();
|
||||
println!("Log-Level: {}", cfg.general.log_level);
|
||||
println!("DB URL: {}", cfg.database.url);
|
||||
|
||||
+50
@@ -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: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
#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> #(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-<linter>:2026.2
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended"],
|
||||
"timezone": "Europe/Berlin",
|
||||
"schedule": ["before 6am on monday"],
|
||||
"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*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "aquasecurity/trivy",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"OSV_SCANNER_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "google/osv-scanner",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"TRUFFLEHOG_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "trufflesecurity/trufflehog",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"CARGO_BINSTALL_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-bins/cargo-binstall",
|
||||
"datasourceTemplate": "github-releases",
|
||||
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"CARGO_DEB_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-deb",
|
||||
"datasourceTemplate": "crate"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"managerFilePatterns": [
|
||||
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"CARGO_GENERATE_RPM_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "cargo-generate-rpm",
|
||||
"datasourceTemplate": "crate"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Prüft, ob die aktuelle Crate-Version bereits in der Gitea Cargo-Registry existiert.
|
||||
|
||||
Liest Name und Version aus Cargo.toml und fragt die Gitea Package-API ab.
|
||||
Das Ergebnis wird als Step-Output "exists" (true/false) in GITHUB_OUTPUT
|
||||
geschrieben, damit der Release/Publish-Job komplett übersprungen werden kann,
|
||||
statt erst nach Build & Tests an einem "Version existiert bereits"-Fehler der
|
||||
Registry zu scheitern.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
def read_cargo_field(field, path="Cargo.toml"):
|
||||
pattern = re.compile(rf'^{field}\s*=\s*"(.*)"')
|
||||
with open(path) as f:
|
||||
for line in f:
|
||||
match = pattern.match(line.strip())
|
||||
if match:
|
||||
return match.group(1)
|
||||
raise SystemExit(f"Feld '{field}' nicht in {path} gefunden.")
|
||||
|
||||
|
||||
def version_exists(gitea_url, owner, name, version, token):
|
||||
url = f"{gitea_url}/api/v1/packages/{owner}/cargo/{name}/{version}"
|
||||
req = urllib.request.Request(url, method="GET")
|
||||
req.add_header("Authorization", f"token {token}")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
return resp.status == 200
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code == 404:
|
||||
return False
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
gitea_url = os.environ["GITEA_URL"].strip().rstrip("/")
|
||||
owner = os.environ["REPO_OWNER"].strip()
|
||||
token = os.environ["TOKEN"].strip()
|
||||
github_output = os.environ["GITHUB_OUTPUT"]
|
||||
|
||||
name = read_cargo_field("name")
|
||||
version = read_cargo_field("version")
|
||||
|
||||
print(f"Prüfe {name}@{version} in der Gitea Cargo Registry...")
|
||||
exists = version_exists(gitea_url, owner, name, version, token)
|
||||
|
||||
if exists:
|
||||
print(f"Version {version} existiert bereits in der Registry. Release/Publish wird übersprungen.")
|
||||
else:
|
||||
print(f"Version {version} ist neu.")
|
||||
|
||||
with open(github_output, "a") as f:
|
||||
f.write(f"exists={'true' if exists else 'false'}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
+31
-251
@@ -5,13 +5,13 @@
|
||||
//! - Einheitliche Pfadermittlung:
|
||||
//! - Bei Ausführung als Root (unter Linux/Unix): `/etc/<program_name>/<config_name>.toml`
|
||||
//! - Bei regulärem Benutzer: Standard-Benutzer-Konfigurationsverzeichnis (z. B. `~/.config/<program_name>/<config_name>.toml`)
|
||||
//! - Anpassbar über `set_program_name`, `set_config_name`, `set_custom_dir` und `set_custom_path`.
|
||||
//! - Anpassbar über `set_config_name`, `set_custom_dir` und `set_custom_path`.
|
||||
//! - Thread-sicheres globales Caching (`get_config::<T>()`).
|
||||
//! - Atomares Laden, Speichern und Modifizieren (`load`, `store`, `modify`, `modify_config`).
|
||||
//!
|
||||
//! # Beispiel
|
||||
//! ```rust
|
||||
//! use config_ctdra::{get_config, modify_config, set_program_name};
|
||||
//! use config_ctdra::{get_config, modify_config};
|
||||
//! use serde::{Deserialize, Serialize};
|
||||
//!
|
||||
//! #[derive(Serialize, Deserialize, Clone, Debug, Default, PartialEq)]
|
||||
@@ -20,8 +20,6 @@
|
||||
//! pub enable_ssl: bool,
|
||||
//! }
|
||||
//!
|
||||
//! set_program_name("my-service");
|
||||
//!
|
||||
//! // Konfiguration abrufen (wird beim ersten Aufruf geladen und zwischengespeichert)
|
||||
//! let cfg = get_config::<MyConfig>();
|
||||
//! println!("Port: {}", cfg.server_port);
|
||||
@@ -34,11 +32,10 @@ use std::path::PathBuf;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
pub use confy::ConfyError;
|
||||
use serde::de::DeserializeOwned;
|
||||
use program_ctdra::try_program_name;
|
||||
use serde::Serialize;
|
||||
|
||||
/// Optionaler benutzerdefinierter Programmname für die Konfigurationspfade.
|
||||
static PROGRAM_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
|
||||
use serde::de::DeserializeOwned;
|
||||
use sudo_ctdra::is_run_as_root;
|
||||
|
||||
/// Optionaler benutzerdefinierter Konfigurationsdateiname (Standard: "config").
|
||||
static CONFIG_NAME: OnceLock<RwLock<Option<String>>> = OnceLock::new();
|
||||
@@ -56,50 +53,13 @@ static GLOBAL_CONFIGS: OnceLock<RwLock<HashMap<TypeId, &'static (dyn Any + Send
|
||||
/// Standardname für Konfigurationsdateien.
|
||||
const DEFAULT_CONFIG_NAME: &str = "config";
|
||||
|
||||
/// Prüft, ob das Programm mit Root-/Administrator-Rechten ausgeführt wird.
|
||||
/// Ermittelt den Programmnamen über `program-ctdra`.
|
||||
///
|
||||
/// # Returns
|
||||
/// # Panics
|
||||
///
|
||||
/// * `true` - Das Programm läuft mit erhöhten Rechten (z. B. UID 0 unter Linux/Unix)
|
||||
/// * `false` - Das Programm läuft mit normalen Benutzerrechten
|
||||
pub fn is_run_as_root() -> bool {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
std::process::Command::new("net")
|
||||
.args(["session"])
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
unsafe { libc::geteuid() == 0 }
|
||||
}
|
||||
}
|
||||
|
||||
/// Setzt den Programmnamen für die Pfadermittlung explizit.
|
||||
pub fn set_program_name(name: impl Into<String>) {
|
||||
let lock = PROGRAM_NAME.get_or_init(|| RwLock::new(None));
|
||||
if let Ok(mut guard) = lock.write() {
|
||||
*guard = Some(name.into());
|
||||
}
|
||||
}
|
||||
|
||||
/// Ermittelt den konfigurierten Programmnamen (aus `set_program_name` oder `std::env::current_exe`).
|
||||
/// Löst eine Panik aus, wenn der Programmname nicht ermittelt werden kann.
|
||||
pub fn get_program_name() -> String {
|
||||
if let Some(lock) = PROGRAM_NAME.get() {
|
||||
if let Ok(guard) = lock.read() {
|
||||
if let Some(name) = guard.as_ref() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
env::current_exe()
|
||||
.ok()
|
||||
.and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or_else(|| "app".to_string())
|
||||
try_program_name().expect("Programmname konnte nicht ermittelt werden")
|
||||
}
|
||||
|
||||
/// Setzt den Namen der Konfigurationsdatei (ohne Dateiendung `.toml`).
|
||||
@@ -112,12 +72,11 @@ pub fn set_config_name(name: impl Into<String>) {
|
||||
|
||||
/// 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() {
|
||||
return name.clone();
|
||||
}
|
||||
}
|
||||
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()
|
||||
}
|
||||
@@ -140,10 +99,10 @@ pub fn clear_custom_dir() {
|
||||
|
||||
/// Liefert das aktuell gesetzte benutzerdefinierte Konfigurationsverzeichnis, falls vorhanden.
|
||||
pub fn get_custom_dir() -> Option<PathBuf> {
|
||||
if let Some(lock) = CUSTOM_CONFIG_DIR.get() {
|
||||
if let Ok(guard) = lock.read() {
|
||||
return guard.clone();
|
||||
}
|
||||
if let Some(lock) = CUSTOM_CONFIG_DIR.get()
|
||||
&& let Ok(guard) = lock.read()
|
||||
{
|
||||
return guard.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -166,10 +125,10 @@ pub fn clear_custom_path() {
|
||||
|
||||
/// Liefert den aktuell gesetzten expliziten Pfad zur Konfigurationsdatei, falls vorhanden.
|
||||
pub fn get_custom_path() -> Option<PathBuf> {
|
||||
if let Some(lock) = CUSTOM_CONFIG_PATH.get() {
|
||||
if let Ok(guard) = lock.read() {
|
||||
return guard.clone();
|
||||
}
|
||||
if let Some(lock) = CUSTOM_CONFIG_PATH.get()
|
||||
&& let Ok(guard) = lock.read()
|
||||
{
|
||||
return guard.clone();
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -303,19 +262,18 @@ 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::<T>()) {
|
||||
if let Some(val) = entry.downcast_ref::<T>() {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
if let Ok(guard) = map_lock.read()
|
||||
&& let Some(entry) = guard.get(&TypeId::of::<T>())
|
||||
&& let Some(val) = entry.downcast_ref::<T>()
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
let mut guard = map_lock.write().unwrap();
|
||||
if let Some(entry) = guard.get(&TypeId::of::<T>()) {
|
||||
if let Some(val) = entry.downcast_ref::<T>() {
|
||||
return val;
|
||||
}
|
||||
if let Some(entry) = guard.get(&TypeId::of::<T>())
|
||||
&& let Some(val) = entry.downcast_ref::<T>()
|
||||
{
|
||||
return val;
|
||||
}
|
||||
|
||||
let loaded: T = load_config::<T>();
|
||||
@@ -323,181 +281,3 @@ where
|
||||
guard.insert(TypeId::of::<T>(), boxed);
|
||||
boxed
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::sync::Mutex;
|
||||
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct DummyAppConfig {
|
||||
general: DummyGeneral,
|
||||
}
|
||||
|
||||
impl Default for DummyAppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
general: DummyGeneral::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct DummyGeneral {
|
||||
log_level: String,
|
||||
apps_dir: String,
|
||||
}
|
||||
|
||||
impl Default for DummyGeneral {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: "info".to_string(),
|
||||
apps_dir: "/var/apps".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct CustomServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Default for CustomServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config_loading() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let cfg = DummyAppConfig::default();
|
||||
assert_eq!(cfg.general.log_level, "info");
|
||||
assert_eq!(cfg.general.apps_dir, "/var/apps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_program_and_config_name() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
set_program_name("test-app");
|
||||
assert_eq!(get_program_name(), "test-app");
|
||||
|
||||
set_config_name("settings");
|
||||
assert_eq!(get_config_name(), "settings");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_path_and_dir() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
clear_custom_path();
|
||||
clear_custom_dir();
|
||||
set_config_name("config");
|
||||
|
||||
let temp_dir = env::temp_dir().join("test-config-crate-dir");
|
||||
let _ = fs::create_dir_all(&temp_dir);
|
||||
|
||||
set_custom_dir(&temp_dir);
|
||||
assert_eq!(get_custom_dir(), Some(temp_dir.clone()));
|
||||
|
||||
let expected_path = temp_dir.join(format!("{}.toml", get_config_name()));
|
||||
assert_eq!(get_config_path(), expected_path);
|
||||
|
||||
let explicit_file = temp_dir.join("explicit.toml");
|
||||
set_custom_path(&explicit_file);
|
||||
assert_eq!(get_custom_path(), Some(explicit_file.clone()));
|
||||
assert_eq!(get_config_path(), explicit_file);
|
||||
|
||||
clear_custom_path();
|
||||
clear_custom_dir();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store_and_load_with_custom_path() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let temp_file = env::temp_dir().join("test_store_load.toml");
|
||||
let _ = fs::remove_file(&temp_file);
|
||||
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let initial_cfg = CustomServerConfig {
|
||||
host: "0.0.0.0".to_string(),
|
||||
port: 9000,
|
||||
};
|
||||
|
||||
let store_res = store(&initial_cfg);
|
||||
assert!(store_res.is_ok());
|
||||
|
||||
let loaded_cfg: CustomServerConfig = load().expect("Failed to load stored config");
|
||||
assert_eq!(loaded_cfg, initial_cfg);
|
||||
|
||||
// Test modify
|
||||
let mod_res = modify::<CustomServerConfig, _>(|c| {
|
||||
c.port = 9090;
|
||||
});
|
||||
assert!(mod_res.is_ok());
|
||||
|
||||
let reloaded_cfg: CustomServerConfig = load().expect("Failed to reload modified config");
|
||||
assert_eq!(reloaded_cfg.port, 9090);
|
||||
|
||||
let _ = fs::remove_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_config_types_global() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let file_a = env::temp_dir().join("test_global_type_a.toml");
|
||||
let _ = fs::remove_file(&file_a);
|
||||
|
||||
set_custom_path(&file_a);
|
||||
|
||||
let cfg_app: &'static DummyAppConfig = get_config();
|
||||
let cfg_server: &'static CustomServerConfig = get_config();
|
||||
|
||||
assert_eq!(cfg_app.general.apps_dir, "/var/apps");
|
||||
assert_eq!(cfg_server.port, 8080);
|
||||
assert_eq!(cfg_server.host, "127.0.0.1");
|
||||
|
||||
let _ = fs::remove_file(&file_a);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_config_convenience() {
|
||||
let _guard = TEST_LOCK.lock().unwrap();
|
||||
let temp_file = env::temp_dir().join("test_modify_convenience.toml");
|
||||
let _ = fs::remove_file(&temp_file);
|
||||
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let initial = CustomServerConfig {
|
||||
host: "localhost".to_string(),
|
||||
port: 3000,
|
||||
};
|
||||
save_config(initial);
|
||||
|
||||
modify_config::<CustomServerConfig, _>(|c| {
|
||||
c.port = 4000;
|
||||
});
|
||||
|
||||
let loaded = load_config::<CustomServerConfig>();
|
||||
assert_eq!(loaded.port, 4000);
|
||||
assert_eq!(loaded.host, "localhost");
|
||||
|
||||
let _ = fs::remove_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_run_as_root() {
|
||||
let _ = is_run_as_root();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,402 @@
|
||||
use config_ctdra::{
|
||||
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,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
|
||||
static TEST_LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn lock_test() -> std::sync::MutexGuard<'static, ()> {
|
||||
TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Default)]
|
||||
struct DummyAppConfig {
|
||||
general: DummyGeneral,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct DummyGeneral {
|
||||
log_level: String,
|
||||
apps_dir: String,
|
||||
}
|
||||
|
||||
impl Default for DummyGeneral {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: "info".to_string(),
|
||||
apps_dir: "/var/apps".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct CustomServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Default for CustomServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct DatabaseConfig {
|
||||
url: String,
|
||||
max_connections: u32,
|
||||
}
|
||||
|
||||
impl Default for DatabaseConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
url: "postgres://localhost/db".to_string(),
|
||||
max_connections: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_temp_file(path: &PathBuf) {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_program_name_resolution() {
|
||||
let _guard = lock_test();
|
||||
let name = get_program_name();
|
||||
assert!(!name.is_empty(), "Programmname darf nicht leer sein");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_name_getter_and_setter() {
|
||||
let _guard = lock_test();
|
||||
set_config_name("custom_app_config");
|
||||
assert_eq!(get_config_name(), "custom_app_config");
|
||||
|
||||
set_config_name("config");
|
||||
assert_eq!(get_config_name(), "config");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_custom_dir_and_path_management() {
|
||||
let _guard = lock_test();
|
||||
clear_custom_path();
|
||||
clear_custom_dir();
|
||||
set_config_name("app");
|
||||
|
||||
let temp_dir = env::temp_dir().join("test_custom_dir_crate");
|
||||
let _ = fs::create_dir_all(&temp_dir);
|
||||
|
||||
// 1. Benutzerdefiniertes Verzeichnis
|
||||
set_custom_dir(&temp_dir);
|
||||
assert_eq!(get_custom_dir(), Some(temp_dir.clone()));
|
||||
assert_eq!(get_config_path(), temp_dir.join("app.toml"));
|
||||
|
||||
// 2. Benutzerdefiniertes Verzeichnis mit .toml im Config-Namen
|
||||
set_config_name("app.toml");
|
||||
assert_eq!(get_config_path(), temp_dir.join("app.toml"));
|
||||
set_config_name("app");
|
||||
|
||||
// 3. Expliziter benutzerdefinierter Pfad hat Vorrang vor Verzeichnis
|
||||
let explicit_path = temp_dir.join("explicit_settings.toml");
|
||||
set_custom_path(&explicit_path);
|
||||
assert_eq!(get_custom_path(), Some(explicit_path.clone()));
|
||||
assert_eq!(get_config_path(), explicit_path);
|
||||
|
||||
// 4. Zurücksetzen
|
||||
clear_custom_path();
|
||||
assert_eq!(get_custom_path(), None);
|
||||
assert_eq!(get_config_path(), temp_dir.join("app.toml"));
|
||||
|
||||
clear_custom_dir();
|
||||
assert_eq!(get_custom_dir(), None);
|
||||
set_config_name("config");
|
||||
let _ = fs::remove_dir_all(&temp_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config_path_resolution() {
|
||||
let _guard = lock_test();
|
||||
clear_custom_path();
|
||||
clear_custom_dir();
|
||||
set_config_name("config");
|
||||
|
||||
let path = get_config_path();
|
||||
let prog_name = get_program_name();
|
||||
let path_str = path.to_string_lossy();
|
||||
|
||||
assert!(
|
||||
path_str.contains(&prog_name),
|
||||
"Pfad {:?} sollte Programmnamen {} enthalten",
|
||||
path,
|
||||
prog_name
|
||||
);
|
||||
assert!(
|
||||
path_str.ends_with("config.toml"),
|
||||
"Pfad {:?} sollte auf config.toml enden",
|
||||
path
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_config_loading() {
|
||||
let _guard = lock_test();
|
||||
let cfg = DummyAppConfig::default();
|
||||
assert_eq!(cfg.general.log_level, "info");
|
||||
assert_eq!(cfg.general.apps_dir, "/var/apps");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_store_and_load_flow() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = env::temp_dir().join("test_store_and_load_flow.toml");
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let initial = CustomServerConfig {
|
||||
host: "192.168.1.100".to_string(),
|
||||
port: 9090,
|
||||
};
|
||||
|
||||
// Speichern
|
||||
let store_result = store(&initial);
|
||||
assert!(store_result.is_ok(), "Store sollte erfolgreich sein");
|
||||
assert!(temp_file.exists(), "Konfigurationsdatei sollte existieren");
|
||||
|
||||
// Laden
|
||||
let loaded: CustomServerConfig = load().expect("Load sollte erfolgreich sein");
|
||||
assert_eq!(loaded, initial);
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_load_config_fallback_and_invalid_toml() {
|
||||
let _guard = lock_test();
|
||||
let corrupt_file = env::temp_dir().join("corrupt_config_test.toml");
|
||||
cleanup_temp_file(&corrupt_file);
|
||||
fs::write(&corrupt_file, "INVALID_TOML_CONTENT = [[[[[").unwrap();
|
||||
set_custom_path(&corrupt_file);
|
||||
|
||||
// load::<T>() sollte bei ungültigem TOML fehlschlagen
|
||||
let res: Result<CustomServerConfig, ConfyError> = load();
|
||||
assert!(
|
||||
res.is_err(),
|
||||
"Laden von korruptem TOML sollte mit ConfyError fehlschlagen"
|
||||
);
|
||||
|
||||
// load_config::<T>() fällt im Fehlerfall auf Default zurück
|
||||
let loaded_default: CustomServerConfig = load_config();
|
||||
assert_eq!(loaded_default, CustomServerConfig::default());
|
||||
|
||||
cleanup_temp_file(&corrupt_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_config_and_load_config() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = env::temp_dir().join("test_save_config_convenience.toml");
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let server_cfg = CustomServerConfig {
|
||||
host: "0.0.0.0".to_string(),
|
||||
port: 443,
|
||||
};
|
||||
|
||||
save_config(server_cfg.clone());
|
||||
assert!(temp_file.exists());
|
||||
|
||||
let reloaded: CustomServerConfig = load_config();
|
||||
assert_eq!(reloaded, server_cfg);
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_function() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = env::temp_dir().join("test_modify_function.toml");
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let initial = CustomServerConfig {
|
||||
host: "10.0.0.1".to_string(),
|
||||
port: 80,
|
||||
};
|
||||
store(&initial).expect("Store initial config failed");
|
||||
|
||||
let modified_result = modify::<CustomServerConfig, _>(|cfg| {
|
||||
cfg.port = 8081;
|
||||
cfg.host = "10.0.0.2".to_string();
|
||||
});
|
||||
|
||||
assert!(modified_result.is_ok());
|
||||
let modified = modified_result.unwrap();
|
||||
assert_eq!(modified.port, 8081);
|
||||
assert_eq!(modified.host, "10.0.0.2");
|
||||
|
||||
let loaded: CustomServerConfig = load().expect("Reload failed");
|
||||
assert_eq!(loaded.port, 8081);
|
||||
assert_eq!(loaded.host, "10.0.0.2");
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_config_convenience() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = env::temp_dir().join("test_modify_config_convenience.toml");
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let initial = DatabaseConfig {
|
||||
url: "sqlite://memory".to_string(),
|
||||
max_connections: 5,
|
||||
};
|
||||
save_config(initial);
|
||||
|
||||
modify_config::<DatabaseConfig, _>(|cfg| {
|
||||
cfg.max_connections = 25;
|
||||
});
|
||||
|
||||
let loaded: DatabaseConfig = load_config();
|
||||
assert_eq!(loaded.max_connections, 25);
|
||||
assert_eq!(loaded.url, "sqlite://memory");
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct GlobalAppConfig {
|
||||
service_name: String,
|
||||
}
|
||||
|
||||
impl Default for GlobalAppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
service_name: "global_service".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct GlobalServerConfig {
|
||||
host: String,
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl Default for GlobalServerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
host: "127.0.0.1".to_string(),
|
||||
port: 8080,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
|
||||
struct ConcurrentTestConfig {
|
||||
worker_id: u32,
|
||||
}
|
||||
|
||||
impl Default for ConcurrentTestConfig {
|
||||
fn default() -> Self {
|
||||
Self { worker_id: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_config_singleton_cache() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = env::temp_dir().join("test_get_config_singleton.toml");
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let initial = DatabaseConfig {
|
||||
url: "postgres://prod-db:5432/main".to_string(),
|
||||
max_connections: 50,
|
||||
};
|
||||
save_config(initial.clone());
|
||||
|
||||
// Erster Aufruf: lädt und speichert im Cache
|
||||
let ref1: &'static DatabaseConfig = get_config();
|
||||
assert_eq!(ref1.url, "postgres://prod-db:5432/main");
|
||||
assert_eq!(ref1.max_connections, 50);
|
||||
|
||||
// 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"
|
||||
);
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_config_types_in_global_cache() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = env::temp_dir().join("test_multiple_types_cache.toml");
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&temp_file);
|
||||
|
||||
let app_cfg: &'static GlobalAppConfig = get_config();
|
||||
let srv_cfg: &'static GlobalServerConfig = get_config();
|
||||
|
||||
assert_eq!(app_cfg.service_name, "global_service");
|
||||
assert_eq!(srv_cfg.port, 8080);
|
||||
assert_eq!(srv_cfg.host, "127.0.0.1");
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_concurrent_access() {
|
||||
let _guard = lock_test();
|
||||
let temp_file = Arc::new(env::temp_dir().join("test_concurrent_access.toml"));
|
||||
cleanup_temp_file(&temp_file);
|
||||
set_custom_path(&*temp_file);
|
||||
|
||||
let initial = ConcurrentTestConfig { worker_id: 42 };
|
||||
save_config(initial);
|
||||
|
||||
let mut handles = Vec::new();
|
||||
for i in 0..10 {
|
||||
let handle = thread::spawn(move || {
|
||||
let cfg: &'static ConcurrentTestConfig = get_config();
|
||||
assert_eq!(cfg.worker_id, 42);
|
||||
let _ = load_config::<ConcurrentTestConfig>();
|
||||
if i % 2 == 0 {
|
||||
modify_config::<ConcurrentTestConfig, _>(|c| {
|
||||
c.worker_id = 1000 + i;
|
||||
});
|
||||
}
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
handle.join().unwrap();
|
||||
}
|
||||
|
||||
cleanup_temp_file(&temp_file);
|
||||
clear_custom_path();
|
||||
}
|
||||
Reference in New Issue
Block a user