Skip to content
Merged

duo #38

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions .github/workflows/rust-guardrails.yml

This file was deleted.

5 changes: 3 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ coverage.xml
local_settings.py
db.sqlite3
db.sqlite3-journal

.copilot-memory
# Flask stuff:
instance/
.webassets-cache
Expand Down Expand Up @@ -152,9 +152,10 @@ criterion/

# Documentation
/doc/

vulnera-sast/tests/snapshots/*
# Cache directories
/.vulnera_data
curls.txt
# Decoupled components
/vulnera-cli/
docs/modules.md
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ GOOGLE_AI_KEY='your-api-key' # for Google AI

# Sandbox (secure module execution)
VULNERA__SANDBOX__ENABLED=true
VULNERA__SANDBOX__BACKEND='auto' # landlock, process, or auto
VULNERA__SANDBOX__BACKEND='landlock' # landlock, auto, process, noop
VULNERA__SANDBOX__FAILURE_MODE='best_effort' # best_effort or fail_closed

# Optional
VULNERA__CACHE__DRAGONFLY_URL='redis://127.0.0.1:6379'
Expand Down
19 changes: 14 additions & 5 deletions docs/src/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,20 +91,28 @@ The sandbox provides secure isolation for SAST and secrets detection modules.
| Variable | Description | Default |
|----------|-------------|---------|
| `VULNERA__SANDBOX__ENABLED` | Enable sandboxing | `true` |
| `VULNERA__SANDBOX__BACKEND` | Sandbox backend (see below) | `auto` |
| `VULNERA__SANDBOX__BACKEND` | Sandbox backend (see below) | `landlock` |
| `VULNERA__SANDBOX__FAILURE_MODE` | Sandbox setup behavior | `best_effort` |
| `VULNERA__SANDBOX__EXECUTION_TIMEOUT_SECS` | Execution timeout | `30` |
| `VULNERA__SANDBOX__MEMORY_LIMIT_MB` | Memory limit (process backend) | `256` |

### Sandbox Backends

| Backend | Description | Requirements |
|---------|-------------|--------------|
| `auto` | Auto-detect best backend | Recommended |
| `landlock` | Kernel-level isolation | Linux 5.13+ |
| `auto` | Auto-detect best backend | Linux/non-Linux |
| `process` | Fork-based isolation | Any Linux |
| `none` | Disable sandboxing | Not recommended |
| `noop` | Disable sandboxing | Not recommended |

**Landlock** provides near-zero overhead security using Linux kernel capabilities. Falls back to **process** on older kernels.
**Landlock** provides near-zero overhead security using Linux kernel capabilities.

Failure modes:

| Mode | Behavior |
|------|----------|
| `best_effort` | Continue analysis if sandbox setup degrades |
| `fail_closed` | Abort module execution if sandbox setup fails |

---

Expand Down Expand Up @@ -141,7 +149,8 @@ VULNERA__LLM__RESILIENCE__ENABLED=true

# Sandbox
VULNERA__SANDBOX__ENABLED=true
VULNERA__SANDBOX__BACKEND='auto'
VULNERA__SANDBOX__BACKEND='landlock'
VULNERA__SANDBOX__FAILURE_MODE='best_effort'

# Server
VULNERA__SERVER__ENABLE_DOCS=false
Expand Down
3 changes: 2 additions & 1 deletion vulnera-api/src/domain/value_objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub enum ApiVulnerabilityType {
IneffectiveScopeHierarchy,
}

/// OpenAPI specification model (simplified)
/// OpenAPI specification model for analyzer pipelines
#[derive(Debug, Clone)]
pub struct OpenApiSpec {
pub version: String,
Expand Down Expand Up @@ -149,6 +149,7 @@ pub struct ApiSchema {
pub multiple_of: Option<f64>, // Number must be multiple of this
pub min_items: Option<u32>, // Minimum array items
pub max_items: Option<u32>, // Maximum array items
pub items: Option<Box<ApiSchema>>, // Array item schema

// Logical constraints (composition)
pub one_of: Vec<ApiSchema>,
Expand Down
58 changes: 26 additions & 32 deletions vulnera-api/src/infrastructure/analyzers/data_exposure_analyzer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@

use crate::domain::entities::{ApiFinding, ApiLocation, FindingSeverity};
use crate::domain::value_objects::{ApiVulnerabilityType, OpenApiSpec, ParameterLocation};
use tracing::error;
use regex::Regex;
use std::sync::OnceLock;

/// Analyzer for sensitive data exposure
pub struct DataExposureAnalyzer;

fn jwt_pattern() -> &'static Regex {
static JWT_PATTERN: OnceLock<Regex> = OnceLock::new();
JWT_PATTERN.get_or_init(|| {
Regex::new(r"eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+")
.expect("JWT regex pattern must be valid")
})
}

fn private_key_pattern() -> &'static Regex {
static PRIVATE_KEY_PATTERN: OnceLock<Regex> = OnceLock::new();
PRIVATE_KEY_PATTERN.get_or_init(|| {
Regex::new(r"-----BEGIN [A-Z]+ PRIVATE KEY-----")
.expect("private key regex pattern must be valid")
})
}

impl DataExposureAnalyzer {
pub fn analyze(spec: &OpenApiSpec) -> Vec<ApiFinding> {
let mut findings = Vec::new();
Expand All @@ -21,31 +38,8 @@ impl DataExposureAnalyzer {
"refresh_token",
];

// Compile regexes for secret detection
// Note: Using lazy_static here would be better performance-wise if this analyzer is instantiated often,
// but for now local compilation is fine or better yet, move to lazy_static in module scope if possible.
// Given existing structure, we'll compile locally or use a static block if we refactor.
let jwt_pattern =
match regex::Regex::new(r"eyJ[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+\.[a-zA-Z0-9-_]+") {
Ok(pattern) => pattern,
Err(error) => {
error!(
"Failed to compile JWT regex in data exposure analyzer: {}",
error
);
return findings;
}
};
let private_key_pattern = match regex::Regex::new(r"-----BEGIN [A-Z]+ PRIVATE KEY-----") {
Ok(pattern) => pattern,
Err(error) => {
error!(
"Failed to compile private key regex in data exposure analyzer: {}",
error
);
return findings;
}
};
let jwt_pattern = jwt_pattern();
let private_key_pattern = private_key_pattern();

for path in &spec.paths {
for operation in &path.operations {
Expand Down Expand Up @@ -114,8 +108,8 @@ impl DataExposureAnalyzer {
&operation.method,
&format!("param:{}", param.name),
&mut findings,
&jwt_pattern,
&private_key_pattern,
jwt_pattern,
private_key_pattern,
);
}
}
Expand All @@ -130,8 +124,8 @@ impl DataExposureAnalyzer {
&operation.method,
"request_body",
&mut findings,
&jwt_pattern,
&private_key_pattern,
jwt_pattern,
private_key_pattern,
);
}
}
Expand All @@ -147,8 +141,8 @@ impl DataExposureAnalyzer {
&operation.method,
&format!("response:{}", response.status_code),
&mut findings,
&jwt_pattern,
&private_key_pattern,
jwt_pattern,
private_key_pattern,
);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,9 @@ impl InputValidationAnalyzer {
// Check for mass assignment (objects allowing additional properties)
if schema.schema_type.as_deref() == Some("object")
&& schema.additional_properties == AdditionalProperties::Allowed
&& (method == "POST" || method == "PUT" || method == "PATCH")
{
findings.push(ApiFinding {
&& (method == "POST" || method == "PUT" || method == "PATCH")
{
findings.push(ApiFinding {
id: format!("mass-assignment-{}-{}-{}", path, method, context),
vulnerability_type: ApiVulnerabilityType::MassAssignmentRisk,
location: ApiLocation {
Expand All @@ -201,7 +201,7 @@ impl InputValidationAnalyzer {
path: Some(path.to_string()),
method: Some(method.to_string()),
});
}
}

// Recurse into properties
for prop in &schema.properties {
Expand Down Expand Up @@ -237,10 +237,16 @@ impl InputValidationAnalyzer {
method: Some(method.to_string()),
});
}
// NOTE: Recursive analysis for array items logic would need item schema extraction,
// but ApiSchema doesn't have 'items' field in value_objects.rs yet (omitted in initial implementation plan?)
// Checked value_objects.rs: 'items' is missing!
// I should add it later, but for now properties recursion is good for objects.

if let Some(item_schema) = &schema.items {
Self::analyze_schema(
item_schema,
path,
method,
&format!("{}[]", context),
findings,
);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,34 @@ use crate::domain::value_objects::{ApiVulnerabilityType, OpenApiSpec};
pub struct ResourceRestrictionAnalyzer;

impl ResourceRestrictionAnalyzer {
fn schema_contains_array(schema: &crate::domain::value_objects::ApiSchema) -> bool {
if schema.schema_type.as_deref() == Some("array") {
return true;
}

if schema
.properties
.iter()
.any(|prop| Self::schema_contains_array(&prop.schema))
{
return true;
}

if schema.one_of.iter().any(Self::schema_contains_array)
|| schema.any_of.iter().any(Self::schema_contains_array)
|| schema.all_of.iter().any(Self::schema_contains_array)
{
return true;
}

match &schema.additional_properties {
crate::domain::value_objects::AdditionalProperties::Schema(nested) => {
Self::schema_contains_array(nested)
}
_ => false,
}
}

pub fn analyze(spec: &OpenApiSpec) -> Vec<ApiFinding> {
let mut findings = Vec::new();

Expand All @@ -24,20 +52,11 @@ impl ResourceRestrictionAnalyzer {
if response.status_code.starts_with('2') {
for content in &response.content {
if let Some(schema) = &content.schema {
if schema.schema_type.as_deref() == Some("array") {
if Self::schema_contains_array(schema) {
returns_array = true;
}
// or object wrapping array (e.g. { data: [...] }) - simplified check for now
// Check properties for array
for prop in &schema.properties {
if prop.schema.schema_type.as_deref() == Some("array") {
returns_array = true;
}
}
}
}

// Check for rate limit headers in 2xx or 429 response
// Check for rate limit headers in 2xx or 429 response
if response
.headers
Expand Down
Loading