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:
169
internal/ssl/ssl.go
Normal file
169
internal/ssl/ssl.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user