///|
/// Optional local review policy. Defaults are deliberately permissive.
pub(all) struct AuditPolicy {
mut min_ttl : Int?
mut max_ttl : Int?
mut min_apex_ns : Int
mut require_ipv6_for_mail : Bool
forbid_types : Array[String]
} derive(Debug)
///|
pub(all) struct PolicyParse {
policy : AuditPolicy
diagnostics : Array[Diagnostic]
} derive(Debug)
///|
pub fn default_policy() -> AuditPolicy {
{
min_ttl: None,
max_ttl: None,
min_apex_ns: 1,
require_ipv6_for_mail: false,
forbid_types: [],
}
}
///|
fn policy_error(
findings : Array[Diagnostic],
code : String,
message : String,
line : Int,
) -> Unit {
findings.push(diagnostic(code, "error", message, line, 1))
}
///|
fn parse_policy_integer(
text : String,
line : Int,
diagnostics : Array[Diagnostic],
) -> Int? {
match decimal_number(text) {
Some(value) => Some(value)
None => {
policy_error(diagnostics, "P001", "expected a nonnegative integer", line)
None
}
}
}
///|
fn parse_policy_boolean(
text : String,
line : Int,
diagnostics : Array[Diagnostic],
) -> Bool? {
match lower(text) {
"true" | "yes" => Some(true)
"false" | "no" => Some(false)
_ => {
policy_error(diagnostics, "P002", "expected true/false or yes/no", line)
None
}
}
}
///|
/// Parse a small, reviewable policy format: one `key value` pair per line.
/// Blank lines and lines beginning with `#` are ignored.
pub fn parse_policy(source : String) -> PolicyParse {
let policy = default_policy()
let diagnostics : Array[Diagnostic] = []
let lines = source.split("\n").collect()
for index in 0..
policy.min_ttl = parse_policy_integer(words[1], index + 1, diagnostics)
"max-ttl" =>
policy.max_ttl = parse_policy_integer(words[1], index + 1, diagnostics)
"min-apex-ns" =>
match parse_policy_integer(words[1], index + 1, diagnostics) {
Some(value) => policy.min_apex_ns = value
None => ()
}
"require-ipv6-for-mail" =>
match parse_policy_boolean(words[1], index + 1, diagnostics) {
Some(value) => policy.require_ipv6_for_mail = value
None => ()
}
"forbid-type" => policy.forbid_types.push(upper(words[1]))
_ =>
policy_error(
diagnostics,
"P004",
"unknown policy key: \{words[0]}",
index + 1,
)
}
}
match (policy.min_ttl, policy.max_ttl) {
(Some(minimum), Some(maximum)) =>
if minimum > maximum {
policy_error(diagnostics, "P005", "min-ttl exceeds max-ttl", 1)
}
_ => ()
}
{ policy, diagnostics, }
}
///|
fn policy_ttl_checks(
zone : Zone,
policy : AuditPolicy,
findings : Array[Diagnostic],
) -> Unit {
for record in zone.records {
match policy.min_ttl {
Some(minimum) =>
if record.ttl < minimum {
add_record_note(
findings,
"P100",
"warning",
"TTL \{record.ttl} is below policy minimum \{minimum}",
record,
)
}
None => ()
}
match policy.max_ttl {
Some(maximum) =>
if record.ttl > maximum {
add_record_note(
findings,
"P101",
"warning",
"TTL \{record.ttl} exceeds policy maximum \{maximum}",
record,
)
}
None => ()
}
}
}
///|
fn policy_ns_checks(
zone : Zone,
policy : AuditPolicy,
findings : Array[Diagnostic],
) -> Unit {
let mut count = 0
for record in zone.records {
if record.owner == zone.origin && record.record_type == "NS" {
count += 1
}
}
if count < policy.min_apex_ns {
findings.push(
diagnostic(
"P102",
"warning",
"apex has \{count} NS records; policy requires \{policy.min_apex_ns}",
1,
1,
owner=zone.origin,
),
)
}
}
///|
fn policy_mail_checks(zone : Zone, findings : Array[Diagnostic]) -> Unit {
let ipv6_owners : Map[String, Bool] = Map([])
for record in zone.records {
if record.record_type == "AAAA" {
ipv6_owners.set(record.owner, true)
}
}
for record in zone.records {
if record.record_type != "MX" || record.rdata.length() != 2 {
continue
}
let target = absolute_rdata_name(record, 1, zone.origin)
if is_within_zone(target, zone.origin) && ipv6_owners.get(target) is None {
add_record_note(
findings,
"P103",
"warning",
"in-zone mail exchanger has no AAAA record: \{target}",
record,
)
}
}
}
///|
fn policy_type_checks(
zone : Zone,
policy : AuditPolicy,
findings : Array[Diagnostic],
) -> Unit {
for record in zone.records {
for forbidden in policy.forbid_types {
if record.record_type == forbidden {
add_record_note(
findings,
"P104",
"error",
"record type \{forbidden} is forbidden by policy",
record,
)
}
}
}
}
///|
/// Apply optional team policy after the standard DNS validation.
pub fn validate_with_policy(
zone : Zone,
policy : AuditPolicy,
) -> Array[Diagnostic] {
let findings = validate_zone(zone)
policy_ttl_checks(zone, policy, findings)
policy_ns_checks(zone, policy, findings)
if policy.require_ipv6_for_mail {
policy_mail_checks(zone, findings)
}
policy_type_checks(zone, policy, findings)
findings
}