use super::code::CodeScanner;
use super::data::DataScanner;
use super::secrets::SecretsScanner;
use super::types::{ExternalizationPolicy, ScanResult, ScanVerdict, Severity};
use crate::lifecycle::ExecResult;
pub struct ExternalizationGate {
secrets: SecretsScanner,
code: CodeScanner,
data: DataScanner,
}
impl ExternalizationGate {
pub fn new() -> Self {
Self {
secrets: SecretsScanner::new(),
code: CodeScanner::new(),
data: DataScanner::new(),
}
}
pub fn apply(
&self,
mut result: ExecResult,
policy: &ExternalizationPolicy,
) -> crate::Result<ExecResult> {
if !policy.enabled {
return Ok(result);
}
let total_size = result.stdout.len() + result.stderr.len();
if total_size > policy.max_artifact_size_bytes {
return Err(crate::KavachError::ExternalizationBlocked(format!(
"output size {} exceeds limit {}",
total_size, policy.max_artifact_size_bytes
)));
}
let combined = format!("{}\n{}", result.stdout, result.stderr);
let mut all_findings = Vec::new();
all_findings.extend(self.secrets.scan(&combined));
all_findings.extend(self.code.scan(&combined));
all_findings.extend(self.data.scan(&combined));
let worst_severity = all_findings
.iter()
.map(|f| f.severity)
.max()
.unwrap_or(Severity::Info);
let scan_result = ScanResult {
verdict: determine_verdict(worst_severity, policy),
findings: all_findings,
worst_severity,
};
match scan_result.verdict {
ScanVerdict::Block => Err(crate::KavachError::ExternalizationBlocked(format!(
"blocked: {} finding(s), worst severity: {}",
scan_result.findings.len(),
scan_result.worst_severity
))),
ScanVerdict::Quarantine => Err(crate::KavachError::ExternalizationBlocked(format!(
"quarantined: {} finding(s), worst severity: {}",
scan_result.findings.len(),
scan_result.worst_severity
))),
ScanVerdict::Warn => {
if policy.redact_secrets {
result.stdout = self.secrets.redact(&result.stdout).into_owned();
result.stderr = self.secrets.redact(&result.stderr).into_owned();
}
Ok(result)
}
ScanVerdict::Pass => Ok(result),
}
}
}
impl Default for ExternalizationGate {
fn default() -> Self {
Self::new()
}
}
fn determine_verdict(worst: Severity, policy: &ExternalizationPolicy) -> ScanVerdict {
if worst >= policy.block_threshold {
ScanVerdict::Block
} else if worst >= policy.quarantine_threshold {
ScanVerdict::Quarantine
} else if worst > Severity::Info {
ScanVerdict::Warn
} else {
ScanVerdict::Pass
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_result(stdout: &str) -> ExecResult {
ExecResult {
exit_code: 0,
stdout: stdout.into(),
stderr: String::new(),
duration_ms: 0,
timed_out: false,
}
}
#[test]
fn pass_clean_output() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy::default();
let result = gate.apply(make_result("hello world"), &policy).unwrap();
assert_eq!(result.stdout, "hello world");
}
#[test]
fn block_private_key() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy::default();
let result = gate.apply(
make_result("-----BEGIN RSA PRIVATE KEY-----\nMIIEp..."),
&policy,
);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("blocked"));
}
#[test]
fn redact_medium_severity() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy {
quarantine_threshold: Severity::High,
block_threshold: Severity::Critical,
..Default::default()
};
let result = gate
.apply(
make_result(r#"config: api_key = "abcdefghijklmnopqrstuvwxyz""#),
&policy,
)
.unwrap();
assert!(result.stdout.contains("[REDACTED:"));
}
#[test]
fn block_oversized() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy {
max_artifact_size_bytes: 10,
..Default::default()
};
let result = gate.apply(make_result("this is longer than 10 bytes"), &policy);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("size"));
}
#[test]
fn disabled_gate_passes_everything() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy {
enabled: false,
..Default::default()
};
let result = gate
.apply(make_result("-----BEGIN RSA PRIVATE KEY-----"), &policy)
.unwrap();
assert!(result.stdout.contains("BEGIN RSA PRIVATE KEY"));
}
#[test]
fn default_gate() {
let gate = ExternalizationGate::default();
let policy = ExternalizationPolicy::default();
let result = gate.apply(make_result("hello"), &policy).unwrap();
assert_eq!(result.stdout, "hello");
}
#[test]
fn quarantine_medium_severity() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy {
quarantine_threshold: Severity::Medium,
block_threshold: Severity::Critical,
..Default::default()
};
let result = gate.apply(
make_result(r#"api_key = "abcdefghijklmnopqrstuvwxyz""#),
&policy,
);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("quarantined"));
}
#[test]
fn stderr_scanning() {
let gate = ExternalizationGate::new();
let policy = ExternalizationPolicy::default();
let mut result = make_result("clean");
result.stderr = "-----BEGIN RSA PRIVATE KEY-----".into();
let outcome = gate.apply(result, &policy);
assert!(outcome.is_err(), "secret in stderr should be caught");
}
#[test]
fn verdict_determination() {
let policy = ExternalizationPolicy::default();
assert_eq!(
determine_verdict(Severity::Info, &policy),
ScanVerdict::Pass
);
assert_eq!(determine_verdict(Severity::Low, &policy), ScanVerdict::Warn);
assert_eq!(
determine_verdict(Severity::High, &policy),
ScanVerdict::Quarantine
);
assert_eq!(
determine_verdict(Severity::Critical, &policy),
ScanVerdict::Block
);
}
}