add initial Go implementation of arcline-audit
Implements the full site health auditor with four check groups: - SSL/TLS (certificate validity, expiry, chain, TLS version, ciphers) - HTTP (redirect chain, security headers, response time) - DNS (A/AAAA, MX, SPF, DKIM, DMARC, DNSSEC) - Infrastructure (CDN detection, common port probes) Includes CLI with --checks filter, --json and --out flags, cross-compile Makefile, and GitLab CI pipeline. Signed-off-by: Blake Ridgway <blake@blakeridgway.com>
This commit is contained in:
141
internal/infra/checker.go
Normal file
141
internal/infra/checker.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package infra
|
||||
|
||||
import (
|
||||
"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
|
||||
|
||||
// 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 ""
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user