use crate::policy::NetworkPolicy;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EgressDecision {
pub allowed: bool,
pub reason: String,
}
pub fn check_network_egress(host: &str, policy: Option<&NetworkPolicy>) -> EgressDecision {
match policy {
None => EgressDecision {
allowed: true,
reason: "no network policy configured".into(),
},
Some(np) if np.allowlist.is_empty() => EgressDecision {
allowed: true,
reason: "network allowlist empty (no restriction)".into(),
},
Some(np) => {
if aa_core::policy::is_host_allowed_by_egress_allowlist(host, &np.allowlist) {
EgressDecision {
allowed: true,
reason: format!("host matches network allowlist ({} pattern(s))", np.allowlist.len()),
}
} else {
EgressDecision {
allowed: false,
reason: format!("host not in network allowlist ({} pattern(s))", np.allowlist.len()),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn list(patterns: &[&str]) -> NetworkPolicy {
NetworkPolicy {
allowlist: patterns.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn none_policy_allows_any_host() {
let d = check_network_egress("api.openai.com", None);
assert!(d.allowed);
assert_eq!(d.reason, "no network policy configured");
}
#[test]
fn empty_allowlist_allows_any_host() {
let np = list(&[]);
let d = check_network_egress("evil.attacker.net", Some(&np));
assert!(d.allowed);
assert!(d.reason.contains("empty"));
}
#[test]
fn matching_host_allowed_with_count_in_reason() {
let np = list(&["api.openai.com", "*.anthropic.com"]);
let d = check_network_egress("api.openai.com", Some(&np));
assert!(d.allowed);
assert!(d.reason.contains("2 pattern"));
}
#[test]
fn non_matching_host_denied_with_count_in_reason() {
let np = list(&["api.openai.com"]);
let d = check_network_egress("evil.attacker.net", Some(&np));
assert!(!d.allowed);
assert!(d.reason.contains("not in network allowlist"));
assert!(d.reason.contains("1 pattern"));
}
#[test]
fn wildcard_subdomain_allowed() {
let np = list(&["*.openai.com"]);
let d = check_network_egress("chat.openai.com", Some(&np));
assert!(d.allowed);
}
}