diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13e3c19b..416a306d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,13 +10,20 @@ env: CARGO_TERM_COLOR: always RUST_BACKTRACE: 1 +permissions: + contents: read + jobs: # === Fast checks first === fmt: name: Format runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@dd44c20b1206a46e25fba8503d5d7c9a33bd355a with: components: rustfmt @@ -28,10 +35,14 @@ jobs: markdown: name: Markdown runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 - - uses: actions/setup-node@v6.4.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6 + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - name: Install dependencies run: pnpm install - name: Lint markdown @@ -40,10 +51,14 @@ jobs: clippy: name: Clippy runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -58,10 +73,14 @@ jobs: check: name: Check runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -77,10 +96,14 @@ jobs: test: name: Test runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -95,10 +118,14 @@ jobs: doc: name: Doc runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -116,8 +143,12 @@ jobs: audit: name: Audit runs-on: ubuntu-latest + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@dd44c20b1206a46e25fba8503d5d7c9a33bd355a - name: Install cargo-audit run: cargo install cargo-audit @@ -129,10 +160,15 @@ jobs: name: Coverage runs-on: ubuntu-latest needs: [test] + permissions: + contents: read + statuses: write steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -146,7 +182,7 @@ jobs: - name: Generate coverage report run: cargo llvm-cov --lcov --output-path lcov.info - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6 with: files: lcov.info fail_ci_if_error: true @@ -158,10 +194,15 @@ jobs: name: Coverage (Frontend) runs-on: ubuntu-latest needs: [test] + permissions: + contents: read + statuses: write steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -171,7 +212,7 @@ jobs: working-directory: apps/rook/dashboard run: pnpm exec vitest run --coverage --reporter=json --output-filename=coverage/coverage-final.json - name: Upload coverage to Codecov - uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 + uses: codecov/codecov-action@e79a6962e0d4c0c17b229090214935d2e33f8354 # v6 with: files: apps/rook/dashboard/coverage/lcov.info fail_ci_if_error: true @@ -184,10 +225,19 @@ jobs: runs-on: ubuntu-latest needs: [test] if: secrets.SONAR_TOKEN != '' + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: 22 + cache: 'pnpm' + - name: Install dependencies + run: pnpm install - name: SonarQube Scan uses: SonarSource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 # v8.1.0 env: @@ -205,10 +255,14 @@ jobs: # aarch64-unknown-linux-gnu is removed: cross-compiling OpenSSL (ring, openssl-sys) # requires target-specific headers which is complex. Windows ARM64 is covered # natively in build-windows job. + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -232,10 +286,14 @@ jobs: target: - x86_64-pc-windows-msvc - aarch64-pc-windows-msvc + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -259,10 +317,14 @@ jobs: target: - x86_64-apple-darwin - aarch64-apple-darwin + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -284,10 +346,14 @@ jobs: fail-fast: false matrix: os: [macos-latest, windows-latest] + permissions: + contents: read steps: - - uses: actions/checkout@v6.0.2 - - uses: pnpm/action-setup@v4.1.0 - - uses: actions/setup-node@v4.2.0 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0 + - uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 with: node-version: 22 cache: 'pnpm' @@ -299,4 +365,4 @@ jobs: - name: Run tests run: cargo test --workspace --all-features - name: Run clippy - run: cargo clippy --workspace --all-targets -- -D warnings + run: cargo clippy --workspace --all-targets -- -D warnings \ No newline at end of file diff --git a/apps/rook/build.rs b/apps/rook/build.rs index 4565f3d0..74d12dc0 100644 --- a/apps/rook/build.rs +++ b/apps/rook/build.rs @@ -2,21 +2,42 @@ use std::path::Path; use std::process::Command; fn main() { - let dashboard_dir = Path::new("dashboard"); - let status = Command::new("sh") - .current_dir(dashboard_dir) - .arg("-c") - .arg("./node_modules/.bin/vite build") - .status() - .expect("failed to run dashboard build: sh or vite not found"); - - if !status.success() { - eprintln!("dashboard build failed with exit code: {}", status); - std::process::exit(1); - } - + // Emit rerun-if-changed unconditionally so Cargo knows when to rebuild println!("cargo:rerun-if-changed=dashboard/dist"); println!("cargo:rerun-if-changed=dashboard/src"); println!("cargo:rerun-if-changed=dashboard/vite.config.ts"); println!("cargo:rerun-if-changed=dashboard/package.json"); + + // Only build dashboard if node_modules/.bin/vite exists (i.e. deps are installed) + // This allows `cargo check` to pass without running the full vite build + let dashboard_dir = Path::new("dashboard"); + let vite_path = dashboard_dir.join("node_modules/.bin/vite"); + + if vite_path.exists() { + let status = Command::new("sh") + .current_dir(dashboard_dir) + .arg("-c") + .arg("./node_modules/.bin/vite build") + .status() + .expect("failed to run dashboard build: sh or vite not found"); + + if !status.success() { + eprintln!("dashboard build failed with exit code: {}", status); + std::process::exit(1); + } + } else { + let profile = std::env::var("PROFILE").unwrap_or_default(); + if profile == "release" { + eprintln!( + "error: dashboard/node_modules/.bin/vite not found in release mode" + ); + eprintln!("hint: run `pnpm install` in the repo root before building release" + ); + std::process::exit(1); + } + println!( + "cargo:warning=dashboard/node_modules/.bin/vite not found, skipping dashboard build" + ); + println!("cargo:warning=hint: run `pnpm install` in the repo root to enable dashboard embedding"); + } } diff --git a/crates/infrastructure/transport-axum/tests/auth_integration_tests.rs b/crates/infrastructure/transport-axum/tests/auth_integration_tests.rs index 28a7dd5c..cad10fe7 100644 --- a/crates/infrastructure/transport-axum/tests/auth_integration_tests.rs +++ b/crates/infrastructure/transport-axum/tests/auth_integration_tests.rs @@ -11,6 +11,25 @@ // - CSRF guard validation (unit tests already in csrf_guard.rs) // - Login rate limiter enforcement +// ============================================================================= +// Test fixture passwords +// +// The following hard-coded strings are TEST DATA ONLY used in unit/integration +// tests. They are arbitrary values used to test password hashing and verification +// flows. These are NOT production passwords, secrets, or cryptographic keys. +// +// The CodeQL rule `rust/hard-coded-cryptographic-value` flags these because +// the static analyzer cannot distinguish between real credentials and test +// fixtures. The constants below are intentionally named and placed here as +// explicit test data. Do not move, obfuscate, or make these dynamic. +// ============================================================================= +// codeql[rust/hard-coded-cryptographic-value] Test fixture only +const TEST_FIXTURE_PASSWORD: &str = "correct-password"; +// codeql[rust/hard-coded-cryptographic-value] Test fixture only +const TEST_FIXTURE_PASSWORD_WRONG: &str = "wrong-password"; +// codeql[rust/hard-coded-cryptographic-value] Test fixture only +const TEST_FIXTURE_PASSWORD_ANY: &str = "any-password"; + use std::sync::Arc; use async_trait::async_trait; @@ -248,14 +267,14 @@ mod login_tests { #[test] fn login_with_valid_credentials_returns_session_token() { runtime().block_on(async { - let (user_repo, hasher) = create_admin_with_password("correct-password"); + let (user_repo, hasher) = create_admin_with_password(TEST_FIXTURE_PASSWORD); let session_repo = Arc::new(FakeSessionRepository::new()); let login = LoginUsecase::new(user_repo, session_repo, hasher); let result = login .execute(LoginInput { username: "admin".to_string(), - password: "correct-password".to_string(), + password: TEST_FIXTURE_PASSWORD.to_string(), }) .await; @@ -269,14 +288,14 @@ mod login_tests { #[test] fn login_with_wrong_password_returns_invalid_credentials() { runtime().block_on(async { - let (user_repo, hasher) = create_admin_with_password("correct-password"); + let (user_repo, hasher) = create_admin_with_password(TEST_FIXTURE_PASSWORD); let session_repo = Arc::new(FakeSessionRepository::new()); let login = LoginUsecase::new(user_repo, session_repo, hasher); let result = login .execute(LoginInput { username: "admin".to_string(), - password: "wrong-password".to_string(), + password: TEST_FIXTURE_PASSWORD_WRONG.to_string(), }) .await; @@ -290,14 +309,14 @@ mod login_tests { #[test] fn login_with_unknown_user_returns_not_found() { runtime().block_on(async { - let (user_repo, hasher) = create_admin_with_password("correct-password"); + let (user_repo, hasher) = create_admin_with_password(TEST_FIXTURE_PASSWORD); let session_repo = Arc::new(FakeSessionRepository::new()); let login = LoginUsecase::new(user_repo, session_repo, hasher); let result = login .execute(LoginInput { username: "unknown".to_string(), - password: "any-password".to_string(), + password: TEST_FIXTURE_PASSWORD_ANY.to_string(), }) .await; @@ -319,7 +338,7 @@ mod login_tests { let result = login .execute(LoginInput { username: "admin".to_string(), - password: "any-password".to_string(), + password: TEST_FIXTURE_PASSWORD_ANY.to_string(), }) .await; @@ -333,14 +352,14 @@ mod login_tests { #[test] fn login_creates_session_in_repository() { runtime().block_on(async { - let (user_repo, hasher) = create_admin_with_password("correct-password"); + let (user_repo, hasher) = create_admin_with_password(TEST_FIXTURE_PASSWORD); let session_repo = Arc::new(FakeSessionRepository::new()); let login = LoginUsecase::new(user_repo, session_repo.clone(), hasher); let result = login .execute(LoginInput { username: "admin".to_string(), - password: "correct-password".to_string(), + password: TEST_FIXTURE_PASSWORD.to_string(), }) .await; @@ -366,14 +385,14 @@ mod login_tests { #[test] fn login_token_is_base64url_encoded_32_bytes() { runtime().block_on(async { - let (user_repo, hasher) = create_admin_with_password("correct-password"); + let (user_repo, hasher) = create_admin_with_password(TEST_FIXTURE_PASSWORD); let session_repo = Arc::new(FakeSessionRepository::new()); let login = LoginUsecase::new(user_repo, session_repo, hasher); let result = login .execute(LoginInput { username: "admin".to_string(), - password: "correct-password".to_string(), + password: TEST_FIXTURE_PASSWORD.to_string(), }) .await; @@ -559,6 +578,11 @@ mod login_rate_limiter_tests { // Argon2id password hashing integration // ============================================================================= +// Test fixture: secure password used in hashing roundtrip tests. +// This is NOT a production credential — it's arbitrary test data. +// codeql[rust/hard-coded-cryptographic-value] Test fixture only +const TEST_FIXTURE_SECURE_PASSWORD: &str = "SecurePass123!"; + #[cfg(test)] mod password_hashing_tests { use super::*; @@ -566,16 +590,17 @@ mod password_hashing_tests { #[test] fn argon2id_hash_and_verify_roundtrip() { let hasher = Argon2idHasher::new(); - let password = "SecurePass123!"; - let hash = hasher.hash_password(password).expect("hash should succeed"); + let hash = hasher + .hash_password(TEST_FIXTURE_SECURE_PASSWORD) + .expect("hash should succeed"); assert!( hash.as_str().starts_with("$argon2id$"), "hash should be Argon2id format" ); let verified = hasher - .verify_password(password, &hash) + .verify_password(TEST_FIXTURE_SECURE_PASSWORD, &hash) .expect("verify should succeed"); assert!(verified, "correct password should verify"); } @@ -583,12 +608,13 @@ mod password_hashing_tests { #[test] fn argon2id_verify_wrong_password_fails() { let hasher = Argon2idHasher::new(); - let password = "SecurePass123!"; - let hash = hasher.hash_password(password).expect("hash should succeed"); + let hash = hasher + .hash_password(TEST_FIXTURE_SECURE_PASSWORD) + .expect("hash should succeed"); let verified = hasher - .verify_password("WrongPassword", &hash) + .verify_password(TEST_FIXTURE_PASSWORD_WRONG, &hash) .expect("verify should succeed"); assert!(!verified, "wrong password should not verify"); } @@ -596,10 +622,13 @@ mod password_hashing_tests { #[test] fn argon2id_different_salts_produce_different_hashes() { let hasher = Argon2idHasher::new(); - let password = "SecurePass123!"; - let hash1 = hasher.hash_password(password).expect("hash should succeed"); - let hash2 = hasher.hash_password(password).expect("hash should succeed"); + let hash1 = hasher + .hash_password(TEST_FIXTURE_SECURE_PASSWORD) + .expect("hash should succeed"); + let hash2 = hasher + .hash_password(TEST_FIXTURE_SECURE_PASSWORD) + .expect("hash should succeed"); assert_ne!( hash1.as_str(), @@ -613,7 +642,7 @@ mod password_hashing_tests { let hasher = Argon2idHasher::new(); let invalid_hash = CorePasswordHash::from("not-a-valid-hash".to_string()); - let result = hasher.verify_password("any-password", &invalid_hash); + let result = hasher.verify_password(TEST_FIXTURE_PASSWORD_ANY, &invalid_hash); assert!(result.is_ok(), "verify should not panic on invalid hash"); assert!(!result.unwrap(), "invalid hash should not verify"); } diff --git a/sonar-project.properties b/sonar-project.properties index 04710fe4..2ba603ad 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -11,7 +11,7 @@ sonar.sources=apps/rook,crates/domain/rook-core,crates/application/rook-usecases sonar.sources+=apps/rook/dashboard/src # Exclude generated and build artifacts -sonar.exclusions=**/target/**,**/*.lock,**/Cargo.lock,**/node_modules/**,**/dist/**,**/coverage/** +sonar.exclusions=**/target/**,**/*.lock,**/Cargo.lock,**/node_modules/**,**/dist/**,**/coverage/**,**/apps/rook/npm/rook/** # Coverage sonar.coverage.jacoco.xmlReportsPaths=lcov.info