use crate::error::RegentError;
use crate::hosts::managed_host::InternalApiCallOutcome;
use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
use crate::hosts::properties::{HostProperties, OsKind};
use crate::secrets::SecretProvidersPool;
use crate::state::Check;
use crate::state::attribute::HostHandler;
use crate::state::attribute::Privilege;
use crate::state::compliance::AttributeComplianceAssessment;
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
pub enum DnsRecordType {
A(IpAddr),
Aaaa(IpAddr),
Cname(String),
}
impl DnsRecordType {
fn dig_arg_equivalent(&self) -> &'static str {
match self {
DnsRecordType::A(_) => "A",
DnsRecordType::Aaaa(_) => "AAAA",
DnsRecordType::Cname(_) => "CNAME",
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
#[serde(rename_all = "PascalCase")]
pub enum DnsExpectedState {
#[serde(rename_all = "PascalCase")]
CheckResponseAndServer {
dns_name: String,
expected_response: DnsRecordType,
server: IpAddr,
},
#[serde(rename_all = "PascalCase")]
CheckResponse {
dns_name: String,
expected_response: DnsRecordType,
},
#[serde(rename_all = "PascalCase")]
CheckServer { dns_name: String, server: IpAddr },
#[serde(rename_all = "PascalCase")]
SimpleCheck { dns_name: String },
}
impl DnsExpectedState {
pub fn check_response_and_server(
dns_name: &str,
expected_response: DnsRecordType,
server: IpAddr,
) -> DnsExpectedState {
DnsExpectedState::CheckResponseAndServer {
dns_name: dns_name.to_string(),
expected_response,
server,
}
}
pub fn check_response(dns_name: &str, expected_response: DnsRecordType) -> DnsExpectedState {
DnsExpectedState::CheckResponse {
dns_name: dns_name.to_string(),
expected_response,
}
}
pub fn check_server(dns_name: &str, server: IpAddr) -> DnsExpectedState {
DnsExpectedState::CheckServer {
dns_name: dns_name.to_string(),
server,
}
}
pub fn simple_check(dns_name: &str) -> DnsExpectedState {
DnsExpectedState::SimpleCheck {
dns_name: dns_name.to_string(),
}
}
pub fn check_a_record(dns_name: &str, ip: Ipv4Addr) -> DnsExpectedState {
Self::check_response(dns_name, DnsRecordType::A(IpAddr::V4(ip)))
}
pub fn check_aaaa_record(dns_name: &str, ip: Ipv6Addr) -> DnsExpectedState {
Self::check_response(dns_name, DnsRecordType::Aaaa(IpAddr::V6(ip)))
}
pub fn check_cname_record(dns_name: &str, target: &str) -> DnsExpectedState {
Self::check_response(dns_name, DnsRecordType::Cname(target.to_string()))
}
pub fn via_server(self, server: IpAddr) -> DnsExpectedState {
match self {
DnsExpectedState::CheckResponseAndServer {
dns_name,
expected_response,
server: _,
}
| DnsExpectedState::CheckResponse {
dns_name,
expected_response,
} => DnsExpectedState::CheckResponseAndServer {
dns_name,
expected_response,
server,
},
DnsExpectedState::CheckServer {
dns_name,
server: _,
}
| DnsExpectedState::SimpleCheck { dns_name } => {
DnsExpectedState::CheckServer { dns_name, server }
}
}
}
}
impl Check for DnsExpectedState {
fn check(&self) -> Result<(), RegentError> {
let (dns_name, expected_response) = match &self {
DnsExpectedState::CheckResponseAndServer {
dns_name,
expected_response,
server: _,
} => (dns_name, Some(expected_response)),
DnsExpectedState::CheckResponse {
dns_name,
expected_response,
} => (dns_name, Some(expected_response)),
DnsExpectedState::CheckServer {
dns_name,
server: _,
} => (dns_name, None),
DnsExpectedState::SimpleCheck { dns_name } => (dns_name, None),
};
if dns_name.trim().is_empty() {
return Err(RegentError::IncoherentExpectedState(
"DnsName is empty".to_string(),
));
}
if !is_valid_dns_name(dns_name) {
return Err(RegentError::IncoherentExpectedState(format!(
"DnsName '{dns_name}' is not a valid DNS name"
)));
}
if let Some(expected_response) = expected_response {
match expected_response {
DnsRecordType::A(ip) => {
if !ip.is_ipv4() {
return Err(RegentError::IncoherentExpectedState(format!(
"A record expects an IPv4 address but got {ip}"
)));
}
}
DnsRecordType::Aaaa(ip) => {
if !ip.is_ipv6() {
return Err(RegentError::IncoherentExpectedState(format!(
"AAAA record expects an IPv6 address but got {ip}"
)));
}
}
DnsRecordType::Cname(target) => {
if target.trim().is_empty() {
return Err(RegentError::IncoherentExpectedState(
"CNAME response is empty".to_string(),
));
}
if !is_valid_dns_name(target) {
return Err(RegentError::IncoherentExpectedState(format!(
"CNAME response '{target}' is not a valid DNS name"
)));
}
}
}
}
Ok(())
}
fn check_host_compatibility(
&self,
host_properties: &HostProperties,
) -> Result<(), RegentError> {
match host_properties.os_kind() {
OsKind::Linux(_) => Ok(()),
incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
"Host is {:?} but DNS resolution is only supported on Linux",
incompatible_os_kind
))),
}
}
}
impl Timeout for DnsExpectedState {
fn default_timeout(&self) -> Duration {
Duration::from_secs(5)
}
}
impl<Handler: HostHandler> AssessCompliance<Handler> for DnsExpectedState {
async fn assess_compliance(
&self,
host_handler: &mut Handler,
host_properties: &Option<HostProperties>,
_privilege: &Privilege,
_optional_secret_provider: &Option<SecretProvidersPool>,
) -> Result<AttributeComplianceAssessment, RegentError> {
if let Some(properties) = host_properties {
self.check_host_compatibility(properties)?;
}
if let Err(details) = host_handler
.is_this_command_available("dig", &Privilege::None)
.await
{
return Err(RegentError::FailedDryRunEvaluation(format!(
"command dig no available on this host : {:?}",
details
)));
}
let (dns_name, server, expected_response) = match &self {
DnsExpectedState::CheckResponseAndServer {
dns_name,
expected_response,
server,
} => (dns_name, Some(*server), Some(expected_response)),
DnsExpectedState::CheckResponse {
dns_name,
expected_response,
} => (dns_name, None, Some(expected_response)),
DnsExpectedState::CheckServer { dns_name, server } => (dns_name, Some(*server), None),
DnsExpectedState::SimpleCheck { dns_name } => (dns_name, None, None),
};
let record_type = expected_response.map(|response| response.dig_arg_equivalent());
let dns_name = normalize_name(dns_name);
let command_result = match host_handler
.run_command(
&final_dns_query(&dns_name, server, record_type),
&Privilege::None,
)
.await
{
Ok(command_result) => command_result,
Err(details) => {
return Err(RegentError::FailedDryRunEvaluation(format!(
"Unable to run dig command: {:?}",
details
)));
}
};
if command_result.return_code != 0 {
return Err(RegentError::FailedDryRunEvaluation(format!(
"Failed dig command: {:?}",
command_result
)));
}
let responses: Vec<&str> = command_result
.stdout
.lines()
.map(|line| line.trim())
.filter(|line| !line.is_empty())
.collect();
if responses.is_empty() {
return Ok(AttributeComplianceAssessment::NonCompliantFatal(
"Name doesn't resolve".to_string(),
));
}
match expected_response {
None => {
Ok(AttributeComplianceAssessment::Compliant)
}
Some(expected_response_details) => match expected_response_details {
DnsRecordType::A(expected) => {
let parsed: Vec<IpAddr> =
responses.iter().filter_map(|l| l.parse().ok()).collect();
if parsed.contains(expected) {
Ok(AttributeComplianceAssessment::Compliant)
} else {
Ok(AttributeComplianceAssessment::NonCompliantFatal(format!(
"Name resolves but expected A record {expected} not found among results ({:?})",
responses
)))
}
}
DnsRecordType::Aaaa(expected) => {
let parsed: Vec<IpAddr> =
responses.iter().filter_map(|l| l.parse().ok()).collect();
if parsed.contains(expected) {
Ok(AttributeComplianceAssessment::Compliant)
} else {
Ok(AttributeComplianceAssessment::NonCompliantFatal(format!(
"Name resolves but expected AAAA record {expected} not found among results ({:?})",
responses
)))
}
}
DnsRecordType::Cname(expected) => {
let expected_normalized_version = normalize_name(expected);
if responses
.iter()
.any(|line| normalize_name(line) == expected_normalized_version)
{
Ok(AttributeComplianceAssessment::Compliant)
} else {
Ok(AttributeComplianceAssessment::NonCompliantFatal(format!(
"Name resolves but expected CNAME record {expected} not found among results ({:?})",
responses
)))
}
}
},
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DnsApiCall {}
impl DnsApiCall {
pub fn display(&self) -> String {
"Should not have been called".to_string()
}
}
impl Check for DnsApiCall {
fn check(&self) -> Result<(), RegentError> {
Err(RegentError::InternalLogicError(
"(check) DnsApiCall should not have been called as we cannot remediate automatically a wrong DNS configuration".to_string()
))
}
fn check_host_compatibility(
&self,
_host_properties: &HostProperties,
) -> Result<(), RegentError> {
Err(RegentError::InternalLogicError(
"(check_host_compatibility) DnsApiCall should not have been called as we cannot remediate automatically a wrong DNS configuration".to_string()
))
}
}
impl<Handler: HostHandler> ReachCompliance<Handler> for DnsApiCall {
async fn call(
&self,
_host_handler: &mut Handler,
_host_properties: &Option<HostProperties>,
_optional_secret_provider: &Option<SecretProvidersPool>,
) -> Result<InternalApiCallOutcome, RegentError> {
Err(RegentError::InternalLogicError(
"(call) DnsApiCall should not have been called as we cannot remediate automatically a wrong DNS configuration".to_string()
))
}
}
fn final_dns_query(dns_name: &str, server: Option<IpAddr>, record_type: Option<&str>) -> String {
let record_type_arg = match record_type {
Some(record) => format!(" {record}"),
None => String::new(),
};
match server {
Some(server_address) => {
format!("dig @{server_address} +short {dns_name}{record_type_arg}")
}
None => {
format!("dig +short {dns_name}{record_type_arg}")
}
}
}
fn normalize_name(name: &str) -> String {
name.trim().trim_end_matches('.').to_ascii_lowercase()
}
fn is_valid_dns_name(name: &str) -> bool {
let name = name.trim_end_matches('.');
!name.is_empty()
&& name.len() <= 253
&& !name.contains("..")
&& name.chars().all(|character| {
character.is_ascii_alphanumeric()
|| character == '-'
|| character == '_'
|| character == '.'
|| character == '*'
})
}