use std::net::SocketAddr;
use openmoat_core::{EvalContext, RepoPolicy};
use super::*;
use crate::decide::{address_policy, named_on};
fn ctx() -> EvalContext {
EvalContext {
home: "/home/u".to_owned(),
project: None,
real_home: None,
real_project: None,
moved_dirs: Vec::new(),
cwd: "/home/u".to_owned(),
case_insensitive_paths: false,
}
}
fn decide(policy: &Policy, host: &str, port: u16) -> (Verdict, String) {
let compiled = CompiledPolicy::compile(policy, &ctx()).unwrap();
let port_policy = port_policy(policy);
let ports = CompiledPolicy::compile(&port_policy, &ctx()).unwrap();
let d = host_port(&compiled, &ports, host, port);
(d.verdict, d.rules[0].clone())
}
const POLICY: &str = "version: 1\ndefaults: { '*': deny, fetch: ask }\n\
deny:\n - id: no-ssh\n net: ['*:22', 'blocked.example.com']\n\
allow:\n - id: web\n net: [example.com, '*.cdn.test', '!evil.cdn.test', 'fe80::1']\n\
\x20 - id: alt\n net: ['alt.example.com:8443', '*.svc.test:9000', '!bad.svc.test:9000']\n\
\x20 - id: everywhere\n fetch: ['*']\n";
#[test]
fn a_host_rule_without_a_port_allows_443_and_80_only() {
let policy = Policy::parse(POLICY).unwrap();
let allow = |rule: &str| (Verdict::Allow, rule.to_owned());
assert_eq!(decide(&policy, "example.com", 443), allow("web"));
assert_eq!(decide(&policy, "example.com", 80), allow("web"));
assert_eq!(decide(&policy, "a.cdn.test", 443), allow("web"));
for port in [22, 8443, 25, 6379] {
let (verdict, rule) = decide(&policy, "example.com", port);
assert_eq!(verdict, Verdict::Deny, "{port}");
assert!(
rule == RULE_PORT || (port == 22 && rule == "no-ssh"),
"{port}: {rule}"
);
}
assert_eq!(decide(&policy, "any.test", 443), allow("everywhere"));
assert_eq!(
decide(&policy, "any.test", 8443),
(Verdict::Deny, RULE_PORT.to_owned())
);
assert_eq!(decide(&policy, "fe80::1", 443).0, Verdict::Allow);
assert_eq!(
decide(&policy, "fe80::1", 8443),
(Verdict::Deny, RULE_PORT.to_owned())
);
}
#[test]
fn a_host_port_rule_allows_that_port_and_no_other() {
let policy = Policy::parse(POLICY).unwrap();
assert_eq!(
decide(&policy, "alt.example.com", 8443),
(Verdict::Allow, "alt".to_owned())
);
assert_eq!(
decide(&policy, "alt.example.com", 443),
(Verdict::Allow, "everywhere".to_owned())
);
assert_eq!(decide(&policy, "alt.example.com", 8444).0, Verdict::Deny);
assert_eq!(decide(&policy, "x.svc.test", 9000).0, Verdict::Allow);
assert_eq!(decide(&policy, "x.svc.test", 9001).0, Verdict::Deny);
assert_eq!(decide(&policy, "bad.svc.test", 9000).0, Verdict::Deny);
}
#[test]
fn denies_and_exclusions_hold_on_every_port() {
let policy = Policy::parse(POLICY).unwrap();
assert_eq!(
decide(&policy, "example.com", 22),
(Verdict::Deny, "no-ssh".to_owned())
);
assert_eq!(
decide(&policy, "blocked.example.com", 443),
(Verdict::Deny, "no-ssh".to_owned())
);
let excluded = "version: 1\ndefaults: { '*': deny }\nallow:\n - id: cdn\n \
net: ['*.cdn.test', '*.cdn.test:8443', '!evil.cdn.test']\n";
let excluded = Policy::parse(excluded).unwrap();
assert_eq!(decide(&excluded, "ok.cdn.test", 8443).0, Verdict::Allow);
assert_eq!(decide(&excluded, "evil.cdn.test", 443).0, Verdict::Deny);
assert_eq!(decide(&excluded, "evil.cdn.test", 8443).0, Verdict::Deny);
let deny_all_but = "version: 1\ndefaults: { '*': deny }\n\
deny:\n - id: only-github\n net: ['*', '!github.com']\n\
allow:\n - id: gh\n net: [github.com]\n";
let deny_all_but = Policy::parse(deny_all_but).unwrap();
assert_eq!(decide(&deny_all_but, "github.com", 443).0, Verdict::Allow);
assert_eq!(
decide(&deny_all_but, "github.com", 22),
(Verdict::Deny, RULE_PORT.to_owned())
);
assert_eq!(decide(&deny_all_but, "evil.test", 443).0, Verdict::Deny);
}
#[test]
fn cloud_metadata_is_refused_whatever_a_port_rule_says() {
let policy = Policy::parse(
"version: 1\nallow:\n - id: wide\n net: ['*:80', 'metadata.google.internal:80']\n",
)
.unwrap();
assert_eq!(
decide(&policy, "metadata.google.internal", 80),
(Verdict::Deny, crate::decide::RULE_ADDRESS.to_owned())
);
}
#[test]
fn a_private_address_named_with_a_port_is_reachable_on_that_port_only() {
let policy = Policy::parse(
"version: 1\nallow:\n - id: db\n net: ['10.0.0.5:5432', '10.0.1.*', '!10.0.1.9']\n",
)
.unwrap();
let addresses = address_policy(&policy);
let addresses = CompiledPolicy::compile(&addresses, &ctx()).unwrap();
let named = |a: &str| named_on(&addresses, a.parse::<SocketAddr>().unwrap());
assert!(named("10.0.0.5:5432"));
assert!(!named("10.0.0.5:5433"));
assert!(named("10.0.1.7:6379"));
assert!(!named("10.0.1.9:443"));
assert!(!named("10.0.1.9:6379"));
}
#[test]
fn a_repository_deny_holds_at_the_proxy_and_its_allow_does_not() {
let user = Policy::parse(
"version: 1\ndefaults: { '*': deny }\nallow:\n - id: web\n net: [example.com, a.test]\n",
)
.unwrap();
let repo = RepoPolicy::parse(
"version: 1\ndeny:\n - id: no-a\n net: [a.test]\nallow:\n - id: more\n \
net: [b.test, 'b.test:8443']\n",
)
.unwrap();
let merged = repo.merge_deny(&user).unwrap();
assert_eq!(
decide(&merged, "a.test", 443),
(Verdict::Deny, "repo:no-a".to_owned())
);
assert_eq!(decide(&merged, "b.test", 443).0, Verdict::Deny);
assert_eq!(decide(&merged, "b.test", 8443).0, Verdict::Deny);
assert_eq!(decide(&merged, "example.com", 443).0, Verdict::Allow);
assert_eq!(decide(&user, "a.test", 443).0, Verdict::Allow);
}
#[test]
fn a_host_port_allow_is_no_host_glob() {
let policy = Policy::parse(
"version: 1\ndefaults: { '*': deny, fetch: ask }\n\
allow:\n - id: alt\n net: ['*:8443', '*:443']\n",
)
.unwrap();
for port in [443, 80] {
assert_eq!(
decide(&policy, "2001:db8::8443", port).0,
Verdict::Deny,
"{port}"
);
assert_eq!(
decide(&policy, "2001:db8::443", port).0,
Verdict::Deny,
"{port}"
);
}
assert_eq!(
decide(&policy, "example.com", 8443),
(Verdict::Allow, "alt".to_owned())
);
let compiled = CompiledPolicy::compile(&policy, &ctx()).unwrap();
let hook = compiled.decide(&openmoat_core::Action::Net {
url: "https://[2001:db8::8443]/".to_owned(),
});
assert_ne!(hook.verdict, Verdict::Allow, "{hook:?}");
}