security: make CI gates blocking, add smoke test, clean up dead code

- gitleaks, cargo audit, trivy fs now block pipeline on findings
- add smoke test (docker run + curl /health) in release workflow
- deny.toml: add review date to RUSTSEC-2025-0119 ignore
- remove unused validation functions (maven, npm, crate)
- replace blanket #![allow(dead_code)] with targeted allows
This commit is contained in:
2026-03-15 19:25:00 +00:00
parent d886426957
commit 233b83f902
8 changed files with 27 additions and 67 deletions

View File

@@ -51,16 +51,14 @@ jobs:
run: | run: |
curl -sL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \ curl -sL https://github.com/gitleaks/gitleaks/releases/download/v8.21.2/gitleaks_8.21.2_linux_x64.tar.gz \
| tar xz -C /usr/local/bin gitleaks | tar xz -C /usr/local/bin gitleaks
gitleaks detect --source . --exit-code 1 --report-format sarif --report-path gitleaks.sarif || true gitleaks detect --source . --exit-code 1 --report-format sarif --report-path gitleaks.sarif
continue-on-error: true # findings are reported, do not block the pipeline
# ── CVE in Rust dependencies ──────────────────────────────────────────── # ── CVE in Rust dependencies ────────────────────────────────────────────
- name: Install cargo-audit - name: Install cargo-audit
run: cargo install cargo-audit --locked run: cargo install cargo-audit --locked
- name: cargo audit — RustSec advisory database - name: cargo audit — RustSec advisory database
run: cargo audit run: cargo audit --ignore RUSTSEC-2025-0119 # known: number_prefix via indicatif
continue-on-error: true # warn only; known CVEs should not block CI until triaged
# ── Licenses, banned crates, supply chain policy ──────────────────────── # ── Licenses, banned crates, supply chain policy ────────────────────────
- name: cargo deny — licenses and banned crates - name: cargo deny — licenses and banned crates
@@ -79,7 +77,7 @@ jobs:
format: sarif format: sarif
output: trivy-fs.sarif output: trivy-fs.sarif
severity: HIGH,CRITICAL severity: HIGH,CRITICAL
exit-code: 0 # warn only; change to 1 to block the pipeline exit-code: 1 # block pipeline on HIGH/CRITICAL vulnerabilities
- name: Upload Trivy fs results to GitHub Security tab - name: Upload Trivy fs results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4 uses: github/codeql-action/upload-sarif@v4

View File

@@ -127,6 +127,17 @@ jobs:
cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra cache-from: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra
cache-to: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra,mode=max cache-to: type=registry,ref=${{ env.NORA }}/${{ env.IMAGE_NAME }}-cache:astra,mode=max
# ── Smoke test ──────────────────────────────────────────────────────────
- name: Smoke test — verify alpine image starts and responds
run: |
docker run --rm -d --name nora-smoke -p 5555:5000 \
${{ env.NORA }}/${{ env.IMAGE_NAME }}:latest
for i in $(seq 1 10); do
curl -sf http://localhost:5555/health && break || sleep 2
done
curl -sf http://localhost:5555/health
docker stop nora-smoke
scan: scan:
name: Scan (${{ matrix.name }}) name: Scan (${{ matrix.name }})
runs-on: [self-hosted, nora] runs-on: [self-hosted, nora]

View File

