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:
Blake Ridgway
2026-06-23 05:08:34 -05:00
parent 088bb7e138
commit fce90f458c
13 changed files with 1167 additions and 11 deletions

141
internal/infra/checker.go Normal file
View 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
}