///|
pub(all) enum ZoneReferenceKind {
NameServerReference
CanonicalNameReference
MailExchangeReference
ServiceTargetReference
SoaPrimaryReference
} derive(Eq, Debug)
///|
struct ReferenceCandidate {
kind_value : ZoneReferenceKind
target_value : DomainName
requires_address_value : Bool
disabled_value : Bool
} derive(Eq, Debug)
///|
pub struct ZoneReference {
source_owner_value : DomainName
kind_value : ZoneReferenceKind
target_value : DomainName
internal_value : Bool
target_declared_value : Bool
address_available_value : Bool
requires_address_value : Bool
disabled_value : Bool
} derive(Eq, Debug)
///|
pub struct ZoneReferenceAnalysis {
reference_values : Array[ZoneReference]
internal_count_value : Int
external_count_value : Int
missing_target_count_value : Int
addressless_target_count_value : Int
disabled_count_value : Int
} derive(Eq, Debug)
///|
fn reference_error(message : String) -> ZoneError {
ZoneError::new(IntegrityViolation, message, SourceSpan::point(0, 0))
}
///|
fn reference_kind_text(kind : ZoneReferenceKind) -> String {
match kind {
NameServerReference => "ns_target"
CanonicalNameReference => "cname_target"
MailExchangeReference => "mx_exchange"
ServiceTargetReference => "srv_target"
SoaPrimaryReference => "soa_primary"
}
}
///|
fn root_target(target : DomainName) -> Bool {
target.is_absolute() && target.labels().length() == 0
}
///|
fn reference_candidate(record : ZoneRecord) -> ReferenceCandidate? {
match record.data() {
NsData(target) =>
Some({
kind_value: NameServerReference,
target_value: target,
requires_address_value: true,
disabled_value: false,
})
CnameData(target) =>
Some({
kind_value: CanonicalNameReference,
target_value: target,
requires_address_value: false,
disabled_value: false,
})
MxData(value) =>
Some({
kind_value: MailExchangeReference,
target_value: value.exchange(),
requires_address_value: true,
disabled_value: false,
})
SrvData(value) => {
let target = value.target()
Some({
kind_value: ServiceTargetReference,
target_value: target,
requires_address_value: !root_target(target),
disabled_value: root_target(target),
})
}
SoaData(value) =>
Some({
kind_value: SoaPrimaryReference,
target_value: value.primary_name(),
requires_address_value: true,
disabled_value: false,
})
_ => None
}
}
///|
fn reference_owner_declared(
records : Array[ZoneRecord],
target : DomainName,
) -> Bool {
for record in records {
if record.owner() == target {
return true
}
}
false
}
///|
fn reference_address_available(
records : Array[ZoneRecord],
target : DomainName,
) -> Bool {
for record in records {
if record.owner() == target {
match record.data() {
AData(_) | AaaaData(_) => return true
_ => ()
}
}
}
false
}
///|
fn build_zone_reference(
record : ZoneRecord,
candidate : ReferenceCandidate,
records : Array[ZoneRecord],
origin : DomainName,
) -> ZoneReference {
if candidate.disabled_value {
return {
source_owner_value: record.owner(),
kind_value: candidate.kind_value,
target_value: candidate.target_value,
internal_value: false,
target_declared_value: false,
address_available_value: false,
requires_address_value: false,
disabled_value: true,
}
}
let internal = domain_is_at_or_below(candidate.target_value, origin)
let declared = internal &&
reference_owner_declared(records, candidate.target_value)
let address = internal &&
reference_address_available(records, candidate.target_value)
{
source_owner_value: record.owner(),
kind_value: candidate.kind_value,
target_value: candidate.target_value,
internal_value: internal,
target_declared_value: declared,
address_available_value: address,
requires_address_value: candidate.requires_address_value,
disabled_value: false,
}
}
///|
/// Analyze domain-name references without treating external targets as errors.
pub fn analyze_zone_references(
zone : ZoneDocument,
max_references? : Int = 100000,
) -> Result[ZoneReferenceAnalysis, ZoneError] {
if max_references < 0 {
return Err(reference_error("maximum reference count cannot be negative"))
}
let records = zone.records()
let origin = match zone.origin() {
Some(value) => value
None =>
return Err(
reference_error("reference analysis requires an absolute zone origin"),
)
}
let references : Array[ZoneReference] = []
let mut internal_count = 0
let mut external_count = 0
let mut missing_count = 0
let mut addressless_count = 0
let mut disabled_count = 0
for record in records {
match reference_candidate(record) {
None => ()
Some(candidate) => {
if references.length() >= max_references {
return Err(
reference_error(
"zone exceeds the configured reference evidence limit",
),
)
}
let reference = build_zone_reference(record, candidate, records, origin)
references.push(reference)
if reference.disabled() {
disabled_count = disabled_count + 1
} else if reference.internal() {
internal_count = internal_count + 1
if !reference.target_declared() {
missing_count = missing_count + 1
} else if reference.requires_address() &&
!reference.address_available() {
addressless_count = addressless_count + 1
}
} else {
external_count = external_count + 1
}
}
}
}
Ok({
reference_values: references,
internal_count_value: internal_count,
external_count_value: external_count,
missing_target_count_value: missing_count,
addressless_target_count_value: addressless_count,
disabled_count_value: disabled_count,
})
}
///|
pub fn ZoneReference::source_owner(self : ZoneReference) -> DomainName {
self.source_owner_value
}
///|
pub fn ZoneReference::kind(self : ZoneReference) -> ZoneReferenceKind {
self.kind_value
}
///|
pub fn ZoneReference::target(self : ZoneReference) -> DomainName {
self.target_value
}
///|
pub fn ZoneReference::internal(self : ZoneReference) -> Bool {
self.internal_value
}
///|
pub fn ZoneReference::target_declared(self : ZoneReference) -> Bool {
self.target_declared_value
}
///|
pub fn ZoneReference::address_available(self : ZoneReference) -> Bool {
self.address_available_value
}
///|
pub fn ZoneReference::requires_address(self : ZoneReference) -> Bool {
self.requires_address_value
}
///|
pub fn ZoneReference::disabled(self : ZoneReference) -> Bool {
self.disabled_value
}
///|
pub fn ZoneReference::healthy(self : ZoneReference) -> Bool {
self.disabled_value ||
!self.internal_value ||
(
self.target_declared_value &&
(!self.requires_address_value || self.address_available_value)
)
}
///|
pub fn ZoneReferenceAnalysis::references(
self : ZoneReferenceAnalysis,
) -> Array[ZoneReference] {
self.reference_values.copy()
}
///|
pub fn ZoneReferenceAnalysis::internal_count(
self : ZoneReferenceAnalysis,
) -> Int {
self.internal_count_value
}
///|
pub fn ZoneReferenceAnalysis::external_count(
self : ZoneReferenceAnalysis,
) -> Int {
self.external_count_value
}
///|
pub fn ZoneReferenceAnalysis::missing_target_count(
self : ZoneReferenceAnalysis,
) -> Int {
self.missing_target_count_value
}
///|
pub fn ZoneReferenceAnalysis::addressless_target_count(
self : ZoneReferenceAnalysis,
) -> Int {
self.addressless_target_count_value
}
///|
pub fn ZoneReferenceAnalysis::disabled_count(
self : ZoneReferenceAnalysis,
) -> Int {
self.disabled_count_value
}
///|
pub fn ZoneReferenceAnalysis::healthy(self : ZoneReferenceAnalysis) -> Bool {
self.missing_target_count_value == 0 &&
self.addressless_target_count_value == 0
}
///|
pub fn render_reference_analysis_text(
analysis : ZoneReferenceAnalysis,
) -> String {
let mut result = "DNS zone reference analysis" +
"\nhealthy: " +
analysis.healthy().to_string() +
"\ninternal: " +
analysis.internal_count().to_string() +
"\nexternal: " +
analysis.external_count().to_string() +
"\nmissing targets: " +
analysis.missing_target_count().to_string() +
"\naddressless targets: " +
analysis.addressless_target_count().to_string() +
"\ndisabled services: " +
analysis.disabled_count().to_string()
for reference in analysis.references() {
result = result +
"\n[" +
reference_kind_text(reference.kind()) +
"] " +
reference.source_owner().to_text() +
" -> " +
reference.target().to_text() +
" healthy=" +
reference.healthy().to_string()
}
result
}
///|
pub fn render_reference_analysis_json(
analysis : ZoneReferenceAnalysis,
) -> String {
let references : Array[Json] = []
for reference in analysis.references() {
references.push(
Json::object({
"kind": Json::string(reference_kind_text(reference.kind())),
"source_owner": Json::string(reference.source_owner().to_text()),
"target": Json::string(reference.target().to_text()),
"internal": Json::boolean(reference.internal()),
"target_declared": Json::boolean(reference.target_declared()),
"requires_address": Json::boolean(reference.requires_address()),
"address_available": Json::boolean(reference.address_available()),
"disabled": Json::boolean(reference.disabled()),
"healthy": Json::boolean(reference.healthy()),
}),
)
}
Json::object({
"type": Json::string("dns_zone_reference_analysis"),
"healthy": Json::boolean(analysis.healthy()),
"internal_count": Json::number(analysis.internal_count().to_double()),
"external_count": Json::number(analysis.external_count().to_double()),
"missing_target_count": Json::number(
analysis.missing_target_count().to_double(),
),
"addressless_target_count": Json::number(
analysis.addressless_target_count().to_double(),
),
"disabled_count": Json::number(analysis.disabled_count().to_double()),
"references": Json::array(references),
}).stringify()
}