diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 994d7aa..a464bc0 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,10 +1,6 @@ { - "image": "mcr.microsoft.com/devcontainers/universal:2", + "image": "mcr.microsoft.com/devcontainers/rust:1-bookworm", "features": { - "ghcr.io/devcontainers/features/rust:1": { - "version": "latest", - "profile": "default" - }, "ghcr.io/devcontainers-contrib/features/vault-asdf:2": { "version": "latest" } diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..c26c011 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,159 @@ +name: Security + +on: + pull_request: + push: + branches: ["main"] + schedule: + - cron: "0 9 * * 1" # weekly Monday 09:00 UTC + +permissions: + contents: read + +jobs: + rust-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: fmt (fail if changed) + run: cargo fmt --all -- --check + + - name: clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: test + run: cargo test --all --all-features + + dependency-audit: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-audit + uses: taiki-e/install-action@v2 + with: + tool: cargo-audit + + - name: RustSec audit + run: cargo audit + + dependency-policy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-deny + uses: taiki-e/install-action@v2 + with: + tool: cargo-deny + + - name: cargo-deny (licenses, bans, sources, advisories) + run: cargo deny check + + sbom: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Rust (stable) + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Install cargo-cyclonedx + uses: taiki-e/install-action@v2 + with: + tool: cargo-cyclonedx + + - name: Generate CycloneDX SBOM + run: | + mkdir -p artifacts + cargo cyclonedx --format json > artifacts/sbom.cdx.json + cargo cyclonedx --format xml > artifacts/sbom.cdx.xml + + - name: Upload SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom + path: artifacts/ + + secrets: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Run Gitleaks + uses: gitleaks/gitleaks-action@v2 + env: + GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "true" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + trivy-fs: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Trivy filesystem scan + uses: aquasecurity/trivy-action@0.28.0 + with: + scan-type: fs + scan-ref: . + format: sarif + output: trivy-fs.sarif + ignore-unfixed: true + + - name: Upload Trivy SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: trivy-fs.sarif + + security-status: + runs-on: ubuntu-latest + needs: [rust-checks, dependency-audit, dependency-policy, sbom, secrets, trivy-fs] + if: always() + steps: + - name: Check all jobs status + run: | + if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then + echo "One or more security checks failed" + exit 1 + else + echo "All security checks passed" + fi \ No newline at end of file diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..ee8eb55 --- /dev/null +++ b/deny.toml @@ -0,0 +1,52 @@ +# cargo-deny configuration +# See: https://embarkstudios.github.io/cargo-deny/ + +[advisories] +version = 2 +# Don't warn on unmaintained packages yet +ignore = [ + "RUSTSEC-2025-0134", # rustls-pemfile is unmaintained, comes from aws-sdk dependencies +] + +[licenses] +# Accept common permissive licenses +allow = [ + "MIT", + "Apache-2.0", + "BSD-3-Clause", + "BSD-2-Clause", + "ISC", + "Unicode-DFS-2016", +] +confidence-threshold = 0.8 +exceptions = [] + +[licenses.private] +# Ignore private crates +ignore = false + +[bans] +# Check for duplicate dependencies +multiple-versions = "warn" +# Wildcards not allowed in dependencies +wildcards = "deny" +# Highlight deprecated crates +highlight = "all" +workspace-default-features = "allow" +external-default-features = "allow" + +# List specific crates to deny (e.g., known security issues) +deny = [] + +# Skip certain crates from duplicate checking +skip = [] +skip-tree = [] + +[sources] +# Ensure all dependencies come from trusted sources +unknown-registry = "deny" +unknown-git = "deny" + +[sources.allow-org] +# Allow crates from GitHub orgs +github = [] diff --git a/src/backends/mod.rs b/src/backends/mod.rs index e4f8cd5..9060901 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -8,11 +8,11 @@ mod secret_backend; mod vault; #[allow(unused_imports)] // Used in tests -pub use aws_secrets::{AwsSecretsClient, create_test_client}; +pub use aws_secrets::{create_test_client, AwsSecretsClient}; pub use file::FileBackend; pub use secret_backend::SecretBackend; #[allow(unused_imports)] // Used in tests -pub use vault::{VaultBackend, VaultClient, SecretMetadata, VaultSecretData, VaultWriteRequest}; +pub use vault::{SecretMetadata, VaultBackend, VaultClient, VaultSecretData, VaultWriteRequest}; /// Backend type enumeration #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/tests/api_target_tests.rs b/tests/api_target_tests.rs index 46004e4..9bb9c3d 100644 --- a/tests/api_target_tests.rs +++ b/tests/api_target_tests.rs @@ -1,5 +1,5 @@ -use secret_rotator::targets::ApiTarget; use secret_rotator::config::ApiTargetConfig; +use secret_rotator::targets::ApiTarget; #[test] fn test_build_url_with_placeholder() { diff --git a/tests/aws_secrets_tests.rs b/tests/aws_secrets_tests.rs index 84f08d4..3679692 100644 --- a/tests/aws_secrets_tests.rs +++ b/tests/aws_secrets_tests.rs @@ -1,5 +1,5 @@ -use secret_rotator::backends::{AwsSecretsClient, create_test_client}; use aws_sdk_secretsmanager::types::Tag; +use secret_rotator::backends::{create_test_client, AwsSecretsClient}; use std::collections::HashMap; #[test] @@ -94,4 +94,3 @@ fn test_metadata_to_tags_empty() { let tags = client.metadata_to_tags(&metadata); assert!(tags.is_empty()); } - diff --git a/tests/env_updater_tests.rs b/tests/env_updater_tests.rs index cbc52be..b2bf092 100644 --- a/tests/env_updater_tests.rs +++ b/tests/env_updater_tests.rs @@ -1,5 +1,5 @@ -use secret_rotator::env_updater::EnvUpdater; use anyhow::Result; +use secret_rotator::env_updater::EnvUpdater; use std::fs; use tempfile::TempDir; diff --git a/tests/file_backend_tests.rs b/tests/file_backend_tests.rs index a623adf..3439501 100644 --- a/tests/file_backend_tests.rs +++ b/tests/file_backend_tests.rs @@ -1,5 +1,5 @@ -use secret_rotator::backends::{FileBackend, SecretBackend}; use anyhow::Result; +use secret_rotator::backends::{FileBackend, SecretBackend}; use std::collections::HashMap; use tempfile::TempDir; diff --git a/tests/rotation_tests.rs b/tests/rotation_tests.rs index 8f8f872..c177d8d 100644 --- a/tests/rotation_tests.rs +++ b/tests/rotation_tests.rs @@ -1,6 +1,6 @@ +use chrono::{Duration, Utc}; use secret_rotator::rotation::{generate_secret, needs_rotation}; use std::collections::HashMap; -use chrono::{Duration, Utc}; #[test] fn test_generate_secret() { diff --git a/tests/vault_tests.rs b/tests/vault_tests.rs index 93b5819..1e73938 100644 --- a/tests/vault_tests.rs +++ b/tests/vault_tests.rs @@ -1,4 +1,4 @@ -use secret_rotator::backends::{VaultClient, SecretMetadata, VaultSecretData, VaultWriteRequest}; +use secret_rotator::backends::{SecretMetadata, VaultClient, VaultSecretData, VaultWriteRequest}; use std::collections::HashMap; #[test]