add PTR/rDNS match check and ASN/org lookup via WHOIS
- DNS: reverse DNS lookup on A record IPs to verify PTR configuration - Infra: ASN and organization lookup via whois.radb.net - Refactor checkARecords to return resolved IPs for downstream use Signed-off-by: Blake Ridgway <blake@blakeridgway.com>
This commit is contained in:
BIN
arcline-audit
BIN
arcline-audit
Binary file not shown.
@@ -12,8 +12,9 @@ import (
|
|||||||
func Run(domain string) types.DNSResult {
|
func Run(domain string) types.DNSResult {
|
||||||
var checks []types.CheckResult
|
var checks []types.CheckResult
|
||||||
|
|
||||||
// A records
|
// A records (also resolves IPs for PTR check)
|
||||||
checks = append(checks, checkARecords(domain)...)
|
ips, aRecordChecks := checkARecords(domain)
|
||||||
|
checks = append(checks, aRecordChecks...)
|
||||||
|
|
||||||
// AAAA records
|
// AAAA records
|
||||||
checks = append(checks, checkAAAARecords(domain)...)
|
checks = append(checks, checkAAAARecords(domain)...)
|
||||||
@@ -27,13 +28,17 @@ func Run(domain string) types.DNSResult {
|
|||||||
// DNSSEC
|
// DNSSEC
|
||||||
checks = append(checks, checkDNSSEC(domain)...)
|
checks = append(checks, checkDNSSEC(domain)...)
|
||||||
|
|
||||||
|
// PTR / rDNS match
|
||||||
|
checks = append(checks, checkPTR(ips)...)
|
||||||
|
|
||||||
return types.DNSResult{Checks: checks}
|
return types.DNSResult{Checks: checks}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkARecords(domain string) []types.CheckResult {
|
// checkARecords returns the resolved IPv4 addresses and the check results.
|
||||||
|
func checkARecords(domain string) ([]string, []types.CheckResult) {
|
||||||
ips, err := net.LookupHost(domain)
|
ips, err := net.LookupHost(domain)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []types.CheckResult{{Status: types.StatusFail, Message: fmt.Sprintf("no A record: %v", err)}}
|
return nil, []types.CheckResult{{Status: types.StatusFail, Message: fmt.Sprintf("no A record: %v", err)}}
|
||||||
}
|
}
|
||||||
var ipv4 []string
|
var ipv4 []string
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
@@ -42,9 +47,9 @@ func checkARecords(domain string) []types.CheckResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(ipv4) == 0 {
|
if len(ipv4) == 0 {
|
||||||
return []types.CheckResult{{Status: types.StatusFail, Message: "no A record found"}}
|
return nil, []types.CheckResult{{Status: types.StatusFail, Message: "no A record found"}}
|
||||||
}
|
}
|
||||||
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("A record: %s", strings.Join(ipv4, ", "))}}
|
return ipv4, []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("A record: %s", strings.Join(ipv4, ", "))}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkAAAARecords(domain string) []types.CheckResult {
|
func checkAAAARecords(domain string) []types.CheckResult {
|
||||||
@@ -59,7 +64,7 @@ func checkAAAARecords(domain string) []types.CheckResult {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(ipv6) == 0 {
|
if len(ipv6) == 0 {
|
||||||
return nil // Not a warning; many sites don't have IPv6
|
return nil
|
||||||
}
|
}
|
||||||
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("AAAA record: %s", strings.Join(ipv6, ", "))}}
|
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("AAAA record: %s", strings.Join(ipv6, ", "))}}
|
||||||
}
|
}
|
||||||
@@ -82,7 +87,6 @@ func checkTXTRecords(domain string) []types.CheckResult {
|
|||||||
|
|
||||||
var checks []types.CheckResult
|
var checks []types.CheckResult
|
||||||
hasSPF := false
|
hasSPF := false
|
||||||
hasDMARC := false
|
|
||||||
|
|
||||||
for _, txt := range txts {
|
for _, txt := range txts {
|
||||||
if strings.HasPrefix(txt, "v=spf1") {
|
if strings.HasPrefix(txt, "v=spf1") {
|
||||||
@@ -109,16 +113,12 @@ func checkTXTRecords(domain string) []types.CheckResult {
|
|||||||
if err == nil {
|
if err == nil {
|
||||||
for _, txt := range dmarcTxts {
|
for _, txt := range dmarcTxts {
|
||||||
if strings.HasPrefix(txt, "v=DMARC1") {
|
if strings.HasPrefix(txt, "v=DMARC1") {
|
||||||
hasDMARC = true
|
checks = append(checks, types.CheckResult{Status: types.StatusOK, Message: "DMARC record found"})
|
||||||
break
|
return checks
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasDMARC {
|
checks = append(checks, types.CheckResult{Status: types.StatusWarn, Message: "no DMARC record"})
|
||||||
checks = append(checks, types.CheckResult{Status: types.StatusOK, Message: "DMARC record found"})
|
|
||||||
} else {
|
|
||||||
checks = append(checks, types.CheckResult{Status: types.StatusWarn, Message: "no DMARC record"})
|
|
||||||
}
|
|
||||||
|
|
||||||
return checks
|
return checks
|
||||||
}
|
}
|
||||||
@@ -137,19 +137,47 @@ func checkDKIM(domain string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func checkDNSSEC(domain string) []types.CheckResult {
|
func checkDNSSEC(domain string) []types.CheckResult {
|
||||||
// DNSSEC is checked via looking up the DS record on the parent zone.
|
|
||||||
// For simplicity, we check if the domain has RRSIG records by looking up
|
|
||||||
// the NS records and checking for authenticated data.
|
|
||||||
// A true DNSSEC check requires a validating resolver. We do a best-effort
|
|
||||||
// check by seeing if the resolver returns authenticated data headers.
|
|
||||||
// As a simple heuristic, we check for DNSKEY records.
|
|
||||||
_, err := net.LookupTXT("_dnssec." + domain)
|
_, err := net.LookupTXT("_dnssec." + domain)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return []types.CheckResult{{Status: types.StatusOK, Message: "DNSSEC appears enabled"}}
|
return []types.CheckResult{{Status: types.StatusOK, Message: "DNSSEC appears enabled"}}
|
||||||
}
|
}
|
||||||
// Try to retrieve DNSKEY records as a secondary heuristic
|
|
||||||
// Note: Go's net package doesn't expose DNSKEY record types directly.
|
|
||||||
// A full DNSSEC check would require a custom DNS resolver.
|
|
||||||
return []types.CheckResult{{Status: types.StatusInfo, Message: "DNSSEC check requires custom resolver (not verified)"}}
|
return []types.CheckResult{{Status: types.StatusInfo, Message: "DNSSEC check requires custom resolver (not verified)"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkPTR performs a reverse DNS lookup on the given IPs and checks if
|
||||||
|
// any returned hostname resolves back to one of the original IPs.
|
||||||
|
func checkPTR(ips []string) []types.CheckResult {
|
||||||
|
if len(ips) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check up to the first 2 IPs to keep things fast.
|
||||||
|
limit := 2
|
||||||
|
if len(ips) < limit {
|
||||||
|
limit = len(ips)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
ip := ips[i]
|
||||||
|
names, err := net.LookupAddr(ip)
|
||||||
|
if err != nil || len(names) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range names {
|
||||||
|
name = strings.TrimSuffix(name, ".")
|
||||||
|
resolved, err := net.LookupHost(name)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, resolvedIP := range resolved {
|
||||||
|
if resolvedIP == ip {
|
||||||
|
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("rDNS matches (%s → %s)", ip, name)}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return []types.CheckResult{{Status: types.StatusInfo, Message: "no PTR record found (rDNS not configured)"}}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package infra
|
package infra
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -72,6 +73,20 @@ func Run(domain string, resolvedIPs []string) types.InfraResult {
|
|||||||
}
|
}
|
||||||
result.CDN = cdn
|
result.CDN = cdn
|
||||||
|
|
||||||
|
// ASN / org lookup via WHOIS
|
||||||
|
asn, org := lookupASN(targetIP)
|
||||||
|
if asn != "" {
|
||||||
|
result.ASN = asn
|
||||||
|
result.Org = org
|
||||||
|
msg := fmt.Sprintf("ASN: %s", asn)
|
||||||
|
if org != "" {
|
||||||
|
msg += fmt.Sprintf(" (%s)", org)
|
||||||
|
}
|
||||||
|
result.Checks = append(result.Checks, types.CheckResult{
|
||||||
|
Status: types.StatusInfo, Message: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// Common ports check
|
// Common ports check
|
||||||
result.Checks = append(result.Checks, checkPorts(targetIP)...)
|
result.Checks = append(result.Checks, checkPorts(targetIP)...)
|
||||||
|
|
||||||
@@ -107,6 +122,42 @@ func detectCDN(ip string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lookupASN queries whois.radb.net for the ASN and organization of an IP.
|
||||||
|
func lookupASN(ip string) (asn, org string) {
|
||||||
|
conn, err := net.DialTimeout("tcp", "whois.radb.net:43", 5*time.Second)
|
||||||
|
if err != nil {
|
||||||
|
return "", ""
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Set a deadline for the entire WHOIS exchange.
|
||||||
|
conn.SetDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
|
||||||
|
fmt.Fprintf(conn, "%s\r\n", ip)
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(conn)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := scanner.Text()
|
||||||
|
if asn == "" && strings.HasPrefix(line, "origin:") {
|
||||||
|
asn = strings.TrimSpace(strings.TrimPrefix(line, "origin:"))
|
||||||
|
asn = strings.ToUpper(asn)
|
||||||
|
}
|
||||||
|
if org == "" && strings.HasPrefix(line, "descr:") {
|
||||||
|
org = strings.TrimSpace(strings.TrimPrefix(line, "descr:"))
|
||||||
|
}
|
||||||
|
if asn != "" && org != "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim long descriptions to keep output readable.
|
||||||
|
if len(org) > 80 {
|
||||||
|
org = org[:77] + "..."
|
||||||
|
}
|
||||||
|
|
||||||
|
return asn, org
|
||||||
|
}
|
||||||
|
|
||||||
func checkPorts(ip string) []types.CheckResult {
|
func checkPorts(ip string) []types.CheckResult {
|
||||||
var checks []types.CheckResult
|
var checks []types.CheckResult
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
|
|||||||
Reference in New Issue
Block a user