Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57a467cb58
|
||
|
|
2bd6a6909c
|
||
|
|
d4dfa1d124
|
||
|
|
c1d13153e2
|
@@ -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
|
||||||
@@ -14,11 +14,22 @@ jobs:
|
|||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
cache: false
|
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
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
cargo test
|
cargo test
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
name: Renovate
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 * * * *"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
renovate:
|
||||||
|
name: Dependency-Updates prüfen & Pull Requests erstellen
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: ghcr.io/renovatebot/renovate:44.82.0
|
||||||
|
steps:
|
||||||
|
- name: Renovate ausführen
|
||||||
|
run: renovate
|
||||||
|
env:
|
||||||
|
RENOVATE_PLATFORM: gitea
|
||||||
|
RENOVATE_ENDPOINT: ${{ gitea.server_url || github.server_url }}/api/v1/
|
||||||
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
RENOVATE_REPOSITORIES: ${{ gitea.repository || github.repository }}
|
||||||
|
RENOVATE_AUTODISCOVER: "false"
|
||||||
|
RENOVATE_GIT_AUTHOR: "Renovate Bot <renovate-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
|
||||||
@@ -14,11 +14,22 @@ jobs:
|
|||||||
uses: actions/checkout@v7
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
- name: Install Rust Toolchain
|
- name: Install Rust Toolchain
|
||||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||||
with:
|
with:
|
||||||
toolchain: stable
|
toolchain: stable
|
||||||
cache: false
|
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
|
- name: Run Tests
|
||||||
run: |
|
run: |
|
||||||
cargo test
|
cargo test
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
name: TruffleHog Secret Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 6 * * 1"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
trufflehog-scan:
|
||||||
|
name: TruffleHog
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
TRUFFLEHOG_VERSION: "3.97.4"
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Lokales bin-Verzeichnis zum PATH hinzufügen
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
|
||||||
|
- name: Cache TruffleHog-Binary
|
||||||
|
id: cache-trufflehog
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: ~/.local/bin/trufflehog
|
||||||
|
key: trufflehog-bin-${{ runner.os }}-${{ env.TRUFFLEHOG_VERSION }}
|
||||||
|
|
||||||
|
- name: Install TruffleHog
|
||||||
|
if: steps.cache-trufflehog.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
curl -fsSL -o trufflehog.tar.gz \
|
||||||
|
"https://github.com/trufflesecurity/trufflehog/releases/download/v${TRUFFLEHOG_VERSION}/trufflehog_${TRUFFLEHOG_VERSION}_linux_amd64.tar.gz"
|
||||||
|
tar -xzf trufflehog.tar.gz trufflehog
|
||||||
|
chmod +x trufflehog
|
||||||
|
mv trufflehog "$HOME/.local/bin/trufflehog"
|
||||||
|
rm trufflehog.tar.gz
|
||||||
|
|
||||||
|
- name: Run TruffleHog Scanner
|
||||||
|
run: |
|
||||||
|
set +e
|
||||||
|
trufflehog git file://. --results=verified,unknown --fail --json > trufflehog-results.json
|
||||||
|
echo "TRUFFLEHOG_EXIT=$?" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Ergebnisse & Gitea-Issue erstellen/aktualisieren
|
||||||
|
env:
|
||||||
|
GITEA_URL: ${{ gitea.server_url || github.server_url }}
|
||||||
|
REPO: ${{ gitea.repository || github.repository }}
|
||||||
|
TOKEN: ${{ secrets.SECURITY_TOKEN }}
|
||||||
|
RUN_URL: ${{ gitea.server_url || github.server_url }}/${{ gitea.repository || github.repository }}/actions/runs/${{ gitea.run_id || github.run_id }}
|
||||||
|
ISSUE_TITLE: "Security-Scan: TruffleHog Secrets"
|
||||||
|
ISSUE_LABEL: "security-scan-trufflehog"
|
||||||
|
run: |
|
||||||
|
python3 scripts/report-security-issue.py "" "" trufflehog-results.json
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
name: Unit-Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types:
|
||||||
|
- opened
|
||||||
|
- synchronize
|
||||||
|
- reopened
|
||||||
|
branches:
|
||||||
|
- testing
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
name: Unit-Tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Repository
|
||||||
|
uses: actions/checkout@v7
|
||||||
|
|
||||||
|
- name: Install Rust Toolchain
|
||||||
|
uses: actions-rust-lang/setup-rust-toolchain@v2
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
cache: false
|
||||||
|
|
||||||
|
- name: Cache Cargo-Abhängigkeiten & Build-Artefakte
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: cargo-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
cargo-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Run Tests
|
||||||
|
run: cargo test
|
||||||
+19
-18
@@ -1,3 +1,21 @@
|
|||||||
|
# ---> Rust
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
# RustRover
|
||||||
|
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||||
|
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||||
|
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||||
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
|
#.idea/
|
||||||
# ---> JetBrains
|
# ---> JetBrains
|
||||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
@@ -91,21 +109,4 @@ fabric.properties
|
|||||||
# Built Visual Studio Code Extensions
|
# Built Visual Studio Code Extensions
|
||||||
*.vsix
|
*.vsix
|
||||||
|
|
||||||
# ---> Rust
|
.junie/plans
|
||||||
# Generated by Cargo
|
|
||||||
# will have compiled files and executables
|
|
||||||
debug/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# These are backup files generated by rustfmt
|
|
||||||
**/*.rs.bk
|
|
||||||
|
|
||||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
|
||||||
*.pdb
|
|
||||||
|
|
||||||
# RustRover
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
#.idea/
|
|
||||||
|
|||||||
Generated
+15
@@ -17,6 +17,21 @@
|
|||||||
<Item>
|
<Item>
|
||||||
<option name="path" value=".gitea/workflows/testing.yaml" />
|
<option name="path" value=".gitea/workflows/testing.yaml" />
|
||||||
</Item>
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/code-quality.yaml" />
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/renovate.yaml" />
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/security-scan.yaml" />
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/trufflehog-scan.yaml" />
|
||||||
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<option name="path" value=".gitea/workflows/unit-tests.yaml" />
|
||||||
|
</Item>
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</SchemaInfo>
|
</SchemaInfo>
|
||||||
|
|||||||
Generated
+1
@@ -3,6 +3,7 @@
|
|||||||
<component name="NewModuleRootManager">
|
<component name="NewModuleRootManager">
|
||||||
<content url="file://$MODULE_DIR$">
|
<content url="file://$MODULE_DIR$">
|
||||||
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/tests" isTestSource="true" />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/target" />
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
</content>
|
</content>
|
||||||
<orderEntry type="inheritedJdk" />
|
<orderEntry type="inheritedJdk" />
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
# AGENTS.md
|
# AGENTS.md
|
||||||
|
|
||||||
Dieses Dokument definiert Richtlinien, Konventionen und Arbeitsanweisungen für KI-Agenten und LLM-Tools, die an diesem Repository oder daraus erstellten Rust-Crates arbeiten.
|
Dieses Dokument definiert Richtlinien, Konventionen und Arbeitsanweisungen für KI-Agenten und LLM-Tools, die an der Rust-Bibliothek `program-ctdra` arbeiten.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 1. Projektübersicht & Kontext
|
## 1. Projektübersicht & Kontext
|
||||||
|
|
||||||
- **Typ:** Rust Library Crate Template
|
- **Name:** `program-ctdra`
|
||||||
|
- **Typ:** Rust Library Crate
|
||||||
|
- **Zweck:** Einfache und zuverlässige Ermittlung des Programmnamens (Dateistamm der aktuellen ausführbaren Datei) mit sicherem Fallback
|
||||||
- **Rust Edition:** `2024`
|
- **Rust Edition:** `2024`
|
||||||
- **Einstiegspunkt:** `src/lib.rs`
|
- **Einstiegspunkt:** `src/lib.rs`
|
||||||
- **CI/CD Plattform:** Gitea Actions (`.gitea/workflows/`)
|
- **CI/CD Plattform:** Gitea Actions (`.gitea/workflows/`)
|
||||||
@@ -20,11 +22,12 @@ Dieses Dokument definiert Richtlinien, Konventionen und Arbeitsanweisungen für
|
|||||||
### 2.1 Sprache & Idiomatik
|
### 2.1 Sprache & Idiomatik
|
||||||
- Verwende modernes, idiomatisches Rust (Edition 2024).
|
- Verwende modernes, idiomatisches Rust (Edition 2024).
|
||||||
- Bevorzuge explizite Typen und klare Signaturen in öffentlichen Schnittstellen (`pub`).
|
- Bevorzuge explizite Typen und klare Signaturen in öffentlichen Schnittstellen (`pub`).
|
||||||
- Halte die API ergonomisch und benutzerfreundlich.
|
- Halte die API ergonomisch, minimalistisch und benutzerfreundlich.
|
||||||
|
- Vermeide unnötige externe Abhängigkeiten (Zero Dependencies).
|
||||||
|
|
||||||
### 2.2 Fehlerbehandlung
|
### 2.2 Fehlerbehandlung
|
||||||
- Nutze `Result<T, E>` und `Option<T>` für alle potenziell fehlschlagenden Operationen.
|
- Nutze `Result<T, E>` und `Option<T>` für alle potenziell fehlschlagenden Operationen.
|
||||||
- Definiere aussagekräftige, domänenspezifische Fehlertypen (z. B. via `thiserror` oder standardmäßig `std::error::Error`).
|
- Definiere aussagekräftige, domänenspezifische Fehlertypen, falls zutreffend.
|
||||||
- **Verboten im produktiven Bibliothekscode (`src/`):**
|
- **Verboten im produktiven Bibliothekscode (`src/`):**
|
||||||
- Unbegründete `unwrap()`, `expect()` oder `panic!()` Aufrufe.
|
- Unbegründete `unwrap()`, `expect()` oder `panic!()` Aufrufe.
|
||||||
- Ignorieren von Fehlern via `let _ = ...`, es sei denn, es ist explizit begründet und dokumentiert.
|
- Ignorieren von Fehlern via `let _ = ...`, es sei denn, es ist explizit begründet und dokumentiert.
|
||||||
@@ -92,10 +95,9 @@ Die CI/CD-Pipelines werden über Gitea Actions gesteuert:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Arbeitsanweisungen für Agenten bei Projekt-Initialisierung
|
## 5. Richtlinien für Agenten bei Änderungen
|
||||||
|
|
||||||
Wenn dieser Template-Stand verwendet wird, um eine neue Crate zu erstellen:
|
1. Halte die `Cargo.toml`-Metadaten (`name`, `version`, `authors`, `repository`, `description`) aktuell und konsistent.
|
||||||
1. Aktualisiere die `Cargo.toml`-Metadaten (`name`, `version`, `authors`, `repository`, `description`).
|
2. Halte die `README.md` synchron mit allen Änderungen an der API oder Funktionalität der Crate.
|
||||||
2. Passe die `README.md` an die konkrete Funktionalität der neuen Crate an.
|
3. Behalte die Gitea-Workflows bei und stelle sicher, dass alle Validierungsschritte bestehen.
|
||||||
3. Behalte die Gitea-Workflows bei oder passe sie an das Ziel-Repository an.
|
|
||||||
4. Stelle sicher, dass keine Secrets, temporären Build-Dateien (`target/`) oder IDE-spezifischen Caches (außer `.idea` Konfigurationen) committed werden.
|
4. Stelle sicher, dass keine Secrets, temporären Build-Dateien (`target/`) oder IDE-spezifischen Caches (außer `.idea` Konfigurationen) committed werden.
|
||||||
|
|||||||
Generated
+2
-2
@@ -3,5 +3,5 @@
|
|||||||
version = 4
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust-creat-template"
|
name = "program-ctdra"
|
||||||
version = "1.0.0"
|
version = "1.0.1"
|
||||||
|
|||||||
+5
-5
@@ -1,12 +1,12 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rust-creat-template" # TODO Setzen
|
name = "program-ctdra"
|
||||||
version = "1.0.0" # TODO Setzen
|
version = "1.0.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ['DragonSlayer_14'] # TODO Setzen
|
authors = ['DragonSlayer_14']
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://gitea.creative-dragonslayer.de/Templates/rust-crate-template" # TODO Setzen
|
repository = "https://gitea.creative-dragonslayer.de/Rust-Crates/program"
|
||||||
description = "Ein Template-Projekt, das fürs erstellen von Rust-Crates verwendet werden kann." # TODO Setzen
|
description = "Eine schlanke Rust-Bibliothek zur einfachen und zuverlässigen Ermittlung des Namens der aktuellen ausführbaren Datei."
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,66 @@
|
|||||||
# Rust Crate Template
|
# program-ctdra
|
||||||
|
|
||||||
Ein vorkonfiguriertes Template-Projekt für die schnelle und standardisierte Entwicklung von Rust-Crates (Libraries) mit automatisierter CI/CD-Pipeline für Gitea.
|
Eine schlanke, abhängigkeitsfreie Rust-Bibliothek zur einfachen und zuverlässigen Ermittlung des Programmnamens (Dateistamm der aktuellen ausführbaren Datei).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚀 Übersicht & Features
|
## 🚀 Übersicht & Features
|
||||||
|
|
||||||
- **Rust Edition 2024**: Moderner Rust-Standard mit optimierten Profil-Einstellungen (`profile.release.debug = "none"`).
|
- **Einfache API**:
|
||||||
|
- `program_name()`: Ermittelt den Programmnamen mit sicherem Fallback (`"app"`).
|
||||||
|
- `program_name_or(fallback)`: Ermittelt den Programmnamen mit anpassbarem Fallback.
|
||||||
|
- `try_program_name()`: Gibt den Programmnamen als `Option<String>` zurück.
|
||||||
|
- **Zero Dependencies**: Nutzt ausschließlich die Rust-Standardbibliothek (`std::env`, `std::path`).
|
||||||
|
- **Rust Edition 2024**: Moderner Rust-Standard mit optimierten Release-Profil-Einstellungen (`debug = "none"`).
|
||||||
|
- **Vollständig getestet & dokumentiert**: Mit Unit-Tests, Integrationstests und getesteten Rustdoc-Codebeispielen.
|
||||||
- **Automatisierte CI/CD-Workflows (Gitea Actions)**:
|
- **Automatisierte CI/CD-Workflows (Gitea Actions)**:
|
||||||
- **Testing-Pipeline (`testing`-Branch)**: Führt Tests und Compiler-Checks aus und erstellt automatisch ein Gitea Pre-Release (`v<VERSION>-preview`) inklusive `.crate`-Paket als Release-Asset.
|
- **Testing-Pipeline (`testing`-Branch)**: Baut, testet und erstellt automatisch ein Gitea Pre-Release (`v<VERSION>-preview`) inklusive `.crate`-Paket.
|
||||||
- **Main-Release-Pipeline (`main`-Branch)**: Führt Tests und Compiler-Checks aus, paketiert die Crate, veröffentlicht sie in der Gitea Cargo Package Registry und erstellt ein offizielles Gitea Release (`v<VERSION>`) mit Asset.
|
- **Main-Release-Pipeline (`main`-Branch)**: Baut, testet, paketiert, veröffentlicht in der Gitea Cargo Package Registry und erstellt ein Gitea Release (`v<VERSION>`).
|
||||||
- **Integrierte Gitea Package Registry**: Vorkonfigurierte sparse index Registry-Anbindung.
|
- **GPL-3.0-or-later Lizenz**: Freie Software unter der GNU General Public License v3.0 oder neuer.
|
||||||
- **GPL-3.0-or-later Lizenz**: Vorkonfiguriert mit Lizenzdatei und Metadaten.
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 Installation & Einbindung
|
||||||
|
|
||||||
|
Um `program-ctdra` aus der Gitea Package Registry in einem Cargo-Projekt zu verwenden:
|
||||||
|
|
||||||
|
### 1. Registry konfigurieren
|
||||||
|
Füge die Gitea Package Registry zu deiner lokalen `~/.cargo/config.toml` oder projektweiten `.cargo/config.toml` hinzu:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[registries.gitea]
|
||||||
|
index = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Abhängigkeit in `Cargo.toml` deklarieren
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
program-ctdra = { version = "1.0.0", registry = "gitea" }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Anwendungsbeispiele
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use program_ctdra::{program_name, program_name_or, try_program_name};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
// 1. Standard-Aufruf mit Fallback "app"
|
||||||
|
let name = program_name();
|
||||||
|
println!("Aktuelles Programm: {name}");
|
||||||
|
|
||||||
|
// 2. Mit benutzerdefiniertem Fallback
|
||||||
|
let service_name = program_name_or("mein-dienst");
|
||||||
|
println!("Dienst-Name: {service_name}");
|
||||||
|
|
||||||
|
// 3. Optionale Ermittlung ohne Fallback
|
||||||
|
match try_program_name() {
|
||||||
|
Some(name) => println!("Executable-Name ermittelt: {name}"),
|
||||||
|
None => eprintln!("Konnte Programmnamen nicht ermitteln!"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -25,7 +74,9 @@ Ein vorkonfiguriertes Template-Projekt für die schnelle und standardisierte Ent
|
|||||||
│ └── testing.yaml # CI/CD: Test, Check & Pre-Release für 'testing'
|
│ └── testing.yaml # CI/CD: Test, Check & Pre-Release für 'testing'
|
||||||
├── .idea/ # Vorkonfigurierte JetBrains IDE Einstellungen
|
├── .idea/ # Vorkonfigurierte JetBrains IDE Einstellungen
|
||||||
├── src/
|
├── src/
|
||||||
│ └── lib.rs # Einstiegspunkt der Crate / Library
|
│ └── lib.rs # Bibliotheks-Implementierung & Unit-Tests
|
||||||
|
├── tests/
|
||||||
|
│ └── integration_test.rs # Integrationstests für die öffentliche API
|
||||||
├── Cargo.lock
|
├── Cargo.lock
|
||||||
├── Cargo.toml # Crate-Manifest & Metadaten
|
├── Cargo.toml # Crate-Manifest & Metadaten
|
||||||
├── LICENSE # GNU General Public License v3.0
|
├── LICENSE # GNU General Public License v3.0
|
||||||
@@ -35,39 +86,16 @@ Ein vorkonfiguriertes Template-Projekt für die schnelle und standardisierte Ent
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🛠️ Verwendung als Template
|
## 💻 Lokale Entwicklung & Validierung
|
||||||
|
|
||||||
### 1. Template initialisieren & Metadaten anpassen
|
Die gängigen Cargo-Befehle zur Entwicklung und Verifizierung:
|
||||||
Passe nach dem Klonen bzw. Erstellen des neuen Repositories die Platzhalter in `Cargo.toml` an:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[package]
|
|
||||||
name = "mein-crate-name"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
authors = ['DeinName <deine.email@example.com>']
|
|
||||||
readme = "README.md"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
repository = "https://gitea.example.com/Organisation/mein-crate-name"
|
|
||||||
description = "Beschreibung der Crate."
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Gitea Repository Secrets einrichten
|
|
||||||
Für die Veröffentlichung und Release-Erstellung in Gitea Actions muss mindestens ein Access-Token als Repository-Secret hinterlegt werden (z. B. unter `Einstellungen -> Secrets -> Actions`):
|
|
||||||
- `PACKAGE_TOKEN` (oder alternativ `RELEASE_TOKEN`, `GITEA_TOKEN`): Ein Personal Access Token mit Berechtigungen für Packages (`write:package`) und Repositories/Releases (`write:repository`).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💻 Lokale Entwicklung & Befehle
|
|
||||||
|
|
||||||
Die gängigen Cargo-Befehle zur Entwicklung:
|
|
||||||
|
|
||||||
- **Kompilierung prüfen:**
|
- **Kompilierung prüfen:**
|
||||||
```bash
|
```bash
|
||||||
cargo check --all-targets
|
cargo check --all-targets
|
||||||
```
|
```
|
||||||
|
|
||||||
- **Tests ausführen:**
|
- **Tests ausführen (Unit-, Integrations- und Doc-Tests):**
|
||||||
```bash
|
```bash
|
||||||
cargo test
|
cargo test
|
||||||
```
|
```
|
||||||
@@ -97,25 +125,7 @@ Die gängigen Cargo-Befehle zur Entwicklung:
|
|||||||
| `testing` | Push auf `testing` | • `cargo test`<br>• `cargo check --all-targets`<br>• `cargo package`<br>• Erstellt/Aktualisiert Pre-Release `v<VERSION>-preview` mit `.crate`-Asset |
|
| `testing` | Push auf `testing` | • `cargo test`<br>• `cargo check --all-targets`<br>• `cargo package`<br>• Erstellt/Aktualisiert Pre-Release `v<VERSION>-preview` mit `.crate`-Asset |
|
||||||
| `main` | Push auf `main` | • `cargo test`<br>• `cargo check --all-targets`<br>• `cargo package`<br>• Veröffentlicht Crate in Gitea Package Registry<br>• Erstellt/Aktualisiert Release `v<VERSION>` mit `.crate`-Asset |
|
| `main` | Push auf `main` | • `cargo test`<br>• `cargo check --all-targets`<br>• `cargo package`<br>• Veröffentlicht Crate in Gitea Package Registry<br>• Erstellt/Aktualisiert Release `v<VERSION>` mit `.crate`-Asset |
|
||||||
|
|
||||||
> **Hinweis zur Versionierung:** Die Versionsnummer wird automatisch aus `Cargo.toml` (`version = "..."`) ausgelesen. Passe vor einem Merge auf `main` oder `testing` die Version in `Cargo.toml` entsprechend SemVer an.
|
> **Hinweis zur Versionierung:** Die Versionsnummer wird automatisch aus `Cargo.toml` (`version = "..."`) ausgelesen. Vor einem Merge auf `main` oder `testing` muss die Version in `Cargo.toml` entsprechend SemVer angepasst werden.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📦 Verwenden der Crate in anderen Projekten
|
|
||||||
|
|
||||||
Um die in der Gitea Package Registry veröffentlichte Crate in einem anderen Cargo-Projekt zu verwenden:
|
|
||||||
|
|
||||||
1. Trage die Registry in deiner lokalen `~/.cargo/config.toml` oder projektweiten `.cargo/config.toml` ein:
|
|
||||||
```toml
|
|
||||||
[registries.gitea]
|
|
||||||
index = "sparse+https://gitea.creative-dragonslayer.de/api/packages/Rust-Crates/cargo/"
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Binde die Crate in deiner `Cargo.toml` ein:
|
|
||||||
```toml
|
|
||||||
[dependencies]
|
|
||||||
mein-crate-name = { version = "1.0.0", registry = "gitea" }
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+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
|
||||||
+102
@@ -0,0 +1,102 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": ["config:recommended"],
|
||||||
|
"timezone": "Europe/Berlin",
|
||||||
|
"schedule": ["before 6am on monday"],
|
||||||
|
"allowCustomCrateRegistries": true,
|
||||||
|
"baseBranchPatterns": [
|
||||||
|
"dev"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchFileNames": [".gitea/workflows/**"],
|
||||||
|
"groupName": "Gitea Actions",
|
||||||
|
"separateMajorMinor": false,
|
||||||
|
"separateMinorPatch": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"groupName": "Cargo Dependencies",
|
||||||
|
"separateMajorMinor": false,
|
||||||
|
"separateMinorPatch": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["dockerfile", "docker-compose"],
|
||||||
|
"groupName": "Docker-Images",
|
||||||
|
"separateMajorMinor": false,
|
||||||
|
"separateMinorPatch": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"customManagers": [
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"TRIVY_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "aquasecurity/trivy",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"OSV_SCANNER_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "google/osv-scanner",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"TRUFFLEHOG_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "trufflesecurity/trufflehog",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"CARGO_BINSTALL_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "cargo-bins/cargo-binstall",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"CARGO_DEB_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "cargo-deb",
|
||||||
|
"datasourceTemplate": "crate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.gitea/workflows/.+\\.ya?ml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"CARGO_GENERATE_RPM_VERSION:\\s*\"(?<currentValue>[^\"]+)\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "cargo-generate-rpm",
|
||||||
|
"datasourceTemplate": "crate"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,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()
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
//! # program-ctdra
|
||||||
|
//!
|
||||||
|
//! Eine schlanke, abhängigkeitsfreie Rust-Bibliothek zur einfachen und zuverlässigen
|
||||||
|
//! Ermittlung des aktuellen Programmnamens (des Dateistamms der ausführbaren Datei).
|
||||||
|
//!
|
||||||
|
//! ## Übersicht
|
||||||
|
//!
|
||||||
|
//! - [`program_name`]: Gibt den Programmnamen zurück oder `"app"`, falls dieser nicht ermittelt werden kann.
|
||||||
|
//! - [`try_program_name`]: Gibt den Programmnamen als [`Option<String>`] zurück.
|
||||||
|
//! - [`program_name_or`]: Gibt den Programmnamen oder einen benutzerdefinierten Fallback zurück.
|
||||||
|
//!
|
||||||
|
//! ## Beispiele
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use program_ctdra::{program_name, program_name_or, try_program_name};
|
||||||
|
//!
|
||||||
|
//! // Standard-Programmname mit Fallback "app"
|
||||||
|
//! let name = program_name();
|
||||||
|
//! assert!(!name.is_empty());
|
||||||
|
//!
|
||||||
|
//! // Programmname mit benutzerdefiniertem Fallback
|
||||||
|
//! let custom = program_name_or("mein-dienst");
|
||||||
|
//! assert!(!custom.is_empty());
|
||||||
|
//!
|
||||||
|
//! // Optionale Ermittlung ohne Fallback
|
||||||
|
//! if let Some(prog) = try_program_name() {
|
||||||
|
//! println!("Ausgeführt als: {prog}");
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use std::env;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Ermittelt den Programmnamen (Dateistamm) aus einem übergebenen Pfad.
|
||||||
|
fn extract_stem_from_path(path: &Path) -> Option<String> {
|
||||||
|
path.file_stem()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Versucht, den Programmnamen (Dateistamm der aktuellen ausführbaren Datei) zu ermitteln.
|
||||||
|
///
|
||||||
|
/// Gibt `Some(String)` zurück, wenn der Pfad der aktuellen Executable ermittelt werden
|
||||||
|
/// konnte und ein nicht-leerer Dateistamm vorhanden ist, andernfalls `None`.
|
||||||
|
///
|
||||||
|
/// # Beispiele
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use program_ctdra::try_program_name;
|
||||||
|
///
|
||||||
|
/// let maybe_name = try_program_name();
|
||||||
|
/// // In einer regulären Test- oder Binärumgebung ist der Name in der Regel vorhanden:
|
||||||
|
/// assert!(maybe_name.is_some());
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn try_program_name() -> Option<String> {
|
||||||
|
env::current_exe()
|
||||||
|
.ok()
|
||||||
|
.and_then(|p| extract_stem_from_path(&p))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liefert den Programmnamen (Dateistamm der aktuellen ausführbaren Datei) oder einen Fallback-Wert.
|
||||||
|
///
|
||||||
|
/// # Parameter
|
||||||
|
///
|
||||||
|
/// - `fallback`: Ein Wert, der in ein [`String`] umgewandelt werden kann und verwendet wird,
|
||||||
|
/// wenn der Programmname nicht ermittelt werden kann.
|
||||||
|
///
|
||||||
|
/// # Beispiele
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use program_ctdra::program_name_or;
|
||||||
|
///
|
||||||
|
/// let name = program_name_or("fallback_app");
|
||||||
|
/// assert!(!name.is_empty());
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn program_name_or(fallback: impl Into<String>) -> String {
|
||||||
|
try_program_name().unwrap_or_else(|| fallback.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Liefert den Programmnamen (Dateistamm der aktuellen ausführbaren Datei).
|
||||||
|
///
|
||||||
|
/// # Rückgabewert
|
||||||
|
///
|
||||||
|
/// - Dateistamm der aktuellen Executable als [`String`].
|
||||||
|
/// - Fallback `"app"`, wenn der Name nicht ermittelt werden kann.
|
||||||
|
///
|
||||||
|
/// # Beispiele
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use program_ctdra::program_name;
|
||||||
|
///
|
||||||
|
/// let name = program_name();
|
||||||
|
/// assert!(!name.is_empty());
|
||||||
|
/// ```
|
||||||
|
#[must_use]
|
||||||
|
pub fn program_name() -> String {
|
||||||
|
program_name_or("app")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_stem_from_path_valid() {
|
||||||
|
assert_eq!(
|
||||||
|
extract_stem_from_path(&PathBuf::from("/usr/bin/my-daemon")),
|
||||||
|
Some("my-daemon".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
extract_stem_from_path(&PathBuf::from("/opt/apps/server.exe")),
|
||||||
|
Some("server".to_string())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
extract_stem_from_path(&PathBuf::from("app.bin")),
|
||||||
|
Some("app".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_extract_stem_from_path_empty_or_root() {
|
||||||
|
assert_eq!(extract_stem_from_path(&PathBuf::from("/")), None);
|
||||||
|
assert_eq!(extract_stem_from_path(&PathBuf::from("")), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_program_name_returns_non_empty() {
|
||||||
|
let name = program_name();
|
||||||
|
assert!(!name.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_try_program_name_in_test_env() {
|
||||||
|
let name = try_program_name();
|
||||||
|
assert!(name.is_some());
|
||||||
|
let name_str = name.unwrap();
|
||||||
|
assert!(!name_str.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_program_name_or_with_custom_fallback() {
|
||||||
|
let name = program_name_or("custom_fallback");
|
||||||
|
assert!(!name.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
use program_ctdra::{program_name, program_name_or, try_program_name};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_public_api_program_name() {
|
||||||
|
let name = program_name();
|
||||||
|
assert!(!name.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_public_api_try_program_name() {
|
||||||
|
let name = try_program_name();
|
||||||
|
assert!(name.is_some());
|
||||||
|
if let Some(n) = name {
|
||||||
|
assert!(!n.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_public_api_program_name_or() {
|
||||||
|
let name = program_name_or("fallback_integration_test");
|
||||||
|
assert!(!name.is_empty());
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user