use crate::ir::{ArgumentSource, ScanTarget};
use crate::rules::{
AttackCategory, Confidence, Detector, Evidence, Finding, RuleMetadata, Severity,
};
pub struct ArbitraryFileAccessDetector;
impl Detector for ArbitraryFileAccessDetector {
fn metadata(&self) -> RuleMetadata {
RuleMetadata {
id: "SHIELD-004".into(),
name: "Arbitrary File Access".into(),
description: "File read/write with path from untrusted input".into(),
default_severity: Severity::High,
attack_category: AttackCategory::ArbitraryFileAccess,
cwe_id: Some("CWE-22".into()),
}
}
fn run(&self, target: &ScanTarget) -> Vec<Finding> {
let mut findings = Vec::new();
for file_op in &target.execution.file_operations {
if file_op.path_arg.is_tainted() {
let source_desc = match &file_op.path_arg {
ArgumentSource::Parameter { name } => format!("parameter '{name}'"),
ArgumentSource::Interpolated => "interpolated string".into(),
ArgumentSource::Unknown => "unknown source".into(),
ArgumentSource::EnvVar { name } => format!("env var '{name}'"),
ArgumentSource::Literal(_) | ArgumentSource::Sanitized { .. } => continue,
};
let confidence = match &file_op.path_arg {
ArgumentSource::Parameter { .. } => Confidence::High,
_ => Confidence::Medium,
};
findings.push(Finding {
rule_id: "SHIELD-004".into(),
rule_name: "Arbitrary File Access".into(),
severity: Severity::High,
confidence,
attack_category: AttackCategory::ArbitraryFileAccess,
message: format!(
"File {:?} with path from {}",
file_op.operation, source_desc
),
location: Some(file_op.location.clone()),
evidence: vec![Evidence {
description: format!("{} flows into file path", source_desc),
location: Some(file_op.location.clone()),
snippet: None,
}],
taint_path: None,
remediation: Some(
"Canonicalize paths and validate against an allowlist of \
permitted directories. Reject paths with '..' traversal."
.into(),
),
cwe_id: Some("CWE-22".into()),
});
}
}
findings
}
}