Chore: Fügt CI/Tooling-Vorlagen aus dem persönlichen Rust-Projekt-Template hinzu
Ersetzt die alten, projektspezifischen Metadaten (Lizenzjahr/-inhaber, .gitignore) durch die Standardvorlage: Gitea-Workflows für CI/Security/ Renovate, Qodana-Konfiguration, Packaging-Skripte sowie IDE-/Cargo- Registry-Einstellungen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FZ3VzCWgbQRMyFEEKPvnZz
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
[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
|
||||
|
||||
[target.aarch64-unknown-linux-gnu]
|
||||
linker = "aarch64-linux-gnu-gcc"
|
||||
|
||||
[target.i686-unknown-linux-gnu]
|
||||
linker = "i686-linux-gnu-gcc"
|
||||
@@ -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
|
||||
@@ -0,0 +1,254 @@
|
||||
name: Main Release & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
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/main.yaml'
|
||||
|
||||
release-and-publish:
|
||||
name: Build, Publish Packages (Stable) & Create Release
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||
CARGO_DEB_VERSION: "3.8.0"
|
||||
CARGO_GENERATE_RPM_VERSION: "0.21.0"
|
||||
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: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||
run: rm -rf target/debian target/generate-rpm target/arch
|
||||
|
||||
- name: Install Cross-Compilation Toolchains (apt)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
|
||||
|
||||
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Rustup Cross-Compilation-Targets
|
||||
id: cache-rustup-targets
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
|
||||
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Add Rust Cross-Compilation Targets
|
||||
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||
run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
||||
|
||||
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||
run: |
|
||||
mkdir -p ~/.cargo/bin
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache Packaging-Tools (cargo-binstall, cargo-deb, cargo-generate-rpm)
|
||||
id: cache-packaging-tools
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-binstall
|
||||
~/.cargo/bin/cargo-deb
|
||||
~/.cargo/bin/cargo-generate-rpm
|
||||
key: packaging-tools-${{ runner.os }}-${{ env.CARGO_BINSTALL_VERSION }}-${{ env.CARGO_DEB_VERSION }}-${{ env.CARGO_GENERATE_RPM_VERSION }}
|
||||
|
||||
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||
if: steps.cache-packaging-tools.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-x86_64-unknown-linux-musl.tgz" | tar -xz -C ~/.cargo/bin
|
||||
~/.cargo/bin/cargo-binstall -y --no-symlinks "cargo-deb@${CARGO_DEB_VERSION}" "cargo-generate-rpm@${CARGO_GENERATE_RPM_VERSION}"
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
cargo test
|
||||
|
||||
- name: Build Release Binaries
|
||||
run: |
|
||||
cargo build --release --target x86_64-unknown-linux-gnu
|
||||
cargo build --release --target aarch64-unknown-linux-gnu
|
||||
cargo build --release --target i686-unknown-linux-gnu
|
||||
|
||||
- name: Determine Build Number
|
||||
id: build_num
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
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: |
|
||||
BUILD_NUM=$(python3 scripts/get-build-number.py)
|
||||
echo "build_number=${BUILD_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_NUMBER=${BUILD_NUM}" >> $GITHUB_ENV
|
||||
echo "Ermittelte Build-Nummer: ${BUILD_NUM}"
|
||||
|
||||
- name: Build Debian Packages (.deb)
|
||||
run: |
|
||||
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||
cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||
|
||||
- name: Build Fedora / RPM Packages (.rpm)
|
||||
run: |
|
||||
mkdir -p target/generate-rpm
|
||||
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||
cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||
|
||||
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
||||
run: |
|
||||
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||
python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||
|
||||
- name: Publish Packages to Gitea Package Registry
|
||||
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: |
|
||||
echo "Veröffentliche Debian-Paket (Distribution: stable, Component: main)..."
|
||||
for deb in target/debian/*.deb; do
|
||||
[ -f "$deb" ] || continue
|
||||
curl -f -s -S -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
--upload-file "$deb" \
|
||||
"${GITEA_URL}/api/packages/${REPO_OWNER}/debian/pool/stable/main/upload"
|
||||
done
|
||||
|
||||
echo "Veröffentliche Fedora/RPM-Paket (Gruppe: stable)..."
|
||||
for rpm in target/generate-rpm/*.rpm; do
|
||||
[ -f "$rpm" ] || continue
|
||||
curl -f -s -S -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
--upload-file "$rpm" \
|
||||
"${GITEA_URL}/api/packages/${REPO_OWNER}/rpm/stable/upload"
|
||||
done
|
||||
|
||||
echo "Veröffentliche Arch Linux-Paket (Repository: stable)..."
|
||||
for pkg in target/arch/*.pkg.tar.zst; do
|
||||
[ -f "$pkg" ] || continue
|
||||
curl -f -s -S -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
--upload-file "$pkg" \
|
||||
"${GITEA_URL}/api/packages/${REPO_OWNER}/arch/stable"
|
||||
done
|
||||
|
||||
- name: Create Gitea Release and Upload Assets
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
REPO_NAME: ${{ gitea.repository_name || github.event.repository.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 }}
|
||||
run: |
|
||||
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
TAG_NAME="v${VERSION}"
|
||||
RELEASE_TITLE="Release ${TAG_NAME}"
|
||||
RELEASE_NOTES="Automatisches Release für ${REPO_NAME} ${VERSION}."
|
||||
|
||||
echo "Erstelle oder hole Release für Tag ${TAG_NAME} in ${REPO}..."
|
||||
|
||||
GET_RESP=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG_NAME}")
|
||||
HTTP_CODE=$(echo "$GET_RESP" | tail -n1)
|
||||
BODY=$(echo "$GET_RESP" | sed '$d')
|
||||
|
||||
RELEASE_ID=""
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
RELEASE_ID=$(echo "$BODY" | jq -r '.id // empty' 2>/dev/null || echo "$BODY" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
echo "Bestehendes Release gefunden (ID: ${RELEASE_ID})."
|
||||
else
|
||||
echo "Erstelle neues Release ${TAG_NAME}..."
|
||||
CREATE_PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"tag_name": "${TAG_NAME}",
|
||||
"target_commitish": "main",
|
||||
"name": "${RELEASE_TITLE}",
|
||||
"body": "${RELEASE_NOTES}",
|
||||
"draft": false,
|
||||
"prerelease": false
|
||||
}
|
||||
EOF
|
||||
)
|
||||
CREATE_RESP=$(curl -f -s -S -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$CREATE_PAYLOAD" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases")
|
||||
RELEASE_ID=$(echo "$CREATE_RESP" | jq -r '.id // empty' 2>/dev/null || echo "$CREATE_RESP" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
echo "Neues Release erstellt (ID: ${RELEASE_ID})."
|
||||
fi
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Fehler: Release-ID konnte nicht ermittelt werden!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXISTING_ASSETS_JSON=$(curl -s \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" || echo "[]")
|
||||
|
||||
for file in target/debian/*.deb target/generate-rpm/*.rpm target/arch/*.pkg.tar.zst; do
|
||||
[ -f "$file" ] || continue
|
||||
filename="$(basename "$file")"
|
||||
echo "Lade Release-Asset hoch: $filename"
|
||||
|
||||
ASSET_ID=$(echo "$EXISTING_ASSETS_JSON" | jq -r --arg name "$filename" '.[]? | select(.name == $name) | .id' 2>/dev/null | head -n1 || true)
|
||||
if [ -n "$ASSET_ID" ] && [ "$ASSET_ID" != "null" ]; then
|
||||
echo "Lösche altes Asset mit ID ${ASSET_ID}..."
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}" || true
|
||||
fi
|
||||
|
||||
curl -f -s -S -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-F "attachment=@${file}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
|
||||
echo "Asset ${filename} erfolgreich hochgeladen."
|
||||
done
|
||||
@@ -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.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_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}"
|
||||
@@ -0,0 +1,254 @@
|
||||
name: Testing Build, Publish & Preview Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 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-publish:
|
||||
name: Build, Publish Packages (Testing) & Create Preview Release
|
||||
needs: detect-changes
|
||||
if: needs.detect-changes.outputs.code_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_BINSTALL_VERSION: "1.23.0"
|
||||
CARGO_DEB_VERSION: "3.8.0"
|
||||
CARGO_GENERATE_RPM_VERSION: "0.21.0"
|
||||
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: Alte Paketierungs-Ausgaben aus dem Cache entfernen
|
||||
run: rm -rf target/debian target/generate-rpm target/arch
|
||||
|
||||
- name: Install Cross-Compilation Toolchains (apt)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y gcc-aarch64-linux-gnu gcc-i686-linux-gnu
|
||||
|
||||
- name: Ermittle Rust-Version für Rustup-Target-Cache-Key
|
||||
run: echo "RUST_VERSION=$(rustc --version | awk '{print $2}')" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Cache Rustup Cross-Compilation-Targets
|
||||
id: cache-rustup-targets
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/aarch64-unknown-linux-gnu
|
||||
~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/i686-unknown-linux-gnu
|
||||
key: rustup-targets-${{ runner.os }}-${{ env.RUST_VERSION }}
|
||||
|
||||
- name: Add Rust Cross-Compilation Targets
|
||||
if: steps.cache-rustup-targets.outputs.cache-hit != 'true'
|
||||
run: rustup target add aarch64-unknown-linux-gnu i686-unknown-linux-gnu
|
||||
|
||||
- name: PATH um Cargo-bin-Verzeichnis ergänzen
|
||||
run: |
|
||||
mkdir -p ~/.cargo/bin
|
||||
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Cache Packaging-Tools (cargo-binstall, cargo-deb, cargo-generate-rpm)
|
||||
id: cache-packaging-tools
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/cargo-binstall
|
||||
~/.cargo/bin/cargo-deb
|
||||
~/.cargo/bin/cargo-generate-rpm
|
||||
key: packaging-tools-${{ runner.os }}-${{ env.CARGO_BINSTALL_VERSION }}-${{ env.CARGO_DEB_VERSION }}-${{ env.CARGO_GENERATE_RPM_VERSION }}
|
||||
|
||||
- name: Install Packaging Tools (Prebuilt Binaries)
|
||||
if: steps.cache-packaging-tools.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
curl -fsSL "https://github.com/cargo-bins/cargo-binstall/releases/download/v${CARGO_BINSTALL_VERSION}/cargo-binstall-x86_64-unknown-linux-musl.tgz" | tar -xz -C ~/.cargo/bin
|
||||
~/.cargo/bin/cargo-binstall -y --no-symlinks "cargo-deb@${CARGO_DEB_VERSION}" "cargo-generate-rpm@${CARGO_GENERATE_RPM_VERSION}"
|
||||
|
||||
- name: Run Tests
|
||||
run: |
|
||||
cargo test
|
||||
|
||||
- name: Build Release Binaries
|
||||
run: |
|
||||
cargo build --release --target x86_64-unknown-linux-gnu
|
||||
cargo build --release --target aarch64-unknown-linux-gnu
|
||||
cargo build --release --target i686-unknown-linux-gnu
|
||||
|
||||
- name: Determine Build Number
|
||||
id: build_num
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
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: |
|
||||
BUILD_NUM=$(python3 scripts/get-build-number.py)
|
||||
echo "build_number=${BUILD_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_NUMBER=${BUILD_NUM}" >> $GITHUB_ENV
|
||||
echo "Ermittelte Build-Nummer: ${BUILD_NUM}"
|
||||
|
||||
- name: Build Debian Packages (.deb)
|
||||
run: |
|
||||
cargo deb --target x86_64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||
cargo deb --target aarch64-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||
cargo deb --target i686-unknown-linux-gnu --deb-revision "${BUILD_NUMBER}" --no-build
|
||||
|
||||
- name: Build Fedora / RPM Packages (.rpm)
|
||||
run: |
|
||||
mkdir -p target/generate-rpm
|
||||
cargo generate-rpm --target x86_64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||
cargo generate-rpm --target aarch64-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||
cargo generate-rpm --target i686-unknown-linux-gnu -s "release=\"${BUILD_NUMBER}\"" -o target/generate-rpm
|
||||
|
||||
- name: Build Arch Linux Packages (.pkg.tar.zst)
|
||||
run: |
|
||||
python3 scripts/package-arch.py --target x86_64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||
python3 scripts/package-arch.py --target aarch64-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||
python3 scripts/package-arch.py --target i686-unknown-linux-gnu --pkgrel "${BUILD_NUMBER}"
|
||||
|
||||
- name: Publish Packages to Gitea Package Registry
|
||||
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: |
|
||||
echo "Veröffentliche Debian-Paket (Distribution: testing, Component: main)..."
|
||||
for deb in target/debian/*.deb; do
|
||||
[ -f "$deb" ] || continue
|
||||
curl -f -s -S -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
--upload-file "$deb" \
|
||||
"${GITEA_URL}/api/packages/${REPO_OWNER}/debian/pool/testing/main/upload"
|
||||
done
|
||||
|
||||
echo "Veröffentliche Fedora/RPM-Paket (Gruppe: testing)..."
|
||||
for rpm in target/generate-rpm/*.rpm; do
|
||||
[ -f "$rpm" ] || continue
|
||||
curl -f -s -S -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
--upload-file "$rpm" \
|
||||
"${GITEA_URL}/api/packages/${REPO_OWNER}/rpm/testing/upload"
|
||||
done
|
||||
|
||||
echo "Veröffentliche Arch Linux-Paket (Repository: testing)..."
|
||||
for pkg in target/arch/*.pkg.tar.zst; do
|
||||
[ -f "$pkg" ] || continue
|
||||
curl -f -s -S -X PUT \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
--upload-file "$pkg" \
|
||||
"${GITEA_URL}/api/packages/${REPO_OWNER}/arch/testing"
|
||||
done
|
||||
|
||||
- name: Create Gitea Pre-Release and Upload Assets
|
||||
env:
|
||||
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||
REPO: ${{ gitea.repository || github.repository }}
|
||||
REPO_NAME: ${{ gitea.repository_name || github.event.repository.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 }}
|
||||
run: |
|
||||
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -n1)"
|
||||
TAG_NAME="v${VERSION}-preview"
|
||||
RELEASE_TITLE="Preview Release ${TAG_NAME}"
|
||||
RELEASE_NOTES="Automatisches Preview-Release für ${REPO_NAME} ${VERSION} (Branch: Testing)."
|
||||
|
||||
echo "Erstelle oder hole Preview-Release für Tag ${TAG_NAME} in ${REPO}..."
|
||||
|
||||
GET_RESP=$(curl -s -w "\n%{http_code}" \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG_NAME}")
|
||||
HTTP_CODE=$(echo "$GET_RESP" | tail -n1)
|
||||
BODY=$(echo "$GET_RESP" | sed '$d')
|
||||
|
||||
RELEASE_ID=""
|
||||
if [ "$HTTP_CODE" -eq 200 ]; then
|
||||
RELEASE_ID=$(echo "$BODY" | jq -r '.id // empty' 2>/dev/null || echo "$BODY" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
echo "Bestehendes Release gefunden (ID: ${RELEASE_ID})."
|
||||
else
|
||||
echo "Erstelle neues Preview-Release ${TAG_NAME}..."
|
||||
CREATE_PAYLOAD=$(cat <<EOF
|
||||
{
|
||||
"tag_name": "${TAG_NAME}",
|
||||
"target_commitish": "testing",
|
||||
"name": "${RELEASE_TITLE}",
|
||||
"body": "${RELEASE_NOTES}",
|
||||
"draft": false,
|
||||
"prerelease": true
|
||||
}
|
||||
EOF
|
||||
)
|
||||
CREATE_RESP=$(curl -f -s -S -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$CREATE_PAYLOAD" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases")
|
||||
RELEASE_ID=$(echo "$CREATE_RESP" | jq -r '.id // empty' 2>/dev/null || echo "$CREATE_RESP" | grep -o '"id":[0-9]*' | head -n1 | cut -d: -f2)
|
||||
echo "Neues Preview-Release erstellt (ID: ${RELEASE_ID})."
|
||||
fi
|
||||
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Fehler: Release-ID konnte nicht ermittelt werden!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXISTING_ASSETS_JSON=$(curl -s \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" || echo "[]")
|
||||
|
||||
for file in target/debian/*.deb target/generate-rpm/*.rpm target/arch/*.pkg.tar.zst; do
|
||||
[ -f "$file" ] || continue
|
||||
filename="$(basename "$file")"
|
||||
echo "Lade Release-Asset hoch: $filename"
|
||||
|
||||
ASSET_ID=$(echo "$EXISTING_ASSETS_JSON" | jq -r --arg name "$filename" '.[]? | select(.name == $name) | .id' 2>/dev/null | head -n1 || true)
|
||||
if [ -n "$ASSET_ID" ] && [ "$ASSET_ID" != "null" ]; then
|
||||
echo "Lösche altes Asset mit ID ${ASSET_ID}..."
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${ASSET_ID}" || true
|
||||
fi
|
||||
|
||||
curl -f -s -S -X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-F "attachment=@${file}" \
|
||||
"${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
|
||||
echo "Asset ${filename} erfolgreich hochgeladen."
|
||||
done
|
||||
@@ -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 }}
|
||||
+1
-401
@@ -4,10 +4,6 @@
|
||||
debug/
|
||||
target/
|
||||
|
||||
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
|
||||
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
|
||||
Cargo.lock
|
||||
|
||||
# These are backup files generated by rustfmt
|
||||
**/*.rs.bk
|
||||
|
||||
@@ -113,400 +109,4 @@ fabric.properties
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
# ---> VisualStudio
|
||||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# Visual Studio 2015/2017 cache/options directory
|
||||
.vs/
|
||||
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||
#wwwroot/
|
||||
|
||||
# Visual Studio 2017 auto generated files
|
||||
Generated\ Files/
|
||||
|
||||
# MSTest test Results
|
||||
[Tt]est[Rr]esult*/
|
||||
[Bb]uild[Ll]og.*
|
||||
|
||||
# NUnit
|
||||
*.VisualState.xml
|
||||
TestResult.xml
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET Core
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
# but not Directory.Build.rsp, as it configures directory-level build defaults
|
||||
!Directory.Build.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
*.pidb
|
||||
*.svclog
|
||||
*.scc
|
||||
|
||||
# Chutzpah Test files
|
||||
_Chutzpah*
|
||||
|
||||
# Visual C++ cache files
|
||||
ipch/
|
||||
*.aps
|
||||
*.ncb
|
||||
*.opendb
|
||||
*.opensdf
|
||||
*.sdf
|
||||
*.cachefile
|
||||
*.VC.db
|
||||
*.VC.VC.opendb
|
||||
|
||||
# Visual Studio profiler
|
||||
*.psess
|
||||
*.vsp
|
||||
*.vspx
|
||||
*.sap
|
||||
|
||||
# Visual Studio Trace Files
|
||||
*.e2e
|
||||
|
||||
# TFS 2012 Local Workspace
|
||||
$tf/
|
||||
|
||||
# Guidance Automation Toolkit
|
||||
*.gpState
|
||||
|
||||
# ReSharper is a .NET coding add-in
|
||||
_ReSharper*/
|
||||
*.[Rr]e[Ss]harper
|
||||
*.DotSettings.user
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# Visual Studio code coverage results
|
||||
*.coverage
|
||||
*.coveragexml
|
||||
|
||||
# NCrunch
|
||||
_NCrunch_*
|
||||
.*crunch*.local.xml
|
||||
nCrunchTemp_*
|
||||
|
||||
# MightyMoose
|
||||
*.mm.*
|
||||
AutoTest.Net/
|
||||
|
||||
# Web workbench (sass)
|
||||
.sass-cache/
|
||||
|
||||
# Installshield output folder
|
||||
[Ee]xpress/
|
||||
|
||||
# DocProject is a documentation generator add-in
|
||||
DocProject/buildhelp/
|
||||
DocProject/Help/*.HxT
|
||||
DocProject/Help/*.HxC
|
||||
DocProject/Help/*.hhc
|
||||
DocProject/Help/*.hhk
|
||||
DocProject/Help/*.hhp
|
||||
DocProject/Help/Html2
|
||||
DocProject/Help/html
|
||||
|
||||
# Click-Once directory
|
||||
publish/
|
||||
|
||||
# Publish Web Output
|
||||
*.[Pp]ublish.xml
|
||||
*.azurePubxml
|
||||
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||
# but database connection strings (with potential passwords) will be unencrypted
|
||||
*.pubxml
|
||||
*.publishproj
|
||||
|
||||
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||
# in these scripts will be unencrypted
|
||||
PublishScripts/
|
||||
|
||||
# NuGet Packages
|
||||
*.nupkg
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# The packages folder can be ignored because of Package Restore
|
||||
**/[Pp]ackages/*
|
||||
# except build/, which is used as an MSBuild target.
|
||||
!**/[Pp]ackages/build/
|
||||
# Uncomment if necessary however generally it will be regenerated when needed
|
||||
#!**/[Pp]ackages/repositories.config
|
||||
# NuGet v3's project.json files produces more ignorable files
|
||||
*.nuget.props
|
||||
*.nuget.targets
|
||||
|
||||
# Microsoft Azure Build Output
|
||||
csx/
|
||||
*.build.csdef
|
||||
|
||||
# Microsoft Azure Emulator
|
||||
ecf/
|
||||
rcf/
|
||||
|
||||
# Windows Store app package directories and files
|
||||
AppPackages/
|
||||
BundleArtifacts/
|
||||
Package.StoreAssociation.xml
|
||||
_pkginfo.txt
|
||||
*.appx
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# Visual Studio cache files
|
||||
# files ending in .cache can be ignored
|
||||
*.[Cc]ache
|
||||
# but keep track of directories ending in .cache
|
||||
!?*.[Cc]ache/
|
||||
|
||||
# Others
|
||||
ClientBin/
|
||||
~$*
|
||||
*~
|
||||
*.dbmdl
|
||||
*.dbproj.schemaview
|
||||
*.jfm
|
||||
*.pfx
|
||||
*.publishsettings
|
||||
orleans.codegen.cs
|
||||
|
||||
# Including strong name files can present a security risk
|
||||
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||
#*.snk
|
||||
|
||||
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||
#bower_components/
|
||||
|
||||
# RIA/Silverlight projects
|
||||
Generated_Code/
|
||||
|
||||
# Backup & report files from converting an old project file
|
||||
# to a newer Visual Studio version. Backup files are not needed,
|
||||
# because we have git ;-)
|
||||
_UpgradeReport_Files/
|
||||
Backup*/
|
||||
UpgradeLog*.XML
|
||||
UpgradeLog*.htm
|
||||
ServiceFabricBackup/
|
||||
*.rptproj.bak
|
||||
|
||||
# SQL Server files
|
||||
*.mdf
|
||||
*.ldf
|
||||
*.ndf
|
||||
|
||||
# Business Intelligence projects
|
||||
*.rdl.data
|
||||
*.bim.layout
|
||||
*.bim_*.settings
|
||||
*.rptproj.rsuser
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).rdl
|
||||
|
||||
# Microsoft Fakes
|
||||
FakesAssemblies/
|
||||
|
||||
# GhostDoc plugin setting file
|
||||
*.GhostDoc.xml
|
||||
|
||||
# Node.js Tools for Visual Studio
|
||||
.ntvs_analysis.dat
|
||||
node_modules/
|
||||
|
||||
# Visual Studio 6 build log
|
||||
*.plg
|
||||
|
||||
# Visual Studio 6 workspace options file
|
||||
*.opt
|
||||
|
||||
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||
*.vbw
|
||||
|
||||
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
|
||||
# Visual Studio LightSwitch build output
|
||||
**/*.HTMLClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/GeneratedArtifacts
|
||||
**/*.DesktopClient/ModelManifest.xml
|
||||
**/*.Server/GeneratedArtifacts
|
||||
**/*.Server/ModelManifest.xml
|
||||
_Pvt_Extensions
|
||||
|
||||
# Paket dependency manager
|
||||
.paket/paket.exe
|
||||
paket-files/
|
||||
|
||||
# FAKE - F# Make
|
||||
.fake/
|
||||
|
||||
# CodeRush personal settings
|
||||
.cr/personal
|
||||
|
||||
# Python Tools for Visual Studio (PTVS)
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Cake - Uncomment if you are using it
|
||||
# tools/**
|
||||
# !tools/packages.config
|
||||
|
||||
# Tabs Studio
|
||||
*.tss
|
||||
|
||||
# Telerik's JustMock configuration file
|
||||
*.jmconfig
|
||||
|
||||
# BizTalk build output
|
||||
*.btp.cs
|
||||
*.btm.cs
|
||||
*.odx.cs
|
||||
*.xsd.cs
|
||||
|
||||
# OpenCover UI analysis results
|
||||
OpenCover/
|
||||
|
||||
# Azure Stream Analytics local run output
|
||||
ASALocalRun/
|
||||
|
||||
# MSBuild Binary and Structured Log
|
||||
*.binlog
|
||||
|
||||
# NVidia Nsight GPU debugger configuration file
|
||||
*.nvuser
|
||||
|
||||
# MFractors (Xamarin productivity tool) working folder
|
||||
.mfractor/
|
||||
|
||||
# Local History for Visual Studio
|
||||
.localhistory/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
.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
+49
@@ -0,0 +1,49 @@
|
||||
<?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/version-bump.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/unit-tests.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/trufflehog-scan.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/testing-to-main-pr.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/testing.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/security-scan.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/renovate.yaml" />
|
||||
</Item>
|
||||
<Item>
|
||||
<option name="path" value=".gitea/workflows/main.yaml" />
|
||||
</Item>
|
||||
</list>
|
||||
</option>
|
||||
</SchemaInfo>
|
||||
</value>
|
||||
</entry>
|
||||
</map>
|
||||
</state>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,138 @@
|
||||
# AGENTS.md
|
||||
|
||||
Dieses Dokument dient als technischer Leitfaden und Kontextdokument für KI-Coding-Agenten (sowie Entwickler), die an diesem Repository oder daraus abgeleiteten Projekten arbeiten.
|
||||
|
||||
---
|
||||
|
||||
## 1. Projektübersicht & Philosophie
|
||||
|
||||
Dieses Repository ist ein **Rust-Projekt-Template** für Linux-Anwendungen und CLI-Tools mit Fokus auf:
|
||||
- Automatisierte Multi-Architektur-Kompilierung (`x86_64`, `aarch64`, `i686`).
|
||||
- Native Paketierung für Debian (`.deb`), Fedora/RHEL (`.rpm`) und Arch Linux (`.pkg.tar.zst`) sowie Docker-Container-Images.
|
||||
- Vollständig automatisierte CI/CD-Pipelines via Gitea Actions (kompatibel mit Forgejo / GitHub Actions).
|
||||
- Automatisierte Sicherheits-Scans (Schwachstellen, Secrets) und Dependency-Updates.
|
||||
|
||||
### Standards & Tech-Stack
|
||||
- **Sprache**: Rust (Edition 2024), Python 3 (für Hilfsskripte in `scripts/`).
|
||||
- **Rust Toolchain**: Stable.
|
||||
- **Zielplattform**: Linux (GLIBC-basiert, Cross-Kompilierung für `x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `i686-unknown-linux-gnu`).
|
||||
- **Container**: Docker-Images werden zusätzlich zu den nativen Paketen gebaut und in die Gitea Container Registry veröffentlicht.
|
||||
- **Sicherheits-Tooling**: Trivy, OSV-Scanner, TruffleHog (Secret-Scanning), Renovate (Dependency-Updates), Qodana (statische Codeanalyse).
|
||||
- **Lizenz**: GPL-3.0-or-later (sofern nicht im abgeleiteten Projekt anders definiert).
|
||||
|
||||
---
|
||||
|
||||
## 2. Projektstruktur
|
||||
|
||||
```text
|
||||
├── .cargo/
|
||||
│ └── config.toml # Linker für Cross-Target-Kompilierung & Registry-Konfiguration
|
||||
├── .gitea/
|
||||
│ └── workflows/
|
||||
│ ├── main.yaml # CI/CD: Stabile Builds, Multi-Arch-Paketierung, Docker-Image, Release & Upload
|
||||
│ ├── testing.yaml # CI/CD: Preview-Builds, Docker-Image & Testing-Pakete
|
||||
│ ├── unit-tests.yaml # CI: Unit-Tests für Pull Requests gegen 'testing'
|
||||
│ ├── security-scan.yaml # CI: Trivy & OSV-Scanner (Schwachstellen/Misconfig/Secrets)
|
||||
│ ├── trufflehog-scan.yaml # CI: TruffleHog Secret-Scan (inkl. Git-Historie)
|
||||
│ └── renovate.yaml # CI: Wöchentlicher Renovate-Lauf für Dependency-Updates
|
||||
├── scripts/
|
||||
│ ├── get-build-number.py # Ermittelt automatisch die nächste Revisions-/Build-Nummer
|
||||
│ ├── package-arch.py # Erzeugt native Arch Linux .pkg.tar.zst Pakete
|
||||
│ └── report-security-issue.py # Meldet Scan-Ergebnisse (Trivy/OSV/TruffleHog) als Gitea-Issue
|
||||
├── src/
|
||||
│ └── main.rs # Einstiegspunkt der Anwendung
|
||||
├── Cargo.toml # Projekt-Manifest & Metadaten für deb, rpm und arch
|
||||
├── qodana.yaml # Konfiguration für JetBrains Qodana (statische Analyse)
|
||||
├── renovate.json # Renovate-Konfiguration (Gruppierung, Versions-Pins in Workflows)
|
||||
├── LICENSE # Lizenztext
|
||||
├── README.md # Benutzerdokumentation & Setup-Checkliste
|
||||
└── AGENTS.md # Dieses Agenten-Handbuch
|
||||
```
|
||||
|
||||
> **Hinweis:** `main.yaml`/`testing.yaml` bauen zusätzlich ein Docker-Image (`docker build .` mit `--build-arg TARGET_BIN=...`). Ein `Dockerfile` ist im Template noch **nicht** enthalten und muss von abgeleiteten Projekten ergänzt werden; der aktuell hartkodierte `TARGET_BIN`-Pfad (`.../release/mirror-package`) ist ein Platzhalter aus einem Referenzprojekt und muss beim Ableiten des Templates auf den tatsächlichen Binärnamen (`Cargo.toml` → `[package] name`) angepasst werden.
|
||||
|
||||
---
|
||||
|
||||
## 3. Regeln & Richtlinien für Agenten
|
||||
|
||||
### 3.1 Code-Stil & Best Practices
|
||||
- **Idiomatisches Rust**: Nutze moderne Sprachfeatures der Rust Edition 2024.
|
||||
- **Fehlerbehandlung**: Verwende aussagekräftige Fehlertypen (z. B. mit `thiserror` oder `anyhow` für CLIs). Vermeide unnötiges `unwrap()` oder `panic!()` im Produktivcode.
|
||||
- **Kommentare**: Ergänze KDoc/RustDoc-Kommentare (`///`) an öffentlichen Funktionen und Typen. Behalte die bestehende Sprachkonvention bei.
|
||||
- **Template-TODOs**: Wenn neue Vorlagen-Features oder Platzhalter ergänzt werden, markiere anpassungsbedürftige Stellen eindeutig mit `// TODO:` (Rust), `# TODO:` (TOML/Python/YAML).
|
||||
|
||||
### 3.2 Paketierungs-Metadaten in `Cargo.toml`
|
||||
Bei Änderungen an Binärnamen, Abhängigkeiten oder Beschreibungen müssen die drei Metadaten-Blöcke in `Cargo.toml` synchron gehalten werden:
|
||||
1. `[package.metadata.deb]` (für `cargo-deb`):
|
||||
- `maintainer`, `copyright`, `section`, `priority`, `depends`, `extended-description`, `assets`.
|
||||
2. `[package.metadata.generate-rpm]` (für `cargo-generate-rpm`):
|
||||
- `requires`, `assets`.
|
||||
3. `[package.metadata.arch]` (für `scripts/package-arch.py`):
|
||||
- `pkgrel`, `arch`, `depends`, `optdepends`.
|
||||
|
||||
Ändert sich der Binärname (`[package] name`), muss auch der `TARGET_BIN`-Build-Arg im Docker-Build-Step von `main.yaml`/`testing.yaml` sowie das (abzuleitende) `Dockerfile` angepasst werden.
|
||||
|
||||
### 3.3 Skripte in `scripts/`
|
||||
- **Generizität**: Die Skripte dürfen keine hardcodierten Anwendungsnamen, spezifischen Abhängigkeiten oder projektspezifischen URLs enthalten. Alle Werte müssen dynamisch aus `Cargo.toml` (via `cargo metadata` oder Dateiparsing) oder Umgebungsvariablen (`BUILD_NUMBER`, `GITEA_URL`, `REPO`, `TOKEN`) ermittelt werden.
|
||||
- **Python-Kompatibilität**: Verwende Standard-Python 3 ohne externe PyPI-Abhängigkeiten (nur Standardbibliothek: `json`, `subprocess`, `urllib`, `argparse`, `os`, `re`, `tempfile`, `tarfile` etc.).
|
||||
- **`get-build-number.py`**: Ermittelt die nächste Build-/Revisions-Nummer nicht mehr rein lokal, sondern dynamisch über:
|
||||
1. Gitea Releases API (Tag-/Asset-Namen),
|
||||
2. Gitea Packages API (jeweils neueste Version pro Paket-Typ: `debian`, `rpm`, `arch`),
|
||||
3. lokale `target/{debian,generate-rpm,arch}`-Verzeichnisse als Fallback.
|
||||
Unterstützt CLI-Flags (`--version`, `--gitea-url`, `--repo`, `--owner`, `--token`, `--build-number`) sowie Umgebungsvariablen-Fallbacks (`BUILD_NUMBER`/`BUILD_NUM`/`PKGREL`, `GITEA_URL`, `REPO`, `REPO_OWNER`, `TOKEN`). Package-Typen (deb/rpm/arch) teilen sich eine gemeinsame Build-Nummer.
|
||||
- **`report-security-issue.py`**: Fasst Funde aus Trivy-, OSV-Scanner- und TruffleHog-JSON-Reports zusammen, sortiert nach Schweregrad und pflegt darüber ein einzelnes offenes Gitea-Issue pro Scan-Typ (Kommentar-Historie statt ständig neuer Issues; Label per `ISSUE_LABEL`, Titel per `ISSUE_TITLE` konfigurierbar). Secret-Werte selbst werden nie ausgegeben.
|
||||
|
||||
---
|
||||
|
||||
## 4. Workflows: Bauen, Testen & Validieren
|
||||
|
||||
Agenten müssen Änderungen vor dem Abschluss validieren.
|
||||
|
||||
### 4.1 Grundlegende Validierung
|
||||
```bash
|
||||
# Syntax- und Typprüfung
|
||||
cargo check
|
||||
|
||||
# Unit- & Integrationstests
|
||||
cargo test
|
||||
|
||||
# Release-Build prüfen
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### 4.2 Hilfsskripte testen
|
||||
```bash
|
||||
# Build-Nummern-Skript testen (rein lokal, ohne Gitea-API)
|
||||
python3 scripts/get-build-number.py
|
||||
|
||||
# Arch Linux Paketierung lokal testen (nach 'cargo build --release')
|
||||
python3 scripts/package-arch.py --arch x86_64 --pkgrel 1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. CI/CD-Pipeline Details
|
||||
|
||||
Alle Workflows liegen unter `.gitea/workflows/` und nutzen gecachte Abhängigkeiten (`actions/cache@v6` für Cargo-Registry/Build-Artefakte, Rustup-Targets und Packaging-Tools) sowie fest gepinnte Tool-Versionen über `env`-Variablen — diese werden von Renovate automatisch aktuell gehalten (siehe 5.2).
|
||||
|
||||
### 5.1 Build & Release
|
||||
- **`main.yaml`** (Trigger: `push` auf `main`): Baut Binaries für alle 3 Architekturen, führt `cargo test` aus, baut `.deb`, `.rpm` und `.pkg.tar.zst`, lädt sie in die Gitea Package Registry hoch, erstellt ein Gitea Release `v<VERSION>` und baut/veröffentlicht zusätzlich ein Docker-Image (`:latest`, `:<VERSION>`, `:v<VERSION>`, `:<VERSION>.<BUILD_NUMBER>`).
|
||||
- **`testing.yaml`** (Trigger: `push` auf `testing`): Analog zu `main.yaml`, veröffentlicht jedoch in die `testing`-Kanäle der Paket-Registry, erstellt ein Pre-Release `v<VERSION>-preview` und taggt Docker-Images mit `:testing`, `:<VERSION>-preview`, `:<VERSION>-testing` etc.
|
||||
- **`unit-tests.yaml`** (Trigger: `pull_request` → `testing`, bei `opened`/`synchronize`/`reopened`): Schnelle CI-Prüfung (`cargo test`) für Pull Requests, ohne Paketierung oder Veröffentlichung.
|
||||
|
||||
### 5.2 Sicherheit & Qualität
|
||||
- **`security-scan.yaml`** (Trigger: `push` auf `main`/`testing`/`dev`, `pull_request`, wöchentlich montags 05:00 UTC, `workflow_dispatch`): Führt Trivy (`vuln`, `secret`, `misconfig`; Schweregrad `CRITICAL`/`HIGH`) und OSV-Scanner aus und meldet Funde über `scripts/report-security-issue.py` als Gitea-Issue (Label `security-scan`).
|
||||
- **`trufflehog-scan.yaml`** (Trigger: `push`, `pull_request`, wöchentlich montags 06:00 UTC, `workflow_dispatch`): Durchsucht die volle Git-Historie (`fetch-depth: 0`) nach verifizierten/unbekannten Secrets und meldet Funde in ein eigenes Issue (Label `security-scan-trufflehog`) inkl. Rotations-Anleitung.
|
||||
- **`qodana.yaml`**: Konfiguration für JetBrains Qodana (statische Codeanalyse, Profil `qodana.starter`); wird über eine externe/IDE-seitige Qodana-CI-Integration ausgeführt, nicht über einen eigenen Gitea-Workflow in diesem Template.
|
||||
- **`renovate.yaml`** (Trigger: wöchentlich montags 04:00 UTC, `workflow_dispatch`): Führt Renovate (Container-Image, gepinnte Version) gegen die Gitea-Plattform aus und erstellt Pull Requests für veraltete Abhängigkeiten gemäß `renovate.json`.
|
||||
|
||||
### 5.3 Renovate-Konfiguration (`renovate.json`)
|
||||
- Basis-Branch für PRs: `dev`.
|
||||
- Gruppiert Updates nach `Gitea Actions`, `Cargo Dependencies` und `Docker-Images`.
|
||||
- Custom-Regex-Manager halten die in den Workflows gepinnten Tool-Versionen aktuell: `TRIVY_VERSION`, `OSV_SCANNER_VERSION`, `TRUFFLEHOG_VERSION`, `CARGO_BINSTALL_VERSION`, `CARGO_DEB_VERSION`, `CARGO_GENERATE_RPM_VERSION`.
|
||||
- Zeitplan: vor 6 Uhr montags (Europe/Berlin).
|
||||
|
||||
### 5.4 Secrets
|
||||
- `PACKAGE_TOKEN` (Fallback-Kette: `RELEASE_TOKEN`, `PUBLISH_TOKEN`, `API_TOKEN`, `PAT_TOKEN`, `CUSTOM_TOKEN`, `GITEA_TOKEN`, `GITHUB_TOKEN`) — API-Zugriff auf Gitea Packages, Releases und die Container Registry.
|
||||
- `SECURITY_TOKEN` — wird von `security-scan.yaml` und `trufflehog-scan.yaml` für das Erstellen/Kommentieren von Gitea-Issues verwendet.
|
||||
- `RENOVATE_TOKEN` — Zugriffstoken für den Renovate-Lauf (Gitea-Plattform-Endpoint & Cargo-Registry-Host-Rule).
|
||||
@@ -209,7 +209,7 @@ 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.
|
||||
|
||||
SmartMount
|
||||
Copyright (C) 2025 DragonSlayer_14
|
||||
Copyright (C) 2026 creative-dragonslayer.de
|
||||
|
||||
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:
|
||||
|
||||
SmartMount Copyright (C) 2025 DragonSlayer_14
|
||||
SmartMount Copyright (C) 2026 creative-dragonslayer.de
|
||||
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.
|
||||
|
||||
|
||||
+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"
|
||||
}
|
||||
]
|
||||
}
|
||||
Executable
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Ermittelt automatisch die nächste Build-Nummer (Revision / Release / pkgrel)
|
||||
für eine gegebene Paket-Version (z. B. 1.0.2 -> wenn 1.0.2-1 existiert, wird 2 zurückgegeben).
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
# Paket-Typen, unter denen dasselbe Release veroeffentlicht wird und die sich
|
||||
# daher eine gemeinsame Build-Nummer teilen muessen.
|
||||
PACKAGE_TYPES = ("debian", "rpm", "arch")
|
||||
|
||||
|
||||
def get_cargo_pkg_info():
|
||||
try:
|
||||
metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"]))
|
||||
pkg = metadata["packages"][0]
|
||||
name = pkg.get("name", "")
|
||||
version = pkg.get("version", "0.0.0")
|
||||
repository = pkg.get("repository", "")
|
||||
return name, version, repository
|
||||
except Exception:
|
||||
# Fallback auf einfaches Parsen der Cargo.toml
|
||||
name = ""
|
||||
version = "0.0.0"
|
||||
repository = ""
|
||||
if os.path.exists("Cargo.toml"):
|
||||
with open("Cargo.toml", "r") as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith("name ="):
|
||||
name = line.split("=", 1)[1].strip().strip('"\'')
|
||||
elif line.startswith("version ="):
|
||||
version = line.split("=", 1)[1].strip().strip('"\'')
|
||||
elif line.startswith("repository ="):
|
||||
repository = line.split("=", 1)[1].strip().strip('"\'')
|
||||
return name, version, repository
|
||||
|
||||
|
||||
def parse_repo_info(repo_url):
|
||||
"""Extrahiert Server-URL, Owner und Repo-Name aus einer Repository-URL."""
|
||||
if not repo_url:
|
||||
return None, None, None
|
||||
parsed = urllib.parse.urlparse(repo_url)
|
||||
server_url = f"{parsed.scheme}://{parsed.netloc}"
|
||||
path_parts = [p for p in parsed.path.strip("/").split("/") if p]
|
||||
if len(path_parts) >= 2:
|
||||
owner = path_parts[0]
|
||||
repo_name = path_parts[1]
|
||||
if repo_name.endswith(".git"):
|
||||
repo_name = repo_name[:-4]
|
||||
return server_url, f"{owner}/{repo_name}", owner
|
||||
return server_url, None, None
|
||||
|
||||
|
||||
def query_existing_build_numbers(name, version, gitea_url, repo, owner, token=None):
|
||||
build_nums = set()
|
||||
pattern = re.compile(rf'(?:{re.escape(name)}[_-]|^v?){re.escape(version)}-(\d+)')
|
||||
|
||||
headers = {"User-Agent": f"{name}-build-resolver" if name else "rust-build-resolver"}
|
||||
if token:
|
||||
headers["Authorization"] = f"token {token}"
|
||||
|
||||
# 1. Gitea Releases API
|
||||
if gitea_url and repo:
|
||||
try:
|
||||
url = f"{gitea_url.rstrip('/')}/api/v1/repos/{repo}/releases?limit=50"
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
releases = json.loads(resp.read().decode())
|
||||
for rel in releases:
|
||||
tag = rel.get("tag_name", "")
|
||||
m = pattern.search(tag)
|
||||
if m:
|
||||
build_nums.add(int(m.group(1)))
|
||||
for asset in rel.get("assets", []):
|
||||
asset_name = asset.get("name", "")
|
||||
m = pattern.search(asset_name)
|
||||
if m:
|
||||
build_nums.add(int(m.group(1)))
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[Hinweis] Konnte Gitea Releases nicht abfragen: {e}\n")
|
||||
|
||||
# 2. Gitea Packages API (falls Token vorhanden): pro Paket-Typ gezielt nur die
|
||||
# neueste Version abfragen (ein Request, keine Pagination noetig). Das haelt
|
||||
# die Abfrage schnell unabhaengig von der Historie und ist unbeeinflusst von
|
||||
# Docker-Tags/anderen Paket-Typen, die unter demselben Owner haengen.
|
||||
if gitea_url and owner and token and name:
|
||||
for pkg_type in PACKAGE_TYPES:
|
||||
try:
|
||||
url = (
|
||||
f"{gitea_url.rstrip('/')}/api/v1/packages/{owner}/"
|
||||
f"{pkg_type}/{urllib.parse.quote(name, safe='')}/-/latest"
|
||||
)
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
pkg = json.loads(resp.read().decode())
|
||||
pkg_ver = pkg.get("version", "")
|
||||
m = pattern.search(pkg_ver)
|
||||
if m:
|
||||
build_nums.add(int(m.group(1)))
|
||||
except urllib.error.HTTPError as e:
|
||||
if e.code != 404:
|
||||
sys.stderr.write(f"[Hinweis] Konnte neueste {pkg_type}-Paketversion nicht abfragen: {e}\n")
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[Hinweis] Konnte neueste {pkg_type}-Paketversion nicht abfragen: {e}\n")
|
||||
|
||||
# 3. Lokales Target-Verzeichnis prüfen
|
||||
target_dirs = ["target/debian", "target/generate-rpm", "target/arch"]
|
||||
for t_dir in target_dirs:
|
||||
if os.path.isdir(t_dir):
|
||||
for file in os.listdir(t_dir):
|
||||
m = pattern.search(file)
|
||||
if m:
|
||||
build_nums.add(int(m.group(1)))
|
||||
|
||||
return build_nums
|
||||
|
||||
|
||||
def get_next_build_number(version=None, gitea_url=None, repo=None, owner=None, token=None, explicit_build_num=None):
|
||||
if explicit_build_num is not None:
|
||||
return int(explicit_build_num)
|
||||
|
||||
env_build_num = os.environ.get("BUILD_NUMBER") or os.environ.get("BUILD_NUM") or os.environ.get("PKGREL")
|
||||
if env_build_num:
|
||||
try:
|
||||
return int(env_build_num)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
pkg_name, pkg_version, pkg_repo = get_cargo_pkg_info()
|
||||
if not version:
|
||||
version = pkg_version
|
||||
|
||||
default_server, default_repo, default_owner = parse_repo_info(pkg_repo)
|
||||
|
||||
gitea_url = gitea_url or os.environ.get("GITEA_URL") or os.environ.get("GITHUB_SERVER_URL") or default_server
|
||||
repo = repo or os.environ.get("REPO") or os.environ.get("GITHUB_REPOSITORY") or default_repo
|
||||
owner = owner or os.environ.get("REPO_OWNER") or os.environ.get("GITHUB_REPOSITORY_OWNER") or default_owner
|
||||
token = token or os.environ.get("TOKEN") or os.environ.get("GITEA_TOKEN") or os.environ.get("PACKAGE_TOKEN") or os.environ.get("RELEASE_TOKEN")
|
||||
|
||||
existing_nums = query_existing_build_numbers(pkg_name, version, gitea_url, repo, owner, token)
|
||||
if existing_nums:
|
||||
return max(existing_nums) + 1
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Ermittelt die nächste Build-Nummer für die Paketierung.")
|
||||
parser.add_argument("--version", help="Paket-Version (Standard: aus Cargo.toml)")
|
||||
parser.add_argument("--gitea-url", help="Gitea Basis-URL")
|
||||
parser.add_argument("--repo", help="Repository (z. B. Owner/Repo)")
|
||||
parser.add_argument("--owner", help="Repository Owner / Organisation")
|
||||
parser.add_argument("--token", help="API-Token")
|
||||
parser.add_argument("--build-number", type=int, help="Explizite Build-Nummer erzwingen")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
build_num = get_next_build_number(
|
||||
version=args.version,
|
||||
gitea_url=args.gitea_url,
|
||||
repo=args.repo,
|
||||
owner=args.owner,
|
||||
token=args.token,
|
||||
explicit_build_num=args.build_number,
|
||||
)
|
||||
print(build_num)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+146
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
TARGET_ARCH_MAP = {
|
||||
"x86_64-unknown-linux-gnu": "x86_64",
|
||||
"x86_64-unknown-linux-musl": "x86_64",
|
||||
"x86_64": "x86_64",
|
||||
"amd64": "x86_64",
|
||||
"aarch64-unknown-linux-gnu": "aarch64",
|
||||
"aarch64-unknown-linux-musl": "aarch64",
|
||||
"aarch64": "aarch64",
|
||||
"arm64": "aarch64",
|
||||
"i686-unknown-linux-gnu": "i686",
|
||||
"i686-unknown-linux-musl": "i686",
|
||||
"i686": "i686",
|
||||
"i386": "i686",
|
||||
}
|
||||
|
||||
|
||||
def resolve_pkgrel(version=None, default="1"):
|
||||
# 1. Environment Variable
|
||||
env_pkgrel = os.environ.get("BUILD_NUMBER") or os.environ.get("BUILD_NUM") or os.environ.get("PKGREL")
|
||||
if env_pkgrel:
|
||||
return str(env_pkgrel)
|
||||
|
||||
# 2. get-build-number.py falls vorhanden
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
getter_path = os.path.join(script_dir, "get-build-number.py")
|
||||
if os.path.exists(getter_path):
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("get_build_number", getter_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return str(module.get_next_build_number(version=version))
|
||||
except Exception as e:
|
||||
sys.stderr.write(f"[Hinweis] Konnte get-build-number nicht ausführen: {e}\n")
|
||||
|
||||
return str(default)
|
||||
|
||||
|
||||
def build_package(target_triple=None, target_arch=None, pkgrel=None):
|
||||
metadata = json.loads(subprocess.check_output(["cargo", "metadata", "--format-version", "1", "--no-deps"]))
|
||||
pkg = metadata["packages"][0]
|
||||
name = pkg["name"]
|
||||
version = pkg["version"]
|
||||
description = pkg.get("description", "")
|
||||
license_name = pkg.get("license", "")
|
||||
repository = pkg.get("repository", "")
|
||||
authors = pkg.get("authors", [])
|
||||
author = authors[0] if authors else "Unknown"
|
||||
|
||||
arch_meta = pkg.get("metadata", {}).get("arch", {})
|
||||
default_pkgrel = arch_meta.get("pkgrel", "1")
|
||||
if not pkgrel:
|
||||
pkgrel = resolve_pkgrel(version=version, default=default_pkgrel)
|
||||
default_arch = arch_meta.get("arch", "x86_64")
|
||||
|
||||
if target_arch:
|
||||
arch = TARGET_ARCH_MAP.get(target_arch, target_arch)
|
||||
elif target_triple:
|
||||
arch = TARGET_ARCH_MAP.get(target_triple, default_arch)
|
||||
else:
|
||||
arch = default_arch
|
||||
|
||||
candidate_paths = []
|
||||
if target_triple:
|
||||
candidate_paths.append(f"target/{target_triple}/release/{name}")
|
||||
candidate_paths.append(f"target/release/{name}")
|
||||
|
||||
bin_path = None
|
||||
for p in candidate_paths:
|
||||
if os.path.exists(p):
|
||||
bin_path = p
|
||||
break
|
||||
|
||||
if not bin_path:
|
||||
raise FileNotFoundError(
|
||||
f"Keine kompilierte Binary für {name} gefunden. Gesuchte Pfade: {candidate_paths}"
|
||||
)
|
||||
|
||||
depends = arch_meta.get("depends", ["gcc-libs", "glibc"])
|
||||
optdepends = arch_meta.get("optdepends", [])
|
||||
|
||||
with tempfile.TemporaryDirectory() as build_dir:
|
||||
bin_dir = os.path.join(build_dir, "usr/bin")
|
||||
doc_dir = os.path.join(build_dir, f"usr/share/doc/{name}")
|
||||
lic_dir = os.path.join(build_dir, f"usr/share/licenses/{name}")
|
||||
os.makedirs(bin_dir, exist_ok=True)
|
||||
os.makedirs(doc_dir, exist_ok=True)
|
||||
os.makedirs(lic_dir, exist_ok=True)
|
||||
|
||||
subprocess.run(["install", "-m", "755", bin_path, f"{bin_dir}/{name}"], check=True)
|
||||
if os.path.exists("LICENSE"):
|
||||
subprocess.run(["install", "-m", "644", "LICENSE", f"{lic_dir}/LICENSE"], check=True)
|
||||
if os.path.exists("README.md"):
|
||||
subprocess.run(["install", "-m", "644", "README.md", f"{doc_dir}/README.md"], check=True)
|
||||
|
||||
installed_size = subprocess.check_output(["du", "-sb", build_dir]).decode().split()[0]
|
||||
builddate = str(int(time.time()))
|
||||
|
||||
pkginfo_lines = [
|
||||
f"pkgname = {name}",
|
||||
f"pkgbase = {name}",
|
||||
f"pkgver = {version}-{pkgrel}",
|
||||
f"pkgdesc = {description}",
|
||||
f"url = {repository}",
|
||||
f"builddate = {builddate}",
|
||||
f"packager = {author}",
|
||||
f"size = {installed_size}",
|
||||
f"arch = {arch}",
|
||||
f"license = {license_name}",
|
||||
]
|
||||
for dep in depends:
|
||||
pkginfo_lines.append(f"depend = {dep}")
|
||||
for optdep in optdepends:
|
||||
pkginfo_lines.append(f"optdepend = {optdep}")
|
||||
pkginfo_lines.append("makepkgopt = strip\n")
|
||||
|
||||
with open(os.path.join(build_dir, ".PKGINFO"), "w") as f:
|
||||
f.write("\n".join(pkginfo_lines))
|
||||
|
||||
os.makedirs("target/arch", exist_ok=True)
|
||||
output_file = os.path.abspath(f"target/arch/{name}-{version}-{pkgrel}-{arch}.pkg.tar.zst")
|
||||
subprocess.run(["tar", "--zstd", "-cf", output_file, ".PKGINFO", "usr"], cwd=build_dir, check=True)
|
||||
print(f"Arch-Paket erfolgreich erstellt: {output_file}")
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Erstellt Arch Linux-Pakete (.pkg.tar.zst)")
|
||||
parser.add_argument("--target", help="Rust Target-Triple (z.B. x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, i686-unknown-linux-gnu)")
|
||||
parser.add_argument("--arch", help="Architektur (z.B. x86_64, aarch64, i686)")
|
||||
parser.add_argument("--pkgrel", help="Release-/Build-Nummer (z.B. 1, 2, ...)")
|
||||
args = parser.parse_args()
|
||||
|
||||
build_package(target_triple=args.target, target_arch=args.arch, pkgrel=args.pkgrel)
|
||||
|
||||
|
||||
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()
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
//! Modul für die Konfigurationsverwaltung der Anwendung.
|
||||
//!
|
||||
//! Dieses Modul stellt die Funktionalität zum Laden, Speichern und Verwalten
|
||||
//! der Anwendungskonfiguration bereit. Die Konfiguration wird in einer Datei gespeichert
|
||||
//! und beim Programmstart automatisch geladen.
|
||||
|
||||
use crate::program;
|
||||
use crate::sudo::is_run_as_root;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::OnceLock;
|
||||
|
||||
/// Hauptkonfigurationsstruktur der Anwendung.
|
||||
///
|
||||
/// Enthält alle Konfigurationseinstellungen, aufgeteilt in verschiedene Bereiche.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct AppConfig {
|
||||
/// Allgemeine Einstellungen
|
||||
pub general: General,
|
||||
/// Lokale Einbindungseinstellungen
|
||||
pub local: Local,
|
||||
/// Remote-Einbindungseinstellungen
|
||||
pub remote: Remote,
|
||||
/// Lokaler Zwischenspeicher
|
||||
pub storage: Option<Storage>,
|
||||
}
|
||||
|
||||
impl Default for AppConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
general: General::default(),
|
||||
local: Local::default(),
|
||||
remote: Remote::default(),
|
||||
storage: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Allgemeine Konfigurationseinstellungen.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct General {
|
||||
/// Log-Level für die Anwendung
|
||||
pub log_level: String,
|
||||
/// Pfad zum Einhängepunkt
|
||||
pub mount_point: String,
|
||||
}
|
||||
|
||||
impl Default for General {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
log_level: "info".to_string(),
|
||||
mount_point: get_default_mount_path(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Konfiguration für lokale Einbindungen.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Local {
|
||||
/// Typ der lokalen Einbindung (z.B. "nfs")
|
||||
pub mount_type: String,
|
||||
/// Pfad zur lokalen Einbindung
|
||||
pub mount_path: String,
|
||||
/// MAC-Adresse des lokalen Geräts
|
||||
pub device_mac: String,
|
||||
}
|
||||
|
||||
impl Default for Local {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mount_type: "nfs".to_string(),
|
||||
mount_path: "".to_string(),
|
||||
device_mac: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Konfiguration für Remote-Einbindungen.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Remote {
|
||||
/// Typ der Remote-Einbindung (z.B. "webdav")
|
||||
pub mount_type: String,
|
||||
/// Pfad zur Remote-Einbindung
|
||||
pub mount_path: String,
|
||||
/// Benutzername für Remote-Zugriff
|
||||
pub username: String,
|
||||
/// Passwort für Remote-Zugriff
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Default for Remote {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
mount_type: "webdav".to_string(),
|
||||
mount_path: "".to_string(),
|
||||
username: "".to_string(),
|
||||
password: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zwischenspeicher für das Programm.
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
pub struct Storage {
|
||||
pub device_ip: String,
|
||||
}
|
||||
|
||||
impl Default for Storage {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
device_ip: "".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ermittelt den Standard-Einhängepunkt basierend auf dem Betriebssystem.
|
||||
fn get_default_mount_path() -> String {
|
||||
#[cfg(not(any(target_os = "windows")))]
|
||||
{
|
||||
"/media/smart_mount".to_string()
|
||||
}
|
||||
#[cfg(any(target_os = "windows"))]
|
||||
{
|
||||
let drives = ('C'..='Z').collect::<Vec<char>>();
|
||||
for drive in drives {
|
||||
let drive_string = format!("{}:", drive);
|
||||
if !std::path::Path::new(&drive_string).exists() {
|
||||
return format!("{}:", drive);
|
||||
}
|
||||
}
|
||||
"H".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
static CONFIG_NAME: &str = "config";
|
||||
static CONFIG: OnceLock<AppConfig> = OnceLock::new();
|
||||
|
||||
/// Gibt die aktuelle Konfiguration zurück.
|
||||
///
|
||||
/// Lädt die Konfiguration beim ersten Aufruf und speichert sie zwischen.
|
||||
/// Nachfolgende Aufrufe geben die gespeicherte Konfiguration zurück.
|
||||
pub fn get_config() -> &'static AppConfig {
|
||||
CONFIG.get_or_init(|| {
|
||||
load_config()
|
||||
})
|
||||
}
|
||||
|
||||
/// Modifiziert die aktuelle Konfiguration mit der übergebenen Mutator-Funktion.
|
||||
///
|
||||
/// Die Funktion lädt die Konfiguration neu von der Festplatte, wendet die Mutator-Funktion an
|
||||
/// und speichert die geänderte Konfiguration anschließend wieder.
|
||||
pub fn modify_config<F>(mutator: F)
|
||||
where
|
||||
F: FnOnce(&mut AppConfig),
|
||||
{
|
||||
// Immer frisch von Disk laden, damit Änderungen konsistent sind
|
||||
let mut cfg = load_config();
|
||||
mutator(&mut cfg);
|
||||
save_config(cfg);
|
||||
}
|
||||
|
||||
/// Lädt die Konfiguration aus der Konfigurationsdatei.
|
||||
fn load_config() -> AppConfig {
|
||||
#[cfg(target_os = "linux")]
|
||||
if is_run_as_root() {
|
||||
return confy::load_path(format!("/etc/{}/{}.toml", program::program_name(), CONFIG_NAME)).unwrap_or_default();
|
||||
}
|
||||
confy::load(&*program::program_name(), CONFIG_NAME).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Speichert die übergebene Konfiguration in der Konfigurationsdatei.
|
||||
fn save_config(config: AppConfig) {
|
||||
#[cfg(target_os = "linux")]
|
||||
if is_run_as_root() {
|
||||
confy::store_path(format!("/etc/{}/{}.toml", program::program_name(), CONFIG_NAME), config).unwrap();
|
||||
return;
|
||||
}
|
||||
confy::store(&*program::program_name(), CONFIG_NAME, config).unwrap();
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
use crate::config::get_config;
|
||||
use crate::log::{log, LogLevel};
|
||||
use std::process::Command;
|
||||
|
||||
/// Speichert die Zugangsdaten für einen WebDAV-Mount-Punkt.
|
||||
///
|
||||
/// # Parameter
|
||||
/// * `mount_path` - Der Netzwerkpfad zum WebDAV-Server
|
||||
/// * `username` - Der Benutzername für den WebDAV-Zugriff
|
||||
/// * `password` - Das Passwort für den WebDAV-Zugriff
|
||||
pub fn save_credentials_webdav() {
|
||||
let config = get_config();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let output = Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!("cmdkey /delete:{}", config.remote.mount_path)
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to delete credentials");
|
||||
|
||||
let output = Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!("cmdkey /add:{} /user:{} /pass:{}",
|
||||
config.remote.mount_path,
|
||||
config.remote.username,
|
||||
config.remote.password
|
||||
)
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to store credentials");
|
||||
|
||||
if !output.status.success() {
|
||||
log(
|
||||
"credentials",
|
||||
"Could not store credentials in Windows Credential Manager",
|
||||
LogLevel::Error,
|
||||
);
|
||||
log(
|
||||
"credentials",
|
||||
&String::from_utf8_lossy(&output.stderr),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
if config.remote.username.is_empty() || config.remote.password.is_empty() {
|
||||
log(
|
||||
"credentials",
|
||||
"No credentials found in config",
|
||||
LogLevel::Error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new("touch")
|
||||
.arg("/etc/davfs2/secrets")
|
||||
.output()
|
||||
.expect("Failed to create secrets file");
|
||||
|
||||
if !output.status.success() {
|
||||
log(
|
||||
"credentials",
|
||||
"Could not create secrets file",
|
||||
LogLevel::Error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let output = Command::new("chmod")
|
||||
.arg("600")
|
||||
.arg("/etc/davfs2/secrets")
|
||||
.output()
|
||||
.expect("Failed to set secrets file permissions");
|
||||
|
||||
if !output.status.success() {
|
||||
log(
|
||||
"credentials",
|
||||
"Could not set secrets file permissions",
|
||||
LogLevel::Error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let credentials = format!("{} \"{}\" \"{}\"\n",
|
||||
config.general.mount_point,
|
||||
config.remote.username,
|
||||
config.remote.password
|
||||
);
|
||||
let mut content = std::fs::read_to_string("/etc/davfs2/secrets")
|
||||
.unwrap_or_default();
|
||||
|
||||
content = content.lines()
|
||||
.filter(|line| !line.starts_with(&config.general.mount_point))
|
||||
.collect::<Vec<&str>>()
|
||||
.join("\n");
|
||||
|
||||
if !content.is_empty() {
|
||||
content.push('\n');
|
||||
}
|
||||
content.push_str(&credentials);
|
||||
|
||||
std::fs::write("/etc/davfs2/secrets", content)
|
||||
.expect("Failed to write credentials");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
pub mod mount;
|
||||
pub mod mounted;
|
||||
pub mod credentials;
|
||||
@@ -1,213 +0,0 @@
|
||||
use crate::filesystem::mounted::{/* is_mounted, */ FS_MOUNT_GUARD};
|
||||
use crate::log::{LogLevel, log};
|
||||
use std::process::Command;
|
||||
use std::sync::{Mutex, OnceLock, RwLock};
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
static GUARD_MOUNT: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
static GUARD_UNMOUNT: OnceLock<Mutex<()>> = OnceLock::new();
|
||||
|
||||
/// Führt die Einbindung eines Dateisystems durch.
|
||||
///
|
||||
/// # Parameter
|
||||
/// * `mount_point` - Der lokale Verzeichnispfad, an dem das Dateisystem eingebunden werden soll
|
||||
/// * `network_path` - Der Netzwerkpfad zum einzubindenden Dateisystem
|
||||
/// * `mount_type` - Der Mount-Typ, z.B. "nfs" oder "davfs2"
|
||||
pub fn mount(mount_point: &str, network_path: &str, mount_type: &str) {
|
||||
// 1) Globalen Write-Lock halten (blockiert parallele Statusabfragen)
|
||||
let _fs_write = FS_MOUNT_GUARD
|
||||
.get_or_init(|| RwLock::new(()))
|
||||
.write()
|
||||
.expect("mount rwlock poisoned");
|
||||
|
||||
// 2) Danach funktionsspezifischen Mutex sperren (konsistente Lock-Reihenfolge!)
|
||||
let m = GUARD_MOUNT.get_or_init(|| Mutex::new(()));
|
||||
let _lock = m.lock().expect("Mutex poisoned");
|
||||
|
||||
log(
|
||||
"mount",
|
||||
&*format!(
|
||||
"Mounting filesystem ({}) at \"{}\" as \"{}\"",
|
||||
network_path, mount_point, mount_type
|
||||
),
|
||||
LogLevel::Info,
|
||||
);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let output = Command::new("New-PSDrive")
|
||||
.args([
|
||||
"-Name",
|
||||
&mount_point[0..1], // First letter for drive letter
|
||||
"-PSProvider",
|
||||
"FileSystem",
|
||||
"-Root",
|
||||
network_path,
|
||||
"-Persist",
|
||||
"-Type",
|
||||
mount_type,
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to execute mount command");
|
||||
|
||||
if !output.status.success() {
|
||||
log(
|
||||
"mount",
|
||||
&*format!(
|
||||
"Filesystem ({}) couldn't be mounted at \"{}\" as \"{}\"",
|
||||
network_path, mount_point, mount_type
|
||||
),
|
||||
LogLevel::Error,
|
||||
);
|
||||
log(
|
||||
"mount",
|
||||
&*String::from_utf8_lossy(&output.stderr),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
// Verzeichnis vorbereiten
|
||||
let output = Command::new("mkdir")
|
||||
.arg("-p")
|
||||
.arg(mount_point)
|
||||
.output()
|
||||
.expect("Failed to create mount point directory");
|
||||
|
||||
if output.status.success() {
|
||||
log("mount", "Mountpoint created successfully!", LogLevel::Info);
|
||||
} else {
|
||||
log(
|
||||
"mount",
|
||||
&*String::from_utf8_lossy(&output.stderr),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
}
|
||||
|
||||
let output = Command::new("chmod")
|
||||
.arg("777")
|
||||
.arg(mount_point)
|
||||
.output()
|
||||
.expect("Failed to set mount point permissions");
|
||||
|
||||
if output.status.success() {
|
||||
log(
|
||||
"mount",
|
||||
"Successfully set mount point permissions.",
|
||||
LogLevel::Info,
|
||||
);
|
||||
} else {
|
||||
log(
|
||||
"mount",
|
||||
&*String::from_utf8_lossy(&output.stderr),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
}
|
||||
|
||||
let output = Command::new("mount")
|
||||
.arg("-t")
|
||||
.arg(mount_type)
|
||||
.arg(network_path)
|
||||
.arg(mount_point)
|
||||
.output()
|
||||
.expect("Failed to execute mount command");
|
||||
|
||||
sleep(Duration::from_secs(1));
|
||||
|
||||
if output.status.success() {
|
||||
log("mount", "Filesystem mounted successfully.", LogLevel::Info);
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
// Häufige „schon gemountet“/Busy-Indikatoren tolerant behandeln
|
||||
let already_or_busy = stderr.contains("already mounted")
|
||||
|| stderr.contains("is busy")
|
||||
|| stderr.contains("Device or resource busy")
|
||||
|| stderr.contains("EBUSY");
|
||||
|
||||
if already_or_busy {
|
||||
log(
|
||||
"mount",
|
||||
"Filesystem appears to be already mounted or mountpoint is busy. Treating as no-op.",
|
||||
LogLevel::Info,
|
||||
);
|
||||
log("mount", &*stderr, LogLevel::Debug);
|
||||
return;
|
||||
}
|
||||
|
||||
log(
|
||||
"mount",
|
||||
&*format!(
|
||||
"Filesystem ({}) couldn't be mounted at \"{}\" as \"{}\"",
|
||||
network_path, mount_point, mount_type
|
||||
),
|
||||
LogLevel::Error,
|
||||
);
|
||||
log("mount", &*stderr, LogLevel::Debug);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Hängt ein Dateisystem aus.
|
||||
///
|
||||
/// # Parameter
|
||||
/// * `mount_point` - Der lokale Verzeichnispfad, von dem das Dateisystem ausgehängt werden soll
|
||||
pub fn unmount(mount_point: &str) {
|
||||
// 1) Globalen Write-Lock halten (blockiert parallele Statusabfragen)
|
||||
let _fs_write = FS_MOUNT_GUARD
|
||||
.get_or_init(|| RwLock::new(()))
|
||||
.write()
|
||||
.expect("mount rwlock poisoned");
|
||||
|
||||
// 2) Danach funktionsspezifischen Mutex sperren (konsistente Lock-Reihenfolge!)
|
||||
let m = GUARD_UNMOUNT.get_or_init(|| Mutex::new(()));
|
||||
let _lock = m.lock().expect("Mutex poisoned");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let output = Command::new("Remove-PSDrive")
|
||||
.args([
|
||||
"-Name",
|
||||
&mount_point[0..1], // First letter for drive letter
|
||||
"-Force",
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to execute unmount command");
|
||||
|
||||
if !output.status.success() {
|
||||
log(
|
||||
"unmount",
|
||||
&*format!("Filesystem couldn't be unmounted from \"{}\"", mount_point),
|
||||
LogLevel::Error,
|
||||
);
|
||||
log(
|
||||
"unmount",
|
||||
&*String::from_utf8_lossy(&output.stderr),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let output = Command::new("umount")
|
||||
.arg(mount_point)
|
||||
.output()
|
||||
.expect("Failed to execute unmount command");
|
||||
|
||||
if !output.status.success() {
|
||||
log(
|
||||
"unmount",
|
||||
&*format!("Filesystem couldn't be unmounted from \"{}\"", mount_point),
|
||||
LogLevel::Error,
|
||||
);
|
||||
log(
|
||||
"unmount",
|
||||
&*String::from_utf8_lossy(&output.stderr),
|
||||
LogLevel::Debug,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
use std::fs::File;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
|
||||
// Globaler RW-Lock für Mount-Operationen und Statusabfragen
|
||||
pub static FS_MOUNT_GUARD: OnceLock<RwLock<()>> = OnceLock::new();
|
||||
|
||||
fn guard() -> &'static RwLock<()> {
|
||||
FS_MOUNT_GUARD.get_or_init(|| RwLock::new(()))
|
||||
}
|
||||
|
||||
/// Überprüft, ob ein Dateisystem am angegebenen Mount-Point mit dem spezifizierten Mount-Typ eingebunden ist.
|
||||
///
|
||||
/// # Parameter
|
||||
/// * `mount_point` - Der Pfad, an dem das Dateisystem eingebunden sein soll
|
||||
/// * `mount_type` - Der erwartete Mount-Typ des Dateisystems (z.B. nfs, smb, davfs2)
|
||||
///
|
||||
/// # Rückgabewert
|
||||
/// * `true` wenn das Dateisystem mit dem angegebenen Typ eingebunden ist
|
||||
/// * `false` wenn das Dateisystem nicht oder mit einem anderen Typ eingebunden ist
|
||||
pub fn is_mounted_as(mount_point: &str, mount_type: &str) -> bool {
|
||||
// Während Statusabfragen nur Read-Lock halten
|
||||
let _read_guard = guard().read().expect("mount rwlock poisoned");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let drive_letter = &mount_point[0..1];
|
||||
let output = Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!(
|
||||
"(Get-PSDrive -Name {} -PSProvider 'FileSystem').Description",
|
||||
drive_letter
|
||||
),
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to execute get-psdrive command");
|
||||
|
||||
if !output.status.success() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let drive_type = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
drive_type == mount_type
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let norm_mp = normalize_mount_point(mount_point);
|
||||
|
||||
for entry in read_proc_mounts() {
|
||||
let (mp, fstype, _opts, _src) = entry;
|
||||
if mp == norm_mp {
|
||||
if type_matches(mount_type, &fstype, &_opts) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Überprüft, ob ein Dateisystem am angegebenen Mount-Point eingebunden ist.
|
||||
///
|
||||
/// # Parameter
|
||||
/// * `mount_point` - Der Pfad, an dem das Dateisystem eingebunden sein soll
|
||||
///
|
||||
/// # Rückgabewert
|
||||
/// * `true` wenn ein Dateisystem am angegebenen Pfad eingebunden ist
|
||||
/// * `false` wenn kein Dateisystem eingebunden ist
|
||||
pub fn is_mounted(mount_point: &str) -> bool {
|
||||
// Während Statusabfragen nur Read-Lock halten
|
||||
let _read_guard = guard().read().expect("mount rwlock poisoned");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::process::Command;
|
||||
let drive_letter = &mount_point[0..1];
|
||||
let output = Command::new("powershell")
|
||||
.args([
|
||||
"-Command",
|
||||
&format!(
|
||||
"(Get-PSDrive -Name {} -PSProvider 'FileSystem')",
|
||||
drive_letter
|
||||
),
|
||||
])
|
||||
.output()
|
||||
.expect("Failed to execute get-psdrive command");
|
||||
|
||||
output.status.success()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let norm_mp = normalize_mount_point(mount_point);
|
||||
read_proc_mounts()
|
||||
.into_iter()
|
||||
.any(|(mp, _, _, _)| mp == norm_mp)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn read_proc_mounts() -> Vec<(String, String, String, String)> {
|
||||
// Liefert Tupel: (mount_point, fstype, options, source)
|
||||
let file = match File::open("/proc/mounts") {
|
||||
Ok(f) => f,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
let reader = BufReader::new(file);
|
||||
let mut result = Vec::new();
|
||||
|
||||
for line in reader.lines().flatten() {
|
||||
// Format /proc/mounts:
|
||||
// fs_spec fs_file fs_vfstype fs_mntops fs_freq fs_passno
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 6 {
|
||||
continue;
|
||||
}
|
||||
let fs_spec = parts[0].to_string();
|
||||
let fs_file = parts[1].to_string();
|
||||
let fs_vfstype = parts[2].to_string();
|
||||
let fs_mntops = parts[3].to_string();
|
||||
|
||||
result.push((fs_file, fs_vfstype, fs_mntops, fs_spec));
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn normalize_mount_point(p: &str) -> String {
|
||||
// Entfernt redundante Slashes am Ende (außer bei "/") und canonicalized soweit möglich.
|
||||
if p == "/" {
|
||||
return "/".to_string();
|
||||
}
|
||||
let trimmed = p.trim_end_matches('/');
|
||||
// Versuche, realpath zu bilden, falle sonst auf trimmed zurück
|
||||
let path = Path::new(trimmed);
|
||||
path.canonicalize()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| trimmed.to_string())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn type_matches(expected: &str, actual_fstype: &str, options: &str) -> bool {
|
||||
// Normalisiere erwartete Typfamilien
|
||||
match expected {
|
||||
// NFS kann als nfs oder nfs4 erscheinen
|
||||
"nfs" | "nfs4" => actual_fstype == "nfs" || actual_fstype == "nfs4",
|
||||
|
||||
// davfs/webdav erscheint häufig als fuse.davfs (oder fuse mit helper=davfs)
|
||||
"webdav" | "davfs" | "davfs2" => {
|
||||
actual_fstype == "fuse.davfs"
|
||||
|| actual_fstype == "davfs"
|
||||
|| (actual_fstype == "fuse" && options.contains("helper=davfs"))
|
||||
}
|
||||
|
||||
// CIFS/Samba Alias
|
||||
"cifs" | "smb" | "smb3" => actual_fstype == "cifs" || actual_fstype == "smb3",
|
||||
|
||||
// Fallback: exakter Vergleich
|
||||
other => other == actual_fstype,
|
||||
}
|
||||
}
|
||||
-241
@@ -1,241 +0,0 @@
|
||||
//! Einfaches, threadsicheres Logging-Modul.
|
||||
//!
|
||||
//! Merkmale:
|
||||
//! - Ausgabe in Terminal (stdout/stderr) und zusätzlich in eine Logdatei im temporären Verzeichnis.
|
||||
//! - Nachrichtenvorlage: `[DD.MM.YYYY HH:MM:SS.mmm][LEVEL][TAG]: <Text>` mit Emoji-Präfix (🛑/⚠️/ℹ️/🚧).
|
||||
//! - Der aktuell verwendete Schweregradfilter ist statisch (`LOG_LEVEL`) und wird zur Laufzeit nicht geändert.
|
||||
//! - Terminal-Erkennung und Dateihandle werden lazily initialisiert und zwischengespeichert (`OnceLock`).
|
||||
//!
|
||||
//! Hinweise:
|
||||
//! - Bei einem leeren `tag` wird ein leerer Tag-Abschnitt erzeugt.
|
||||
//! - Die Logdatei wird unterhalb eines prozessspezifischen Ordners im System-Temp-Verzeichnis abgelegt und nach Datum
|
||||
//! benannt (z. B. `log-2025-08-18.log`). Es wird im Append-Modus geschrieben.
|
||||
//!
|
||||
//! Beispiel (ohne Ausführung in Doctests):
|
||||
//! ```rust,no_run
|
||||
//! use crate::log::{log, LogLevel};
|
||||
//!
|
||||
//! log("startup", "Dienst wird initialisiert …", LogLevel::Info);
|
||||
//! log("db", "Verbindung unterbrochen!", LogLevel::Warn);
|
||||
//! log("core", "Unerwarteter Fehler", LogLevel::Error);
|
||||
//! ```
|
||||
|
||||
use std::env;
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::fs::{create_dir_all, File, OpenOptions};
|
||||
use std::io::{stdout, IsTerminal, Write};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
|
||||
use crate::config;
|
||||
use crate::program;
|
||||
use time::{macros::format_description, OffsetDateTime};
|
||||
|
||||
/// Schweregrade für Logeinträge in aufsteigender Detailtiefe.
|
||||
///
|
||||
/// Die Reihenfolge bestimmt den Filter: Nur Einträge mit `log_level <= LOG_LEVEL` werden ausgegeben.
|
||||
///
|
||||
/// Anzeige (Display):
|
||||
/// - `Error` -> `ERROR`
|
||||
/// - `Warn` -> `WARN`
|
||||
/// - `Info` -> `INFO`
|
||||
/// - `Debug` -> `DEBUG`
|
||||
#[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone)]
|
||||
pub enum LogLevel {
|
||||
/// Kritische Fehler, gehen zusätzlich auf `stderr`.
|
||||
Error = 1,
|
||||
/// Wichtige Warnungen über potenzielle Probleme.
|
||||
Warn = 2,
|
||||
/// Allgemeine Betriebsinformationen.
|
||||
Info = 3,
|
||||
/// Ausführliche Diagnoseausgaben für die Entwicklung.
|
||||
Debug = 4,
|
||||
}
|
||||
|
||||
impl Display for LogLevel {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
LogLevel::Error => write!(f, "ERROR"),
|
||||
LogLevel::Warn => write!(f, "WARN"),
|
||||
LogLevel::Info => write!(f, "INFO"),
|
||||
LogLevel::Debug => write!(f, "DEBUG"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<String> for LogLevel {
|
||||
type Error = LogLevel;
|
||||
|
||||
fn try_from(value: String) -> Result<Self, Self> {
|
||||
match value.to_lowercase().as_str() {
|
||||
"error" => Ok(LogLevel::Error),
|
||||
"warn" => Ok(LogLevel::Warn),
|
||||
"info" => Ok(LogLevel::Info),
|
||||
"debug" => Ok(LogLevel::Debug),
|
||||
_ => Err(LogLevel::Info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Zwischenspeicher für die einmalig ermittelte Terminal-Fähigkeit von `stdout`.
|
||||
static IS_TERMINAL: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
/// Globaler Schweregradfilter für Ausgabe.
|
||||
static LOG_LEVEL: OnceLock<LogLevel> = OnceLock::new();
|
||||
|
||||
/// Lazy-initialisiertes Handle zur Logdatei; kann `None` sein, falls das Öffnen fehlschlug.
|
||||
static LOG_FILE: OnceLock<Mutex<Option<File>>> = OnceLock::new();
|
||||
|
||||
/// Protokolliert eine Nachricht abhängig vom angegebenen Schweregrad.
|
||||
///
|
||||
/// Verhalten:
|
||||
/// - Wenn `log_level` größer als der globale Filter ist, wird nichts ausgegeben.
|
||||
/// - Bei Terminalausgabe gehen `Error`-Meldungen nach `stderr`, alle anderen nach `stdout`.
|
||||
/// - Zusätzlich wird in eine tägliche Logdatei im Temp-Verzeichnis geschrieben (wenn erfolgreich geöffnet).
|
||||
///
|
||||
/// Parameter:
|
||||
/// - `tag`: Kurzer Kontext (z. B. Modulname).
|
||||
/// - `message`: Die eigentliche Nachricht (eine Zeile).
|
||||
/// - `log_level`: Schweregrad der Nachricht.
|
||||
///
|
||||
/// Thread-Sicherheit:
|
||||
/// - Dateischreibzugriffe sind über `Mutex` serialisiert.
|
||||
///
|
||||
/// Beispiel:
|
||||
/// ```rust,no_run
|
||||
/// # use crate::log::{log, LogLevel};
|
||||
/// log("http", "Server gestartet auf Port 8080", LogLevel::Info);
|
||||
/// ```
|
||||
pub fn log(tag: &str, message: &str, log_level: LogLevel) {
|
||||
if log_level <= get_log_level() {
|
||||
let message: String = format_message(tag, message, &log_level);
|
||||
|
||||
if is_terminal() {
|
||||
if log_level == LogLevel::Error {
|
||||
eprintln!("{}", message)
|
||||
} else {
|
||||
println!("{}", message)
|
||||
}
|
||||
}
|
||||
|
||||
let file_lock = get_or_init_log_file();
|
||||
if let Ok(mut guard) = file_lock.lock() {
|
||||
if let Some(f) = guard.as_mut() {
|
||||
let _ = writeln!(f, "{}", message);
|
||||
let _ = f.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Liefert das einmalig initialisierte Log-Level für die Filterung.
|
||||
///
|
||||
/// Ermittelt das konfigurierte Log-Level aus der Config-Datei.
|
||||
/// Wenn ungültig oder nicht vorhanden, wird LogLevel::Info als Fallback verwendet.
|
||||
///
|
||||
/// Rückgabe:
|
||||
/// - `LogLevel`: Das zu verwendende Log-Level als enum-Wert.
|
||||
fn get_log_level() -> LogLevel {
|
||||
*LOG_LEVEL.get_or_init(|| {
|
||||
let log_level = &config::get_config().general.log_level;
|
||||
LogLevel::try_from(log_level.to_string()).unwrap_or(LogLevel::Info)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/// Ermittelt einmalig, ob `stdout` ein Terminal ist, und cached das Ergebnis.
|
||||
///
|
||||
/// Rückgabe:
|
||||
/// - `true`, wenn `stdout` ein TTY/Terminal ist.
|
||||
/// - `false` andernfalls.
|
||||
fn is_terminal() -> bool {
|
||||
*IS_TERMINAL.get_or_init(|| stdout().is_terminal())
|
||||
}
|
||||
|
||||
/// Formatiert eine Lognachricht mit Zeitstempel, Level, Tag und Emoji-Präfix.
|
||||
///
|
||||
/// Format:
|
||||
/// - Datum/Zeit lokal (Fallback: UTC) im Format `DD.MM.YYYY HH:MM:SS.mmm`.
|
||||
/// - Level als Text (`ERROR`, `WARN`, `INFO`, `DEBUG`).
|
||||
/// - Tag in eckigen Klammern; bei leerem Tag entsteht ein leeres `[]`.
|
||||
/// - Emoji-Präfix pro Level: 🛑/⚠️/ℹ️/🚧.
|
||||
///
|
||||
/// Beispielausgabe:
|
||||
/// - `ℹ️[18.08.2025 14:23:45.012][INFO][init]: Fertig`
|
||||
///
|
||||
/// Hinweis:
|
||||
/// - Diese Funktion formatiert nur; sie führt keinen I/O aus.
|
||||
fn format_message(tag: &str, message: &str, log_level: &LogLevel) -> String {
|
||||
let mut prefix: String = String::new();
|
||||
|
||||
let now_local: OffsetDateTime =
|
||||
OffsetDateTime::now_local().unwrap_or(OffsetDateTime::now_utc());
|
||||
let fmt =
|
||||
format_description!("[day].[month].[year] [hour]:[minute]:[second].[subsecond digits:3]");
|
||||
|
||||
if tag != "" {
|
||||
prefix = format!("[{}]", tag);
|
||||
}
|
||||
|
||||
let message = format!(
|
||||
"[{}][{}][{}]: {}",
|
||||
now_local.format(fmt).unwrap(),
|
||||
log_level.to_string(),
|
||||
prefix,
|
||||
message
|
||||
);
|
||||
|
||||
match log_level {
|
||||
&LogLevel::Error => format!("{}{}", "🛑", message),
|
||||
&LogLevel::Warn => format!("{}{}", "⚠️", message),
|
||||
&LogLevel::Info => format!("{}{}", "ℹ️", message),
|
||||
&LogLevel::Debug => format!("{}{}", "🚧", message),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialisiert das Logdatei-Handle beim ersten Aufruf und liefert eine Referenz darauf.
|
||||
///
|
||||
/// Rückgabe:
|
||||
/// - `&'static Mutex<Option<File>>`: Das Mutex schützt den optionalen Dateihandler.
|
||||
/// `None` bedeutet, dass das Öffnen fehlgeschlagen ist (z. B. fehlende Rechte).
|
||||
fn get_or_init_log_file() -> &'static Mutex<Option<File>> {
|
||||
LOG_FILE.get_or_init(|| {
|
||||
let file = open_log_file().ok();
|
||||
Mutex::new(file)
|
||||
})
|
||||
}
|
||||
|
||||
/// Öffnet (und erstellt bei Bedarf) die tagesbasierte Logdatei im Temp-Verzeichnis.
|
||||
///
|
||||
/// Pfadaufbau:
|
||||
/// - Basis: `std::env::temp_dir()`
|
||||
/// - Unterordner: `<programmname>-<konstante-uuid>`
|
||||
/// - Datei: `log-YYYY-MM-DD.log` im Append-Modus
|
||||
///
|
||||
/// Rückgabe:
|
||||
/// - `Ok(File)`, wenn der Ordner erstellt/gefunden und die Datei geöffnet/angelegt werden konnte.
|
||||
/// - `Err(std::io::Error)`, wenn ein I/O-Fehler auftrat.
|
||||
///
|
||||
/// Fehler:
|
||||
/// - Gibt I/O-Fehler unverändert weiter (z. B. beim Erstellen des Ordners oder Öffnen der Datei).
|
||||
fn open_log_file() -> std::io::Result<File> {
|
||||
let program = program::program_name();
|
||||
let rand = "13692bbf-a93b-43e9-9cc6-f05f94a8cfb6";
|
||||
|
||||
let mut dir = env::temp_dir();
|
||||
dir.push(format!("{}-{}", program, rand));
|
||||
create_dir_all(&dir)?;
|
||||
|
||||
let today_fmt = format_description!("[year]-[month]-[day]");
|
||||
let date_str = OffsetDateTime::now_local()
|
||||
.unwrap_or(OffsetDateTime::now_utc())
|
||||
.format(today_fmt)
|
||||
.unwrap_or_else(|_| "0000-00-00".to_string());
|
||||
|
||||
let file_name = format!("log-{}.log", date_str);
|
||||
dir.push(file_name);
|
||||
|
||||
OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(dir)
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
pub mod network_interface;
|
||||
pub mod utils;
|
||||
@@ -1,192 +0,0 @@
|
||||
use crate::log;
|
||||
use crate::log::LogLevel;
|
||||
use std::process::Command;
|
||||
|
||||
/// Prüft, ob mindestens eine verfügbare Netzwerkschnittstelle (LAN/WLAN etc.) aktiv ist
|
||||
/// und nicht nur die Loopback-Schnittstelle.
|
||||
///
|
||||
/// Rückgabe:
|
||||
/// - Option<String>: Name der aktiven Netzwerkkarte oder None wenn keine gefunden
|
||||
pub fn get_active_network_interface() -> Option<String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
// Prüfe Interfaces in /sys/class/net
|
||||
let sysfs = Path::new("/sys/class/net");
|
||||
if let Ok(entries) = fs::read_dir(sysfs) {
|
||||
for entry in entries.flatten() {
|
||||
let ifname = match entry.file_name().into_string() {
|
||||
Ok(n) => n,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ifname == "lo" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let iface_path = entry.path();
|
||||
|
||||
// 1) operstate == "up"
|
||||
let operstate = fs::read_to_string(iface_path.join("operstate"))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
if operstate != "up" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2) carrier == "1" (hat Link)
|
||||
let carrier = fs::read_to_string(iface_path.join("carrier"))
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
if carrier != "1" {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3) Virtuell ausschließen:
|
||||
// /sys/class/net/<if>/device -> Symlink zu physischer HW;
|
||||
// zeigt der Pfad unterhalb von ".../virtual/..." oder existiert er gar nicht,
|
||||
// dann ist das i. d. R. ein virtuelles Interface.
|
||||
let device_link = iface_path.join("device");
|
||||
let is_virtual = match fs::read_link(&device_link) {
|
||||
Ok(target) => target.as_os_str().to_string_lossy().contains("/virtual/"),
|
||||
Err(_) => true, // kein device-Link -> in der Regel virtuell
|
||||
};
|
||||
if is_virtual {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Optional: bekannte „logische" Typen via Verzeichnis-Erkennung ausschließen
|
||||
// (Bridges, VLANs, Macvlan etc.)
|
||||
if iface_path.join("bridge").exists()
|
||||
|| iface_path.join("bonding").exists()
|
||||
|| iface_path.join("team").exists()
|
||||
|| iface_path.join("vlan").exists()
|
||||
|| iface_path.join("macvlan").exists()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Wenn wir hier sind, haben wir eine aktive physische NIC
|
||||
log::log("network_interface", "Found active network interface.", LogLevel::Info);
|
||||
return Some(ifname);
|
||||
}
|
||||
}
|
||||
|
||||
let output = Command::new("ip")
|
||||
.args(["-o", "link", "show", "up"])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
for line in stdout.lines() {
|
||||
if let Some(rest) = line.splitn(2, ": ").nth(1) {
|
||||
if let Some(ifname) = rest.split(':').next() {
|
||||
let name = ifname.trim();
|
||||
if name != "lo" {
|
||||
log::log("network_interface", "Found active network interface.", LogLevel::Debug);
|
||||
return Some(name.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// Windows: PowerShell verwenden, um aktive Adapter zu finden (Status = Up).
|
||||
// Wir ignorieren Loopback/Pseudo-Interfaces, indem wir Physical=true filtern.
|
||||
let ps_cmd = r#"
|
||||
$adapters = Get-NetAdapter -Physical | Where-Object {$_.Status -eq 'Up'} | Select-Object -First 1
|
||||
if ($adapters) { $adapters.Name } else { 'NONE' }
|
||||
"#;
|
||||
let output = Command::new("powershell")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", ps_cmd])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
|
||||
if stdout != "NONE" {
|
||||
log::log("network_interface", "Found active network interface.", LogLevel::Debug);
|
||||
return Some(stdout);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
log::log("network_interface", "OS not supported.", LogLevel::Error);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Ermittelt die IP-Adresse (mit Netzmaske) für eine angegebene Netzwerkschnittstelle
|
||||
///
|
||||
/// # Parameter
|
||||
/// - `interface`: Name der Netzwerkschnittstelle
|
||||
///
|
||||
/// # Rückgabe
|
||||
/// - Option<String>: IP-Adresse mit Netzmaske (z.B. "192.168.1.100/24") oder None wenn keine gefunden
|
||||
pub fn get_interface_ip_address(interface: &str) -> Option<String> {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let output = Command::new("ip")
|
||||
.args(["addr", "show", interface])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
for line in stdout.lines() {
|
||||
if line.contains("inet ") {
|
||||
if let Some(ip) = line.split_whitespace().nth(1) {
|
||||
return Some(ip.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let ps_cmd = format!(
|
||||
r#"Get-NetIPAddress -InterfaceAlias '{}' -AddressFamily IPv4 | Select-Object IPAddress,PrefixLength | ForEach-Object {{ "{0}/{1}" -f $_.IPAddress,$_.PrefixLength }}"#,
|
||||
interface
|
||||
);
|
||||
let output = Command::new("powershell")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", &ps_cmd])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
||||
if !stdout.is_empty() {
|
||||
return Some(stdout);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
log::log("network_interface", "OS not supported.", LogLevel::Error);
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -1,391 +0,0 @@
|
||||
use crate::log;
|
||||
use crate::log::LogLevel;
|
||||
use std::net::Ipv4Addr;
|
||||
use std::process::Command;
|
||||
|
||||
/// Prüft, ob eine angegebene IP-Adresse oder URL erreichbar ist
|
||||
///
|
||||
/// # Parameter
|
||||
/// - `address`: Eine IP-Adresse oder vollständige HTTPS-URL
|
||||
///
|
||||
/// # Rückgabe
|
||||
/// - `bool`: True wenn die Adresse erreichbar ist, False wenn nicht
|
||||
pub fn is_reachable(address: &str) -> bool {
|
||||
let addr_string = if let Some((ip_str, mask_str)) = address.split_once('/') {
|
||||
if ip_str.parse::<Ipv4Addr>().is_ok()
|
||||
&& mask_str.parse::<u8>().ok().filter(|m| *m <= 32).is_some()
|
||||
{
|
||||
let ip = ip_str.parse::<Ipv4Addr>().unwrap();
|
||||
let next_ip = Ipv4Addr::from(u32::from(ip).saturating_add(1));
|
||||
next_ip.to_string()
|
||||
} else {
|
||||
address.to_string()
|
||||
}
|
||||
} else {
|
||||
address.to_string()
|
||||
};
|
||||
let addr = addr_string.as_str();
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
if addr.parse::<Ipv4Addr>().is_ok() {
|
||||
let output = Command::new("ping")
|
||||
.args(["-c", "1", "-W", "2", addr])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
log("network_utils", &*stdout, LogLevel::Debug);
|
||||
|
||||
out.status.success()
|
||||
} else {
|
||||
log::log("network_utils", &*format!("Couldn't reach address {}!", addr), LogLevel::Error);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
let output = Command::new("curl")
|
||||
.args(["--head", "--silent", "--fail", addr])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
out.status.success()
|
||||
} else {
|
||||
log::log("network_utils", &*format!("Couldn't reach address {}!", addr), LogLevel::Error);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if addr.parse::<Ipv4Addr>().is_ok() {
|
||||
let output = Command::new("ping")
|
||||
.args(["-n", "1", "-w", "2000", addr])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
out.status.success()
|
||||
} else {
|
||||
log::log("network_utils", &*format!("Couldn't reach address {}!", addr), LogLevel::Error);
|
||||
false
|
||||
}
|
||||
} else {
|
||||
let output = Command::new("powershell")
|
||||
.args(["-Command", &format!("Invoke-WebRequest -Uri {} -Method HEAD -UseBasicParsing", addr)])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
out.status.success()
|
||||
} else {
|
||||
log::log("network_utils", &*format!("Couldn't reach address {}!", addr), LogLevel::Error);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
log::log("network_utils", "OS not supported.", LogLevel::Error);
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Berechnet die Netzwerkadresse aus einer IP-Adresse mit Subnetzmaske
|
||||
///
|
||||
/// # Parameter
|
||||
/// - `address`: Eine IPv4-Adresse mit Subnetzmaske im Format "xxx.xxx.xxx.xxx/yy"
|
||||
///
|
||||
/// # Rückgabe
|
||||
/// - Option<String>: Die Netzwerkadresse im gleichen Format oder None bei ungültiger Eingabe
|
||||
pub fn get_network_address(address: &str) -> Option<String> {
|
||||
let parts: Vec<&str> = address.split('/').collect();
|
||||
if parts.len() != 2 {
|
||||
return None;
|
||||
}
|
||||
|
||||
if let Ok(ip) = parts[0].parse::<Ipv4Addr>() {
|
||||
if let Ok(mask) = parts[1].parse::<u8>() {
|
||||
if mask > 32 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let ip_bits: u32 = u32::from(ip);
|
||||
let mask_bits: u32 = !0u32 << (32 - mask);
|
||||
let network = Ipv4Addr::from(ip_bits & mask_bits);
|
||||
|
||||
return Some(format!("{}/{}", network, mask));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
/// Ermittelt die IP-Adresse zu einer gegebenen MAC-Adresse im lokalen Netzwerk.
|
||||
/// Verwendet einen nmap-Scan, um das Gerät zu finden.
|
||||
///
|
||||
/// # Parameter
|
||||
/// - `mac`: MAC-Adresse im Format "xx:xx:xx:xx:xx:xx"
|
||||
/// - `network`: Netzwerkadresse im Format "xxx.xxx.xxx.xxx/yy"
|
||||
///
|
||||
/// # Rückgabe
|
||||
/// - Option<String>: Die IP-Adresse des Geräts oder None wenn nicht gefunden
|
||||
pub fn get_ip_from_mac(mac: &str, network: &str) -> Option<String> {
|
||||
#[cfg(any(target_os = "linux", target_os = "windows"))]
|
||||
{
|
||||
let output = Command::new("nmap")
|
||||
.args(["-sn", "-n", "--system-dns", "-PR", network])
|
||||
.output();
|
||||
|
||||
match output {
|
||||
Ok(out) => {
|
||||
if !out.status.success() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
log("network_utils", &*stdout, LogLevel::Debug);
|
||||
|
||||
// MAC normalisieren: nur Hex-Zeichen, klein, ohne Trennzeichen
|
||||
let normalize_mac = |m: &str| -> String {
|
||||
m.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.flat_map(|c| c.to_lowercase())
|
||||
.collect::<String>()
|
||||
};
|
||||
let target_mac = normalize_mac(mac);
|
||||
|
||||
// Beispielausgaben:
|
||||
// Nmap scan report for 192.168.0.10
|
||||
// Nmap scan report for printer.lan (192.168.0.10)
|
||||
// MAC Address: 00:11:22:33:44:55 (Vendor)
|
||||
//
|
||||
// Strategie:
|
||||
// - Merke die zuletzt gesehene IP aus "Nmap scan report for ..."
|
||||
// - Wenn danach eine "MAC Address:"-Zeile folgt und MAC passt -> gib IP zurück
|
||||
let mut current_ip: Option<String> = None;
|
||||
|
||||
for raw_line in stdout.lines() {
|
||||
let line = raw_line.trim();
|
||||
|
||||
if line.starts_with("Nmap scan report for") {
|
||||
// Versuche IP zu extrahieren:
|
||||
// 1) Klammerform: ... (x.x.x.x)
|
||||
// 2) Sonst letztes Token als IP
|
||||
let ip = if let Some(start) = line.rfind('(') {
|
||||
if let Some(end) = line.rfind(')') {
|
||||
let in_parens = &line[start + 1..end];
|
||||
Some(in_parens.to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
line.split_whitespace()
|
||||
.last()
|
||||
.map(|s| s.to_string())
|
||||
};
|
||||
|
||||
// Grobe Validierung IPv4
|
||||
if let Some(ip_str) = ip {
|
||||
let maybe_ip = ip_str.parse::<Ipv4Addr>().ok().map(|ip| ip.to_string());
|
||||
current_ip = maybe_ip;
|
||||
} else {
|
||||
current_ip = None;
|
||||
}
|
||||
} else if line.starts_with("MAC Address:") {
|
||||
// Format: "MAC Address: XX:XX:XX:XX:XX:XX (Vendor)"
|
||||
let mac_part = line.strip_prefix("MAC Address:").unwrap().trim();
|
||||
let mac_token = mac_part.split_whitespace().next().unwrap_or("");
|
||||
let seen_mac = normalize_mac(mac_token);
|
||||
|
||||
if !seen_mac.is_empty() && seen_mac == target_mac {
|
||||
if let Some(ip) = current_ip.clone() {
|
||||
return Some(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == std::io::ErrorKind::NotFound {
|
||||
log::log("network_utils", "nmap is not installed on the system!", LogLevel::Error);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
log::log("network_utils", "OS not supported.", LogLevel::Error);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Ermittelt die MAC-Adresse zu einer gegebenen IP-Adresse im lokalen Netzwerk
|
||||
/// durch Auslesen der ARP-Tabelle.
|
||||
///
|
||||
/// # Parameter
|
||||
/// - `ip`: IP-Adresse für die die MAC-Adresse ermittelt werden soll
|
||||
///
|
||||
/// # Rückgabe
|
||||
/// - Option<String>: Die MAC-Adresse des Geräts oder None wenn nicht gefunden
|
||||
pub fn get_mac_from_ip(ip: &str) -> Option<String> {
|
||||
// Hilfsfunktionen zur MAC-Normalisierung
|
||||
let normalize_mac = |m: &str| -> String {
|
||||
m.chars()
|
||||
.filter(|c| c.is_ascii_hexdigit())
|
||||
.flat_map(|c| c.to_lowercase())
|
||||
.collect::<String>()
|
||||
};
|
||||
let canonicalize_mac = |hex_no_sep: &str| -> String {
|
||||
hex_no_sep
|
||||
.as_bytes()
|
||||
.chunks(2)
|
||||
.map(std::str::from_utf8)
|
||||
.filter_map(Result::ok)
|
||||
.collect::<Vec<&str>>()
|
||||
.join(":")
|
||||
};
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// 1) Ziel kurz anpingen, damit ein ARP-Eintrag entsteht
|
||||
let _ = Command::new("ping")
|
||||
.args(["-c", "1", "-W", "1", ip])
|
||||
.output();
|
||||
|
||||
// 2) Bis zu drei Versuche, ARP/Neigh einzulesen (kleine Wartezeit)
|
||||
for _ in 0..3 {
|
||||
let output = Command::new("ip")
|
||||
.args(["neigh", "show", ip])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if out.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
// Beispiel: 192.168.1.1 dev eth0 lladdr 00:11:22:33:44:55 REACHABLE
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.get(0).copied() == Some(ip) {
|
||||
if let Some(idx) = parts.iter().position(|p| *p == "lladdr") {
|
||||
if let Some(mac_tok) = parts.get(idx + 1) {
|
||||
let seen_norm = normalize_mac(mac_tok);
|
||||
if seen_norm.len() == 12 {
|
||||
return Some(canonicalize_mac(&seen_norm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// 1) Ziel kurz anpingen, damit ein ARP-Eintrag entsteht
|
||||
let _ = Command::new("ping")
|
||||
.args(["-n", "1", "-w", "500", ip])
|
||||
.output();
|
||||
|
||||
// 2) Bis zu drei Versuche, ARP einzulesen (kleine Wartezeit)
|
||||
for _ in 0..3 {
|
||||
// Hinweis: `arp -a` auf Windows zeigt die gesamte Tabelle; mit IP filtert es i. d. R. auf Interface,
|
||||
// daher filtern wir inhaltlich auf die Zeile mit der Ziel-IP.
|
||||
let output = Command::new("arp")
|
||||
.args(["-a"])
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if out.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&out.stdout);
|
||||
for line in stdout.lines() {
|
||||
let cols: Vec<&str> = line.split_whitespace().collect();
|
||||
// Typisch: "192.168.1.1 00-11-22-33-44-55 dynamic"
|
||||
if cols.get(0).copied() == Some(ip) && cols.len() >= 2 {
|
||||
let mac_colon = cols[1].replace('-', ":");
|
||||
let seen_norm = normalize_mac(&mac_colon);
|
||||
if seen_norm.len() == 12 {
|
||||
return Some(canonicalize_mac(&seen_norm));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
log::log("network_utils", "OS not supported.", LogLevel::Error);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Sendet ein Wake-on-LAN Magic Packet an eine MAC-Adresse
|
||||
///
|
||||
/// # Parameter
|
||||
/// - `mac`: MAC-Adresse im Format "xx:xx:xx:xx:xx:xx"
|
||||
///
|
||||
/// # Funktionsweise
|
||||
/// Sendet ein Magic Packet bestehend aus:
|
||||
/// - 6 Bytes 0xFF (Synchronisierung)
|
||||
/// - 16x die MAC-Adresse wiederholt
|
||||
/// - Port 9 (Standard Wake-on-LAN Port)
|
||||
pub fn wake_on_land(mac: &str) {
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let output = Command::new("wakeonlan")
|
||||
.arg(mac)
|
||||
.output();
|
||||
|
||||
if let Ok(out) = output {
|
||||
if !out.status.success() {
|
||||
log("network_utils", "Failed to send WOL packet!", LogLevel::Error);
|
||||
}
|
||||
} else {
|
||||
log("network_utils", "wakeonlan is not installed on the system!", LogLevel::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let ps_cmd = format!(
|
||||
r#"
|
||||
$mac="{}"
|
||||
$macByteArray=[byte[]]::new(102)
|
||||
6..101 | %{{ $macByteArray[$_]=0xFF }}
|
||||
$macAddr=[byte[]]::new(6)
|
||||
$mac -split "[:-]" | %{{$macAddr[$i]=[Convert]::ToByte($_,16); $i++}}
|
||||
6..101 | %{{ $macByteArray[$_]=$macAddr[($_ - 6) % 6] }}
|
||||
$UdpClient=New-Object System.Net.Sockets.UdpClient
|
||||
$UdpClient.Connect(([System.Net.IPAddress]::Broadcast),9)
|
||||
$UdpClient.Send($macByteArray,$macByteArray.Length)
|
||||
"#,
|
||||
mac
|
||||
);
|
||||
|
||||
let output = Command::new("powershell")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", &ps_cmd])
|
||||
.output();
|
||||
|
||||
if let Err(_) = output {
|
||||
log::log("network_utils", "Failed to send WOL packet!", LogLevel::Error);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "linux", target_os = "windows")))]
|
||||
{
|
||||
log::log("network_utils", "OS not supported.", LogLevel::Error);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
use std::env;
|
||||
|
||||
/// Liefert den Programmnamen (Dateistamm der aktuellen ausführbaren Datei).
|
||||
///
|
||||
/// Rückgabe:
|
||||
/// - Dateistamm der aktuellen Executable als `String`.
|
||||
/// - Fallback `"app"`, wenn der Name nicht ermittelt werden kann.
|
||||
pub(crate) fn program_name() -> String {
|
||||
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())
|
||||
}
|
||||
-62
@@ -1,62 +0,0 @@
|
||||
use std::env;
|
||||
use std::process::Command;
|
||||
|
||||
/// Prüft, ob das Programm mit Root-/Administrator-Rechten ausgeführt wird.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `true` - Das Programm läuft mit erhöhten Rechten
|
||||
/// * `false` - Das Programm läuft mit normalen Benutzerrechten
|
||||
pub fn is_run_as_root() -> bool {
|
||||
#[cfg(any(target_os = "windows"))]
|
||||
{
|
||||
// Windows: Prüfen ob Admin-Rechte vorhanden
|
||||
Command::new("net")
|
||||
.args(&["session"])
|
||||
.output()
|
||||
.map(|output| output.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows")))]
|
||||
{
|
||||
// Linux/Unix: Prüfen ob Root oder sudo
|
||||
unsafe { libc::geteuid() == 0 }
|
||||
}
|
||||
}
|
||||
/// Startet das Programm mit Root-/Administrator-Rechten neu.
|
||||
///
|
||||
/// Diese Funktion versucht das Programm mit erhöhten Rechten neu zu starten.
|
||||
///
|
||||
/// # Details
|
||||
///
|
||||
/// - Unter Linux wird das Programm mit `sudo` neu gestartet
|
||||
/// - Unter Windows wird der UAC-Dialog zur Rechteanfrage angezeigt
|
||||
pub fn run_as_root() {
|
||||
#[cfg(any(target_os = "windows"))]
|
||||
{
|
||||
let commandline_args: Vec<String> = env::args().collect();
|
||||
let program_path = commandline_args[0].clone();
|
||||
|
||||
Command::new("powershell")
|
||||
.args(&[
|
||||
"Start-Process",
|
||||
&program_path,
|
||||
"-ArgumentList",
|
||||
&commandline_args[1..].join(" "),
|
||||
"-Verb",
|
||||
"RunAs"
|
||||
])
|
||||
.exec();
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "windows")))]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
||||
let commandline_args: Vec<String> = env::args().collect();
|
||||
let _output = Command::new("sudo")
|
||||
.args(&commandline_args)
|
||||
.exec(); // Bye bye never returns
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user