From f324a4e86420ae64dfe27d47d59f380eafb42d3a Mon Sep 17 00:00:00 2001 From: Nawaz Dhandala Date: Fri, 13 Feb 2026 13:42:45 +0000 Subject: [PATCH] feat: Implement DNS monitoring configuration and secret handling --- APIReference/Service/DataTypeDetail.ts | 126 ++++++++++++++++++ Common/Server/Services/MonitorService.ts | 8 ++ .../Utils/Monitor/MonitorTemplateUtil.ts | 37 +++++ Common/Utils/Monitor/MonitorMetricType.ts | 3 +- ProbeIngest/Utils/Monitor.ts | 39 ++++++ 5 files changed, 212 insertions(+), 1 deletion(-) diff --git a/APIReference/Service/DataTypeDetail.ts b/APIReference/Service/DataTypeDetail.ts index a9c8f94cf9..e895b48f81 100644 --- a/APIReference/Service/DataTypeDetail.ts +++ b/APIReference/Service/DataTypeDetail.ts @@ -521,6 +521,19 @@ const dataTypeDetails: Dictionary = { }, ], }, + { + name: "dnsMonitor", + type: "MonitorStepDnsMonitor", + required: false, + description: + "Configuration for DNS monitoring. Required for DNS monitor type. Defines query name (domain), record type, optional DNS server, port, timeout, and retry settings. See MonitorStepDnsMonitor.", + typeLinks: [ + { + label: "MonitorStepDnsMonitor", + path: "monitor-step-dns-monitor", + }, + ], + }, ], values: [], jsonExample: JSON.stringify( @@ -2560,6 +2573,31 @@ const dataTypeDetails: Dictionary = { description: "Whether the SNMP device is reachable. Use with 'True' or 'False'. Applies to: SNMP monitors.", }, + { + value: "DNS Response Time (in ms)", + description: + "The DNS query response time in milliseconds. Use with numeric FilterTypes. Applies to: DNS monitors.", + }, + { + value: "DNS Is Online", + description: + "Whether the DNS resolution succeeded. Use with 'True' or 'False'. Applies to: DNS monitors.", + }, + { + value: "DNS Record Value", + description: + "The value of a DNS record returned by the query. Use with string FilterTypes (Contains, EqualTo, etc.). Applies to: DNS monitors.", + }, + { + value: "DNSSEC Is Valid", + description: + "Whether DNSSEC validation passed (AD flag present). Use with 'True' or 'False'. Applies to: DNS monitors.", + }, + { + value: "DNS Record Exists", + description: + "Whether any DNS records were returned for the query. Use with 'True' or 'False'. Applies to: DNS monitors.", + }, { value: "JavaScript Expression", description: @@ -3112,6 +3150,94 @@ const dataTypeDetails: Dictionary = { 2, ), }, + "monitor-step-dns-monitor": { + title: "MonitorStepDnsMonitor", + description: + "Configuration for a DNS monitor step. Defines the domain to query, record type, optional custom DNS server, and timeout settings. Used as the 'dnsMonitor' property on a MonitorStep when the monitor type is 'DNS'. The criteria filters can then use 'DNS Is Online', 'DNS Response Time (in ms)', 'DNS Record Value', 'DNSSEC Is Valid', and 'DNS Record Exists' as CheckOn values.", + isEnum: false, + relatedTypes: [ + { + name: "MonitorStep", + path: "monitor-step", + relationship: "Parent that holds this as dnsMonitor property", + }, + { + name: "CheckOn", + path: "check-on", + relationship: "Use DNS-specific CheckOn values with DNS monitors", + }, + ], + properties: [ + { + name: "queryName", + type: "string", + required: true, + description: + "The domain name to query (e.g., 'example.com').", + }, + { + name: "recordType", + type: "string (enum)", + required: true, + description: + "The DNS record type to query. Possible values: 'A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'PTR', 'SRV', 'CAA'.", + }, + { + name: "hostname", + type: "string", + required: false, + description: + "Custom DNS server to use for the query (e.g., '8.8.8.8'). Leave empty to use system default DNS resolver.", + }, + { + name: "port", + type: "number", + required: false, + description: "DNS port. Default is 53.", + }, + { + name: "timeout", + type: "number", + required: false, + description: + "Timeout for DNS queries in milliseconds. Default is 5000 (5 seconds).", + }, + { + name: "retries", + type: "number", + required: false, + description: "Number of retries for failed DNS queries. Default is 3.", + }, + ], + values: [], + jsonExample: JSON.stringify( + { + "// Example 1: Basic A record lookup": { + queryName: "example.com", + recordType: "A", + timeout: 5000, + retries: 3, + }, + "// Example 2: MX record with custom DNS server": { + queryName: "example.com", + recordType: "MX", + hostname: "8.8.8.8", + port: 53, + timeout: 5000, + retries: 3, + }, + "// Example 3: TXT record for SPF verification": { + queryName: "example.com", + recordType: "TXT", + hostname: "1.1.1.1", + timeout: 5000, + retries: 3, + }, + }, + null, + 2, + ), + }, }; export default class ServiceHandler { diff --git a/Common/Server/Services/MonitorService.ts b/Common/Server/Services/MonitorService.ts index 78ffad4f57..f18c718fed 100644 --- a/Common/Server/Services/MonitorService.ts +++ b/Common/Server/Services/MonitorService.ts @@ -127,6 +127,14 @@ export class Service extends DatabaseService { monitorDestination = `${monitorDestination}:${port}`; } } + + // For DNS monitors, use the queryName from dnsMonitor config + if (monitorType === MonitorType.DNS && firstStep?.data?.dnsMonitor) { + monitorDestination = firstStep.data.dnsMonitor.queryName || ""; + if (firstStep.data.dnsMonitor.hostname) { + monitorDestination = `${monitorDestination} @${firstStep.data.dnsMonitor.hostname}`; + } + } } } diff --git a/Common/Server/Utils/Monitor/MonitorTemplateUtil.ts b/Common/Server/Utils/Monitor/MonitorTemplateUtil.ts index d0e6cddd9e..49550b9aaf 100644 --- a/Common/Server/Utils/Monitor/MonitorTemplateUtil.ts +++ b/Common/Server/Utils/Monitor/MonitorTemplateUtil.ts @@ -14,6 +14,9 @@ import SyntheticMonitorResponse from "../../../Types/Monitor/SyntheticMonitors/S import SnmpMonitorResponse, { SnmpOidResponse, } from "../../../Types/Monitor/SnmpMonitor/SnmpMonitorResponse"; +import DnsMonitorResponse, { + DnsRecordResponse, +} from "../../../Types/Monitor/DnsMonitor/DnsMonitorResponse"; import Typeof from "../../../Types/Typeof"; import VMUtil from "../VM/VMAPI"; import DataToProcess from "./DataToProcess"; @@ -240,6 +243,40 @@ export default class MonitorTemplateUtil { } } } + + if (data.monitorType === MonitorType.DNS) { + const dnsResponse: DnsMonitorResponse | undefined = ( + data.dataToProcess as ProbeMonitorResponse + ).dnsResponse; + + storageMap = { + isOnline: (data.dataToProcess as ProbeMonitorResponse).isOnline, + responseTimeInMs: dnsResponse?.responseTimeInMs, + failureCause: dnsResponse?.failureCause, + isTimeout: dnsResponse?.isTimeout, + isDnssecValid: dnsResponse?.isDnssecValid, + } as JSONObject; + + // Add DNS records + if (dnsResponse?.records) { + storageMap["records"] = dnsResponse.records.map( + (record: DnsRecordResponse) => { + return { + type: record.type, + value: record.value, + ttl: record.ttl, + }; + }, + ); + + // Add record values as a flat array for easier templating + storageMap["recordValues"] = dnsResponse.records.map( + (record: DnsRecordResponse) => { + return record.value; + }, + ); + } + } } catch (err) { logger.error(err); } diff --git a/Common/Utils/Monitor/MonitorMetricType.ts b/Common/Utils/Monitor/MonitorMetricType.ts index b2562a82ce..d68554c244 100644 --- a/Common/Utils/Monitor/MonitorMetricType.ts +++ b/Common/Utils/Monitor/MonitorMetricType.ts @@ -85,7 +85,8 @@ class MonitorMetricTypeUtil { monitorType === MonitorType.Ping || monitorType === MonitorType.IP || monitorType === MonitorType.Port || - monitorType === MonitorType.SNMP + monitorType === MonitorType.SNMP || + monitorType === MonitorType.DNS ) { return [MonitorMetricType.IsOnline, MonitorMetricType.ResponseTime]; } diff --git a/ProbeIngest/Utils/Monitor.ts b/ProbeIngest/Utils/Monitor.ts index 54a5691a7d..2aa622b3b4 100644 --- a/ProbeIngest/Utils/Monitor.ts +++ b/ProbeIngest/Utils/Monitor.ts @@ -196,6 +196,45 @@ export default class MonitorUtil { } } + if (monitorType === MonitorType.DNS) { + for (const monitorStep of monitorSteps?.data?.monitorStepsInstanceArray || + []) { + // Handle DNS hostname secrets (custom DNS server) + if ( + monitorStep.data?.dnsMonitor?.hostname && + this.hasSecrets(monitorStep.data.dnsMonitor.hostname) + ) { + if (!isSecretsLoaded) { + monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId); + isSecretsLoaded = true; + } + + monitorStep.data.dnsMonitor.hostname = + (await MonitorUtil.fillSecretsInStringOrJSON({ + secrets: monitorSecrets, + populateSecretsIn: monitorStep.data.dnsMonitor.hostname, + })) as string; + } + + // Handle DNS query name secrets + if ( + monitorStep.data?.dnsMonitor?.queryName && + this.hasSecrets(monitorStep.data.dnsMonitor.queryName) + ) { + if (!isSecretsLoaded) { + monitorSecrets = await MonitorUtil.loadMonitorSecrets(monitorId); + isSecretsLoaded = true; + } + + monitorStep.data.dnsMonitor.queryName = + (await MonitorUtil.fillSecretsInStringOrJSON({ + secrets: monitorSecrets, + populateSecretsIn: monitorStep.data.dnsMonitor.queryName, + })) as string; + } + } + } + return monitorSteps; }