@@ -5,7 +5,7 @@
# Vulnerability database (RustSec) # Vulnerability database (RustSec)
db-urls = ["https://github.com/rustsec/advisory-db"] db-urls = ["https://github.com/rustsec/advisory-db"]
ignore = [ ignore = [
"RUSTSEC-2025-0119", # number_prefix unmaintained, transitive via indicatif; no fix available "RUSTSEC-2025-0119", # number_prefix unmaintained via indicatif; no fix available. Review by 2026-06-15
] ]
[licenses] [licenses]

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2026 Volkov Pavel | DevITWay // Copyright (c) 2026 Volkov Pavel | DevITWay
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#![allow(dead_code)]
//! Application error handling with HTTP response conversion //! Application error handling with HTTP response conversion
//! //!
//! Provides a unified error type that can be converted to HTTP responses //! Provides a unified error type that can be converted to HTTP responses
@@ -18,6 +17,7 @@ use thiserror::Error;
use crate::storage::StorageError; use crate::storage::StorageError;
use crate::validation::ValidationError; use crate::validation::ValidationError;
#[allow(dead_code)] // Wiring into handlers planned for v0.3
/// Application-level errors with HTTP response conversion /// Application-level errors with HTTP response conversion
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum AppError { pub enum AppError {
@@ -40,6 +40,7 @@ pub enum AppError {
Validation(#[from] ValidationError), Validation(#[from] ValidationError),
} }
#[allow(dead_code)]
/// JSON error response body /// JSON error response body
#[derive(Serialize)] #[derive(Serialize)]
struct ErrorResponse { struct ErrorResponse {
@@ -74,6 +75,7 @@ impl IntoResponse for AppError {
} }
} }
#[allow(dead_code)]
impl AppError { impl AppError {
/// Create a not found error /// Create a not found error
pub fn not_found(msg: impl Into<String>) -> Self { pub fn not_found(msg: impl Into<String>) -> Self {

View File

@@ -5,7 +5,8 @@
//! //!
//! Functions in this module are stubs used only for generating OpenAPI documentation. //! Functions in this module are stubs used only for generating OpenAPI documentation.
#![allow(dead_code)]
#![allow(dead_code)] // utoipa doc stubs — not called at runtime, used by derive macros
use axum::Router; use axum::Router;
use std::sync::Arc; use std::sync::Arc;

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2026 Volkov Pavel | DevITWay // Copyright (c) 2026 Volkov Pavel | DevITWay
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#![allow(dead_code)] // Foundational code for future S3/Vault integration
//! Secrets management for NORA //! Secrets management for NORA
//! //!
@@ -34,6 +33,7 @@ use async_trait::async_trait;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error; use thiserror::Error;
#[allow(dead_code)] // Variants used by provider impls; external error handling planned for v0.4
/// Secrets provider error /// Secrets provider error
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum SecretsError { pub enum SecretsError {
@@ -56,9 +56,11 @@ pub enum SecretsError {
#[async_trait] #[async_trait]
pub trait SecretsProvider: Send + Sync { pub trait SecretsProvider: Send + Sync {
/// Get a secret by key (required) /// Get a secret by key (required)
#[allow(dead_code)]
async fn get_secret(&self, key: &str) -> Result<ProtectedString, SecretsError>; async fn get_secret(&self, key: &str) -> Result<ProtectedString, SecretsError>;
/// Get a secret by key (optional, returns None if not found) /// Get a secret by key (optional, returns None if not found)
#[allow(dead_code)]
async fn get_secret_optional(&self, key: &str) -> Option<ProtectedString> { async fn get_secret_optional(&self, key: &str) -> Option<ProtectedString> {
self.get_secret(key).await.ok() self.get_secret(key).await.ok()
} }

View File

@@ -13,12 +13,14 @@ use zeroize::{Zeroize, Zeroizing};
/// - Implements Zeroize: memory is overwritten with zeros when dropped /// - Implements Zeroize: memory is overwritten with zeros when dropped
/// - Debug shows `***REDACTED***` instead of actual value /// - Debug shows `***REDACTED***` instead of actual value
/// - Clone creates a new protected copy /// - Clone creates a new protected copy
#[allow(dead_code)] // Used internally by SecretsProvider impls; external callers planned for v0.4
#[derive(Clone, Zeroize)] #[derive(Clone, Zeroize)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct ProtectedString { pub struct ProtectedString {
inner: String, inner: String,
} }
#[allow(dead_code)]
impl ProtectedString { impl ProtectedString {
/// Create a new protected string /// Create a new protected string
pub fn new(value: String) -> Self { pub fn new(value: String) -> Self {
@@ -68,6 +70,7 @@ impl From<&str> for ProtectedString {
} }
/// S3 credentials with protected secrets /// S3 credentials with protected secrets
#[allow(dead_code)] // S3 storage backend planned for v0.4
#[derive(Clone, Zeroize)] #[derive(Clone, Zeroize)]
#[zeroize(drop)] #[zeroize(drop)]
pub struct S3Credentials { pub struct S3Credentials {
@@ -77,6 +80,7 @@ pub struct S3Credentials {
pub region: Option<String>, pub region: Option<String>,
} }
#[allow(dead_code)]
impl S3Credentials { impl S3Credentials {
pub fn new(access_key_id: String, secret_access_key: String) -> Self { pub fn new(access_key_id: String, secret_access_key: String) -> Self {
Self { Self {

View File

@@ -1,7 +1,6 @@
// Copyright (c) 2026 Volkov Pavel | DevITWay // Copyright (c) 2026 Volkov Pavel | DevITWay
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
#![allow(dead_code)]
//! Input validation for artifact registry paths and identifiers //! Input validation for artifact registry paths and identifiers
//! //!
//! Provides security validation to prevent path traversal attacks and //! Provides security validation to prevent path traversal attacks and
@@ -309,63 +308,6 @@ pub fn validate_docker_reference(reference: &str) -> Result<(), ValidationError>
Ok(()) Ok(())
} }
/// Validate Maven artifact path.
///
/// Maven paths follow the pattern: groupId/artifactId/version/filename
/// Example: `org/apache/commons/commons-lang3/3.12.0/commons-lang3-3.12.0.jar`
pub fn validate_maven_path(path: &str) -> Result<(), ValidationError> {
validate_storage_key(path)
}
/// Validate npm package name.
pub fn validate_npm_name(name: &str) -> Result<(), ValidationError> {
if name.is_empty() {
return Err(ValidationError::EmptyInput);
}
if name.len() > 214 {
return Err(ValidationError::TooLong {
max: 214,
actual: name.len(),
});
}
// Check for path traversal
if name.contains("..") {
return Err(ValidationError::PathTraversal);
}
Ok(())
}
/// Validate Cargo crate name.
pub fn validate_crate_name(name: &str) -> Result<(), ValidationError> {
if name.is_empty() {
return Err(ValidationError::EmptyInput);
}
if name.len() > 64 {
return Err(ValidationError::TooLong {
max: 64,
actual: name.len(),
});
}
// Check for path traversal
if name.contains("..") || name.contains('/') {
return Err(ValidationError::PathTraversal);
}
// Crate names: alphanumeric, underscores, hyphens
for c in name.chars() {
if !matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-') {
return Err(ValidationError::ForbiddenCharacter(c));
}
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;