- 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>
193 lines
4.4 KiB
Go
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
|
|
}
|
|
|