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

110
internal/report/renderer.go Normal file
View File

@@ -0,0 +1,110 @@
package report
import (
"encoding/json"
"fmt"
"io"
"strings"
"time"
"arcline-audit/internal/types"
)
const sectionWidth = 55
// Terminal renders the audit result to the given writer in a human-readable terminal format.
func Terminal(w io.Writer, result types.AuditResult) {
fmt.Fprintf(w, "\n")
printSection(w, "SSL", result.SSL.Checks, func() {
if result.SSL.Expiry != (time.Time{}) {
fmt.Fprintf(w, " Issuer %s\n", result.SSL.Issuer)
fmt.Fprintf(w, " Expires %s (%d days)\n",
result.SSL.Expiry.Format("2006-01-02"), result.SSL.DaysLeft)
if result.SSL.TLSVersion != "" {
fmt.Fprintf(w, " TLS %s\n", result.SSL.TLSVersion)
}
}
})
printSection(w, "HTTP", result.HTTP.Checks, func() {
if result.HTTP.FinalURL != "" {
fmt.Fprintf(w, " Final URL %s\n", result.HTTP.FinalURL)
}
fmt.Fprintf(w, " Response %dms\n", result.HTTP.ResponseTime.Milliseconds())
})
printSection(w, "DNS", result.DNS.Checks, nil)
printSection(w, "Infrastructure", result.Infra.Checks, func() {
if result.Infra.CDN != "" {
fmt.Fprintf(w, " CDN %s\n", result.Infra.CDN)
}
if result.Infra.ASN != "" {
fmt.Fprintf(w, " ASN %s\n", result.Infra.ASN)
}
if result.Infra.Org != "" {
fmt.Fprintf(w, " Org %s\n", result.Infra.Org)
}
})
fmt.Fprintf(w, "\n")
}
// JSON renders the audit result as indented JSON.
func JSON(w io.Writer, result types.AuditResult) {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
enc.Encode(result)
}
// PlainText renders the audit result as plain text (no ANSI codes).
func PlainText(w io.Writer, result types.AuditResult) {
sections := []struct {
name string
checks []types.CheckResult
}{
{"SSL", result.SSL.Checks},
{"HTTP", result.HTTP.Checks},
{"DNS", result.DNS.Checks},
{"Infrastructure", result.Infra.Checks},
}
fmt.Fprintf(w, "Audit for %s — %s\n\n", result.Domain, result.Time.Format(time.RFC3339))
for _, sec := range sections {
fmt.Fprintf(w, "── %s %s\n", sec.name, strings.Repeat("─", sectionWidth-5-len(sec.name)))
for _, c := range sec.checks {
fmt.Fprintf(w, "[%s] %s\n", c.Status, c.Message)
}
fmt.Fprintf(w, "\n")
}
}
func printSection(w io.Writer, name string, checks []types.CheckResult, extra func()) {
header := fmt.Sprintf("── %s ", name)
fmt.Fprintf(w, " %s%s\n", header, strings.Repeat("─", sectionWidth-len(header)-2))
for _, c := range checks {
prefix := statusPrefix(c.Status)
fmt.Fprintf(w, " %s %s\n", prefix, c.Message)
}
if extra != nil {
extra()
}
}
func statusPrefix(s types.Status) string {
switch s {
case types.StatusOK:
return "[OK] "
case types.StatusWarn:
return "[WARN]"
case types.StatusFail:
return "[FAIL]"
case types.StatusInfo:
return "[INFO]"
default:
return "[????]"
}
}