mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 09:10:32 +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: |
|
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
|
||||||
|
|||||||
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-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]
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user