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>
111 lines
2.8 KiB
Go
111 lines
2.8 KiB
Go
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 "[????]"
|
|
}
|
|
}
|
|
|