mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 06:50:31 +00:00
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:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -51,16 +51,14 @@ jobs:
|
||||
run: |
|
||||
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
|
||||
gitleaks detect --source . --exit-code 1 --report-format sarif --report-path gitleaks.sarif || true
|
||||
continue-on-error: true # findings are reported, do not block the pipeline
|
||||
gitleaks detect --source . --exit-code 1 --report-format sarif --report-path gitleaks.sarif
|
||||
|
||||
# ── CVE in Rust dependencies ────────────────────────────────────────────
|
||||
- name: Install cargo-audit
|
||||
run: cargo install cargo-audit --locked
|
||||
|
||||
- name: cargo audit — RustSec advisory database
|
||||
run: cargo audit
|
||||
continue-on-error: true # warn only; known CVEs should not block CI until triaged
|
||||
run: cargo audit --ignore RUSTSEC-2025-0119 # known: number_prefix via indicatif
|
||||
|
||||
# ── Licenses, banned crates, supply chain policy ────────────────────────
|
||||
- name: cargo deny — licenses and banned crates
|
||||
@@ -79,7 +77,7 @@ jobs:
|
||||
format: sarif
|
||||
output: trivy-fs.sarif
|
||||
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
|
||||
uses: github/codeql-action/upload-sarif@v4
|
||||
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -127,6 +127,17 @@ jobs:
|
||||
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
|
||||
|
||||
# ── 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:
|
||||
name: Scan (${{ matrix.name }})
|
||||
runs-on: [self-hosted, nora]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
# Vulnerability database (RustSec)
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
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]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(dead_code)]
|
||||
//! Application error handling with HTTP response conversion
|
||||
//!
|
||||
//! 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::validation::ValidationError;
|
||||
|
||||
#[allow(dead_code)] // Wiring into handlers planned for v0.3
|
||||
/// Application-level errors with HTTP response conversion
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
@@ -40,6 +40,7 @@ pub enum AppError {
|
||||
Validation(#[from] ValidationError),
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// JSON error response body
|
||||
#[derive(Serialize)]
|
||||
struct ErrorResponse {
|
||||
@@ -74,6 +75,7 @@ impl IntoResponse for AppError {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl AppError {
|
||||
/// Create a not found error
|
||||
pub fn not_found(msg: impl Into<String>) -> Self {
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
//!
|
||||
//! 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 std::sync::Arc;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(dead_code)] // Foundational code for future S3/Vault integration
|
||||
|
||||
//! Secrets management for NORA
|
||||
//!
|
||||
@@ -34,6 +33,7 @@ use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[allow(dead_code)] // Variants used by provider impls; external error handling planned for v0.4
|
||||
/// Secrets provider error
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SecretsError {
|
||||
@@ -56,9 +56,11 @@ pub enum SecretsError {
|
||||
#[async_trait]
|
||||
pub trait SecretsProvider: Send + Sync {
|
||||
/// Get a secret by key (required)
|
||||
#[allow(dead_code)]
|
||||
async fn get_secret(&self, key: &str) -> Result<ProtectedString, SecretsError>;
|
||||
|
||||
/// Get a secret by key (optional, returns None if not found)
|
||||
#[allow(dead_code)]
|
||||
async fn get_secret_optional(&self, key: &str) -> Option<ProtectedString> {
|
||||
self.get_secret(key).await.ok()
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ use zeroize::{Zeroize, Zeroizing};
|
||||
/// - Implements Zeroize: memory is overwritten with zeros when dropped
|
||||
/// - Debug shows `***REDACTED***` instead of actual value
|
||||
/// - Clone creates a new protected copy
|
||||
#[allow(dead_code)] // Used internally by SecretsProvider impls; external callers planned for v0.4
|
||||
#[derive(Clone, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct ProtectedString {
|
||||
inner: String,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl ProtectedString {
|
||||
/// Create a new protected string
|
||||
pub fn new(value: String) -> Self {
|
||||
@@ -68,6 +70,7 @@ impl From<&str> for ProtectedString {
|
||||
}
|
||||
|
||||
/// S3 credentials with protected secrets
|
||||
#[allow(dead_code)] // S3 storage backend planned for v0.4
|
||||
#[derive(Clone, Zeroize)]
|
||||
#[zeroize(drop)]
|
||||
pub struct S3Credentials {
|
||||
@@ -77,6 +80,7 @@ pub struct S3Credentials {
|
||||
pub region: Option<String>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl S3Credentials {
|
||||
pub fn new(access_key_id: String, secret_access_key: String) -> Self {
|
||||
Self {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#![allow(dead_code)]
|
||||
//! Input validation for artifact registry paths and identifiers
|
||||
//!
|
||||
//! Provides security validation to prevent path traversal attacks and
|
||||
@@ -309,63 +308,6 @@ pub fn validate_docker_reference(reference: &str) -> Result<(), ValidationError>
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user