Files
audit/internal/infra/checker.go
Blake Ridgway 825df331a4 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>
2026-06-23 05:12:22 -05:00

193 lines
4.4 KiB
Go

package infra
import (
"bufio"
"fmt"
"net"
"strings"
"sync"
"time"
"arcline-audit/internal/types"
)
// CDN ranges for common CDN providers (simplified detection based on IP prefixes).
var cdnRanges = map[string][]string{
"Cloudflare": {
"104.16.", "104.17.", "104.18.", "104.19.", "104.20.", "104.21.",
"104.22.", "104.23.", "104.24.", "104.25.", "104.26.", "104.27.",
"104.28.", "104.29.", "104.30.", "104.31.",
"172.64.", "172.65.", "172.66.", "172.67.", "172.68.", "172.69.",
"172.70.", "172.71.",
},
"Fastly": {
"151.101.", "199.232.", "23.235.", "146.75.",
},
"Amazon CloudFront": {
"13.32.", "13.33.", "13.224.", "13.225.", "13.226.", "13.227.",
"13.249.", "54.192.", "54.230.", "54.239.",
},
}
// commonPorts are the ports to probe.
var commonPorts = map[int]string{
80: "HTTP",
443: "HTTPS",
22: "SSH",
3306: "MySQL",
5432: "PostgreSQL",
}
// Run performs all infrastructure checks for the given domain.
// It accepts pre-resolved IPs to avoid redundant lookups; if empty, it resolves the domain itself.
func Run(domain string, resolvedIPs []string) types.InfraResult {
var result types.InfraResult
ips := resolvedIPs
if len(ips) == 0 {
var err error
ips, err = net.LookupHost(domain)
if err != nil || len(ips) == 0 {
result.Checks = append(result.Checks, types.CheckResult{
Status: types.StatusFail, Message: fmt.Sprintf("cannot resolve domain: %v", err),
})
return result
}
}
// Select the first IPv4 address for port scanning, but check all IPs for CDN.
targetIP := selectIPv4(ips)
// CDN detection (check all IPs)
cdn := detectCDNAny(ips)
if cdn != "" {
result.Checks = append(result.Checks, types.CheckResult{
Status: types.StatusOK,
Message: fmt.Sprintf("CDN detected: %s", cdn),
})
} else {
result.Checks = append(result.Checks, types.CheckResult{
Status: types.StatusOK,
Message: "not behind a 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
result.Checks = append(result.Checks, checkPorts(targetIP)...)
return result
}
func selectIPv4(ips []string) string {
for _, ip := range ips {
if parsed := net.ParseIP(ip); parsed != nil && parsed.To4() != nil {
return ip
}
}
return ips[0]
}
func detectCDNAny(ips []string) string {
for _, ip := range ips {
if cdn := detectCDN(ip); cdn != "" {
return cdn
}
}
return ""
}
func detectCDN(ip string) string {
for provider, prefixes := range cdnRanges {
for _, prefix := range prefixes {
if strings.HasPrefix(ip, prefix) {
return provider
}
}
}
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 {
var checks []types.CheckResult
var mu sync.Mutex
var wg sync.WaitGroup
host := ip
if strings.Contains(ip, ":") {
host = "[" + ip + "]"
}
for port, name := range commonPorts {
wg.Add(1)
go func(port int, name string) {
defer wg.Done()
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
conn, err := net.DialTimeout("tcp", addr, 2*time.Second)
if err == nil {
conn.Close()
mu.Lock()
checks = append(checks, types.CheckResult{
Status: types.StatusInfo,
Message: fmt.Sprintf("port %d (%s) open", port, name),
})
mu.Unlock()
}
}(port, name)
}
wg.Wait()
return checks
}