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

169
internal/ssl/ssl.go Normal file
View File

@@ -0,0 +1,169 @@
package ssl
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"time"
"arcline-audit/internal/types"
)
// insecureCipherSuites is a set of cipher suite IDs considered weak.
var insecureCipherSuites = map[uint16]bool{
tls.TLS_RSA_WITH_RC4_128_SHA: true,
tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA: true,
tls.TLS_RSA_WITH_AES_128_CBC_SHA: true,
tls.TLS_RSA_WITH_AES_256_CBC_SHA: true,
tls.TLS_ECDHE_ECDSA_WITH_RC4_128_SHA: true,
tls.TLS_ECDHE_RSA_WITH_RC4_128_SHA: true,
tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA: true,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA: true,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA: true,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA: true,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA: true,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256: true,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384: true,
}
// Run performs all SSL/TLS checks for the given domain.
func Run(domain string) types.SSLResult {
addr := net.JoinHostPort(domain, "443")
dialer := &net.Dialer{Timeout: 10 * time.Second}
conn, err := tls.DialWithDialer(dialer, "tcp", addr, &tls.Config{
InsecureSkipVerify: true,
})
if err != nil {
return types.SSLResult{
Checks: []types.CheckResult{
{Status: types.StatusFail, Message: fmt.Sprintf("failed to connect: %v", err)},
},
}
}
defer conn.Close()
state := conn.ConnectionState()
var checks []types.CheckResult
// Certificate validity and details
checks = append(checks, checkCertificates(state.PeerCertificates)...)
// TLS version
checks = append(checks, checkTLSVersion(state.Version)...)
// Cipher suite
checks = append(checks, checkCipherSuite(state.CipherSuite)...)
result := types.SSLResult{Checks: checks}
if len(state.PeerCertificates) > 0 {
cert := state.PeerCertificates[0]
result.Issuer = cert.Issuer.String()
result.Expiry = cert.NotAfter
result.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
}
result.TLSVersion = tlsVersionName(state.Version)
return result
}
func checkCertificates(certs []*x509.Certificate) []types.CheckResult {
var checks []types.CheckResult
if len(certs) == 0 {
checks = append(checks, types.CheckResult{
Status: types.StatusFail, Message: "no certificates presented",
})
return checks
}
leaf := certs[0]
now := time.Now()
// Check expiry
if now.After(leaf.NotAfter) {
checks = append(checks, types.CheckResult{
Status: types.StatusFail,
Message: fmt.Sprintf("certificate expired on %s", leaf.NotAfter.Format("2006-01-02")),
})
} else {
daysLeft := int(time.Until(leaf.NotAfter).Hours() / 24)
if daysLeft < 30 {
checks = append(checks, types.CheckResult{
Status: types.StatusWarn,
Message: fmt.Sprintf("certificate expires in %d days (%s)", daysLeft, leaf.NotAfter.Format("2006-01-02")),
})
} else {
checks = append(checks, types.CheckResult{
Status: types.StatusOK,
Message: fmt.Sprintf("valid certificate (%s)", leaf.NotAfter.Format("2006-01-02")),
})
}
}
// Check self-signed
if leaf.Issuer.String() == leaf.Subject.String() {
checks = append(checks, types.CheckResult{
Status: types.StatusWarn, Message: "self-signed certificate",
})
} else {
checks = append(checks, types.CheckResult{
Status: types.StatusOK, Message: "not self-signed",
})
}
// Check chain completeness
if len(certs) >= 2 {
checks = append(checks, types.CheckResult{
Status: types.StatusOK, Message: "certificate chain is complete",
})
} else {
checks = append(checks, types.CheckResult{
Status: types.StatusWarn, Message: "certificate chain may be incomplete",
})
}
return checks
}
func checkTLSVersion(vers uint16) []types.CheckResult {
name := tlsVersionName(vers)
switch vers {
case tls.VersionTLS13:
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("TLS %s", name)}}
case tls.VersionTLS12:
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("TLS %s", name)}}
case tls.VersionTLS11, tls.VersionTLS10:
return []types.CheckResult{{Status: types.StatusWarn, Message: fmt.Sprintf("TLS %s is insecure", name)}}
case 0:
return []types.CheckResult{{Status: types.StatusFail, Message: "unknown TLS version"}}
default:
return []types.CheckResult{{Status: types.StatusInfo, Message: fmt.Sprintf("TLS %s", name)}}
}
}
func checkCipherSuite(id uint16) []types.CheckResult {
name := tls.CipherSuiteName(id)
if name == "" {
name = fmt.Sprintf("unknown (0x%04X)", id)
}
if insecureCipherSuites[id] {
return []types.CheckResult{{Status: types.StatusWarn, Message: fmt.Sprintf("weak cipher suite: %s", name)}}
}
return []types.CheckResult{{Status: types.StatusOK, Message: fmt.Sprintf("cipher suite: %s", name)}}
}
func tlsVersionName(v uint16) string {
switch v {
case tls.VersionTLS10:
return "1.0"
case tls.VersionTLS11:
return "1.1"
case tls.VersionTLS12:
return "1.2"
case tls.VersionTLS13:
return "1.3"
default:
return fmt.Sprintf("0x%04X", v)
}
}