Initial commit
This commit is contained in:
commit
612a19c01a
23 changed files with 2402 additions and 0 deletions
586
checker/report.go
Normal file
586
checker/report.go
Normal file
|
|
@ -0,0 +1,586 @@
|
|||
package checker
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
// ─── View models ────────────────────────────────────────────────────
|
||||
|
||||
type reportFix struct {
|
||||
Severity string
|
||||
Code string
|
||||
Message string
|
||||
Fix string
|
||||
Endpoint string
|
||||
}
|
||||
|
||||
type reportTLSPosture struct {
|
||||
CheckedAt time.Time
|
||||
ChainValid *bool
|
||||
HostnameMatch *bool
|
||||
NotAfter time.Time
|
||||
TLSVersion string
|
||||
Issues []reportFix
|
||||
}
|
||||
|
||||
type reportEndpoint struct {
|
||||
Transport string
|
||||
TransportTag string // "SIP/UDP", "SIP/TCP", "SIPS/TLS"
|
||||
SRVPrefix string
|
||||
Target string
|
||||
Port uint16
|
||||
Address string
|
||||
IsIPv6 bool
|
||||
Reachable bool
|
||||
ReachableErr string
|
||||
TLSVersion string
|
||||
TLSCipher string
|
||||
OptionsSent bool
|
||||
OptionsStatus string
|
||||
OptionsRTTMs int64
|
||||
ServerHeader string
|
||||
UserAgent string
|
||||
AllowMethods []string
|
||||
ContactURI string
|
||||
ElapsedMS int64
|
||||
Error string
|
||||
OK bool
|
||||
StatusLabel string
|
||||
StatusClass string
|
||||
|
||||
TLSPosture *reportTLSPosture
|
||||
}
|
||||
|
||||
type reportSRVEntry struct {
|
||||
Prefix string
|
||||
Target string
|
||||
Port uint16
|
||||
Priority uint16
|
||||
Weight uint16
|
||||
IPv4 []string
|
||||
IPv6 []string
|
||||
}
|
||||
|
||||
type reportData struct {
|
||||
Domain string
|
||||
RunAt string
|
||||
StatusLabel string
|
||||
StatusClass string
|
||||
HasIssues bool
|
||||
Fixes []reportFix
|
||||
NAPTR []NAPTRRecord
|
||||
SRV []reportSRVEntry
|
||||
FallbackProbed bool
|
||||
Endpoints []reportEndpoint
|
||||
HasIPv4 bool
|
||||
HasIPv6 bool
|
||||
WorkingUDP bool
|
||||
WorkingTCP bool
|
||||
WorkingTLS bool
|
||||
HasTLSPosture bool
|
||||
}
|
||||
|
||||
// ─── Template ───────────────────────────────────────────────────────
|
||||
|
||||
var reportTpl = template.Must(template.New("sip").Funcs(template.FuncMap{
|
||||
"deref": func(b *bool) bool { return b != nil && *b },
|
||||
}).Parse(`<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SIP Report, {{.Domain}}</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
:root {
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: #1f2937;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
body { margin: 0; padding: 1rem; }
|
||||
code { font-family: ui-monospace, monospace; font-size: .9em; }
|
||||
h1 { margin: 0 0 .4rem; font-size: 1.15rem; font-weight: 700; }
|
||||
h2 { font-size: 1rem; font-weight: 700; margin: 0 0 .6rem; }
|
||||
h3 { font-size: .9rem; font-weight: 600; margin: 0 0 .4rem; }
|
||||
|
||||
.hd, .section, details {
|
||||
background: #fff;
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.08);
|
||||
}
|
||||
.hd { border-radius: 10px; padding: 1rem 1.25rem; margin-bottom: .75rem; }
|
||||
.section { border-radius: 8px; padding: .85rem 1rem; margin-bottom: .6rem; }
|
||||
details { border-radius: 8px; margin-bottom: .45rem; overflow: hidden; }
|
||||
.section details { box-shadow: none; border: 1px solid #e5e7eb; margin-bottom: .4rem; }
|
||||
summary {
|
||||
display: flex; align-items: center; gap: .5rem;
|
||||
padding: .65rem 1rem; cursor: pointer; user-select: none; list-style: none;
|
||||
}
|
||||
summary::-webkit-details-marker { display: none; }
|
||||
summary::before {
|
||||
content: "▶"; font-size: .65rem; color: #9ca3af;
|
||||
transition: transform .15s; flex-shrink: 0;
|
||||
}
|
||||
details[open] > summary::before { transform: rotate(90deg); }
|
||||
|
||||
.badge {
|
||||
display: inline-flex; align-items: center;
|
||||
padding: .2em .65em; border-radius: 9999px;
|
||||
font-size: .78rem; font-weight: 700; letter-spacing: .02em;
|
||||
}
|
||||
.badge.ok { background: #d1fae5; color: #065f46; }
|
||||
.badge.warn { background: #fef3c7; color: #92400e; }
|
||||
.badge.fail { background: #fee2e2; color: #991b1b; }
|
||||
.badge.muted{ background: #e5e7eb; color: #374151; }
|
||||
|
||||
.chips { display: flex; gap: .35rem; flex-wrap: wrap; margin: .4rem 0; }
|
||||
.chip {
|
||||
display: inline-flex; align-items: center; gap: .35rem;
|
||||
padding: .15rem .55rem; border-radius: 6px;
|
||||
font-size: .75rem; font-weight: 600;
|
||||
background: #f3f4f6; color: #374151;
|
||||
}
|
||||
.chip.ok { background: #d1fae5; color: #065f46; }
|
||||
.chip.fail { background: #fee2e2; color: #991b1b; }
|
||||
|
||||
.fix {
|
||||
border-left: 3px solid #e5e7eb;
|
||||
padding: .5rem .75rem;
|
||||
margin-bottom: .4rem;
|
||||
background: #fafafa;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
.fix.crit { border-left-color: #dc2626; background: #fef2f2; }
|
||||
.fix.warn { border-left-color: #d97706; background: #fffbeb; }
|
||||
.fix.info { border-left-color: #2563eb; background: #eff6ff; }
|
||||
.fix .code { font-size: .7rem; color: #6b7280; font-family: ui-monospace, monospace; }
|
||||
.fix .msg { font-weight: 600; margin: .1rem 0; }
|
||||
.fix .how { color: #374151; font-size: .85rem; }
|
||||
|
||||
.conn-head { font-weight: 600; flex: 1; font-size: .9rem; font-family: ui-monospace, monospace; }
|
||||
.details-body { padding: .6rem 1rem .85rem; border-top: 1px solid #f3f4f6; }
|
||||
|
||||
dl { display: grid; grid-template-columns: max-content 1fr; gap: .2rem .75rem; margin: 0; font-size: .85rem; }
|
||||
dt { color: #6b7280; }
|
||||
dd { margin: 0; }
|
||||
|
||||
table { border-collapse: collapse; width: 100%; font-size: .85rem; }
|
||||
th, td { text-align: left; padding: .3rem .5rem; border-bottom: 1px solid #f3f4f6; vertical-align: top; }
|
||||
th { font-weight: 600; color: #6b7280; }
|
||||
|
||||
.check-ok { color: #059669; font-weight: 700; }
|
||||
.check-fail { color: #dc2626; font-weight: 700; }
|
||||
.note { color: #6b7280; font-size: .85rem; }
|
||||
.footer { color: #6b7280; font-size: .75rem; text-align: center; margin-top: 1rem; }
|
||||
.meth { display: inline-block; font-size: .72rem; padding: .1rem .45rem; background: #eef2ff; color: #4338ca; border-radius: 4px; margin: .1rem .15rem 0 0; font-family: ui-monospace, monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="hd">
|
||||
<h1>SIP / VoIP, {{.Domain}}</h1>
|
||||
<span class="badge {{.StatusClass}}">{{.StatusLabel}}</span>
|
||||
<div class="chips" style="margin-top:.45rem">
|
||||
<span class="chip {{if .WorkingUDP}}ok{{else}}fail{{end}}">{{if .WorkingUDP}}✓{{else}}✗{{end}} UDP</span>
|
||||
<span class="chip {{if .WorkingTCP}}ok{{else}}fail{{end}}">{{if .WorkingTCP}}✓{{else}}✗{{end}} TCP</span>
|
||||
<span class="chip {{if .WorkingTLS}}ok{{else}}fail{{end}}">{{if .WorkingTLS}}✓{{else}}✗{{end}} TLS</span>
|
||||
<span class="chip {{if .HasIPv4}}ok{{end}}">IPv4</span>
|
||||
<span class="chip {{if .HasIPv6}}ok{{end}}">IPv6</span>
|
||||
</div>
|
||||
{{if .FallbackProbed}}<div class="note">No SIP SRV records were published. Probed the bare domain on default ports.</div>{{end}}
|
||||
{{if .RunAt}}<div class="note">Checked {{.RunAt}}</div>{{end}}
|
||||
</div>
|
||||
|
||||
{{if .HasIssues}}
|
||||
<div class="section">
|
||||
<h2>What to fix</h2>
|
||||
{{range .Fixes}}
|
||||
<div class="fix {{.Severity}}">
|
||||
<div class="code">{{.Code}}{{if .Endpoint}} · {{.Endpoint}}{{end}}</div>
|
||||
<div class="msg">{{.Message}}</div>
|
||||
{{if .Fix}}<div class="how">→ {{.Fix}}</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .NAPTR}}
|
||||
<div class="section">
|
||||
<h2>NAPTR ({{len .NAPTR}})</h2>
|
||||
<table>
|
||||
<tr><th>Order</th><th>Pref</th><th>Flags</th><th>Service</th><th>Replacement</th></tr>
|
||||
{{range .NAPTR}}
|
||||
<tr>
|
||||
<td>{{.Order}}</td>
|
||||
<td>{{.Preference}}</td>
|
||||
<td><code>{{.Flags}}</code></td>
|
||||
<td><code>{{.Service}}</code></td>
|
||||
<td>{{if .Replacement}}<code>{{.Replacement}}</code>{{else}}<span class="note">.</span>{{end}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .SRV}}
|
||||
<div class="section">
|
||||
<h2>SRV records ({{len .SRV}})</h2>
|
||||
<table>
|
||||
<tr><th>Prefix</th><th>Target</th><th>Port</th><th>Prio/Weight</th><th>A / AAAA</th></tr>
|
||||
{{range .SRV}}
|
||||
<tr>
|
||||
<td><code>{{.Prefix}}</code></td>
|
||||
<td><code>{{.Target}}</code></td>
|
||||
<td>{{.Port}}</td>
|
||||
<td>{{.Priority}} / {{.Weight}}</td>
|
||||
<td>
|
||||
{{range .IPv4}}<code>{{.}}</code> {{end}}
|
||||
{{range .IPv6}}<code>{{.}}</code> {{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .Endpoints}}
|
||||
<div class="section">
|
||||
<h2>Endpoint probes ({{len .Endpoints}})</h2>
|
||||
{{range .Endpoints}}
|
||||
<details{{if not .OK}} open{{end}}>
|
||||
<summary>
|
||||
<span class="conn-head">{{.TransportTag}} · {{.Address}}</span>
|
||||
<span class="badge {{.StatusClass}}">{{.StatusLabel}}</span>
|
||||
</summary>
|
||||
<div class="details-body">
|
||||
<dl>
|
||||
<dt>Target</dt><dd><code>{{.Target}}:{{.Port}}</code>{{if .SRVPrefix}} <span class="note">({{.SRVPrefix}})</span>{{end}}</dd>
|
||||
<dt>Reachable</dt>
|
||||
<dd>
|
||||
{{if .Reachable}}<span class="check-ok">✓ connected</span>{{else}}<span class="check-fail">✗ {{if .ReachableErr}}{{.ReachableErr}}{{else}}unreachable{{end}}</span>{{end}}
|
||||
</dd>
|
||||
{{if or .TLSVersion .TLSCipher}}
|
||||
<dt>TLS</dt><dd>{{.TLSVersion}}{{if and .TLSVersion .TLSCipher}} · {{end}}{{.TLSCipher}}</dd>
|
||||
{{end}}
|
||||
{{if .OptionsSent}}
|
||||
<dt>OPTIONS</dt>
|
||||
<dd>
|
||||
{{if .OptionsStatus}}<span class="badge {{if .OK}}ok{{else}}warn{{end}}">{{.OptionsStatus}}</span>{{else}}<span class="badge fail">no reply</span>{{end}}
|
||||
{{if .OptionsRTTMs}} · {{.OptionsRTTMs}} ms{{end}}
|
||||
</dd>
|
||||
{{end}}
|
||||
{{if .ServerHeader}}<dt>Server</dt><dd><code>{{.ServerHeader}}</code></dd>{{end}}
|
||||
{{if .UserAgent}}<dt>User-Agent</dt><dd><code>{{.UserAgent}}</code></dd>{{end}}
|
||||
{{if .ContactURI}}<dt>Contact</dt><dd><code>{{.ContactURI}}</code></dd>{{end}}
|
||||
{{if .AllowMethods}}
|
||||
<dt>Allow</dt>
|
||||
<dd>{{range .AllowMethods}}<span class="meth">{{.}}</span>{{end}}</dd>
|
||||
{{end}}
|
||||
{{if .TLSPosture}}
|
||||
<dt>TLS posture</dt>
|
||||
<dd>
|
||||
{{if .TLSPosture.ChainValid}}
|
||||
{{if deref .TLSPosture.ChainValid}}<span class="check-ok">✓ chain valid</span>{{else}}<span class="check-fail">✗ chain invalid</span>{{end}}
|
||||
{{end}}
|
||||
{{if .TLSPosture.HostnameMatch}}
|
||||
· {{if deref .TLSPosture.HostnameMatch}}<span class="check-ok">✓ hostname match</span>{{else}}<span class="check-fail">✗ hostname mismatch</span>{{end}}
|
||||
{{end}}
|
||||
{{if not .TLSPosture.NotAfter.IsZero}}
|
||||
· expires <code>{{.TLSPosture.NotAfter.Format "2006-01-02"}}</code>
|
||||
{{end}}
|
||||
{{if not .TLSPosture.CheckedAt.IsZero}}
|
||||
<div class="note">TLS checked {{.TLSPosture.CheckedAt.Format "2006-01-02 15:04 MST"}}</div>
|
||||
{{end}}
|
||||
{{range .TLSPosture.Issues}}
|
||||
<div class="fix {{.Severity}}" style="margin-top:.3rem">
|
||||
<div class="code">{{.Code}}</div>
|
||||
<div class="msg">{{.Message}}</div>
|
||||
{{if .Fix}}<div class="how">→ {{.Fix}}</div>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
</dd>
|
||||
{{end}}
|
||||
<dt>Duration</dt><dd>{{.ElapsedMS}} ms</dd>
|
||||
{{if .Error}}<dt>Error</dt><dd><span class="check-fail">{{.Error}}</span></dd>{{end}}
|
||||
</dl>
|
||||
</div>
|
||||
</details>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<p class="footer">{{if .HasTLSPosture}}TLS posture above comes from the TLS checker on the same endpoints.{{else}}Run the TLS checker on this domain to see chain / SAN / expiry per SIPS endpoint.{{end}}</p>
|
||||
|
||||
</body>
|
||||
</html>`))
|
||||
|
||||
// ─── Rendering ──────────────────────────────────────────────────────
|
||||
|
||||
// GetHTMLReport implements sdk.CheckerHTMLReporter. Related TLS
|
||||
// observations (tls_probes) are folded in so cert posture surfaces on
|
||||
// the SIP page directly.
|
||||
func (p *sipProvider) GetHTMLReport(rctx sdk.ReportContext) (string, error) {
|
||||
var d SIPData
|
||||
if err := json.Unmarshal(rctx.Data(), &d); err != nil {
|
||||
return "", fmt.Errorf("unmarshal sip observation: %w", err)
|
||||
}
|
||||
view := buildReportData(&d, rctx.Related(TLSRelatedKey), rctx.States())
|
||||
var buf strings.Builder
|
||||
if err := reportTpl.Execute(&buf, view); err != nil {
|
||||
return "", fmt.Errorf("render sip report: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func buildReportData(d *SIPData, related []sdk.RelatedObservation, states []sdk.CheckState) reportData {
|
||||
tlsByAddr := indexTLSByAddress(related)
|
||||
|
||||
// Coverage is a pure aggregation of the raw endpoint probes: it
|
||||
// powers the header chips and is NOT a judgment.
|
||||
cov := computeCoverageView(d)
|
||||
|
||||
view := reportData{
|
||||
Domain: d.Domain,
|
||||
RunAt: d.RunAt,
|
||||
FallbackProbed: d.SRV.FallbackProbed,
|
||||
HasIPv4: cov.HasIPv4,
|
||||
HasIPv6: cov.HasIPv6,
|
||||
WorkingUDP: cov.WorkingUDP,
|
||||
WorkingTCP: cov.WorkingTCP,
|
||||
WorkingTLS: cov.WorkingTLS,
|
||||
HasTLSPosture: len(tlsByAddr) > 0,
|
||||
}
|
||||
|
||||
// Hint/fix section reads ONLY from ctx.States(): Message, Meta["fix"],
|
||||
// Status. When no states are supplied (data-only rendering path), we
|
||||
// skip the section entirely and show a neutral status based on the
|
||||
// raw probe facts.
|
||||
view.Fixes, view.StatusLabel, view.StatusClass = buildFixesFromStates(states)
|
||||
view.HasIssues = len(view.Fixes) > 0
|
||||
|
||||
if len(states) == 0 {
|
||||
// Data-only view: no judgment, no hint block. Status reflects
|
||||
// raw reachability only.
|
||||
view.HasIssues = false
|
||||
if len(d.Endpoints) == 0 {
|
||||
view.StatusLabel = "UNKNOWN"
|
||||
view.StatusClass = "muted"
|
||||
} else if cov.AnyWorking {
|
||||
view.StatusLabel = "OK"
|
||||
view.StatusClass = "ok"
|
||||
} else {
|
||||
view.StatusLabel = "FAIL"
|
||||
view.StatusClass = "fail"
|
||||
}
|
||||
}
|
||||
|
||||
view.NAPTR = append(view.NAPTR, d.NAPTR...)
|
||||
|
||||
addSRV := func(prefix string, records []SRVRecord) {
|
||||
for _, r := range records {
|
||||
view.SRV = append(view.SRV, reportSRVEntry{
|
||||
Prefix: prefix, Target: r.Target, Port: r.Port,
|
||||
Priority: r.Priority, Weight: r.Weight,
|
||||
IPv4: r.IPv4, IPv6: r.IPv6,
|
||||
})
|
||||
}
|
||||
}
|
||||
addSRV("_sip._udp", d.SRV.UDP)
|
||||
addSRV("_sip._tcp", d.SRV.TCP)
|
||||
addSRV("_sips._tcp", d.SRV.SIPS)
|
||||
|
||||
for _, ep := range d.Endpoints {
|
||||
re := reportEndpoint{
|
||||
Transport: string(ep.Transport),
|
||||
TransportTag: transportTag(ep.Transport),
|
||||
SRVPrefix: ep.SRVPrefix,
|
||||
Target: ep.Target,
|
||||
Port: ep.Port,
|
||||
Address: ep.Address,
|
||||
IsIPv6: ep.IsIPv6,
|
||||
Reachable: ep.Reachable,
|
||||
ReachableErr: ep.ReachableErr,
|
||||
TLSVersion: ep.TLSVersion,
|
||||
TLSCipher: ep.TLSCipher,
|
||||
OptionsSent: ep.OptionsSent,
|
||||
OptionsStatus: ep.OptionsStatus,
|
||||
OptionsRTTMs: ep.OptionsRTTMs,
|
||||
ServerHeader: ep.ServerHeader,
|
||||
UserAgent: ep.UserAgent,
|
||||
AllowMethods: ep.AllowMethods,
|
||||
ContactURI: ep.ContactURI,
|
||||
ElapsedMS: ep.ElapsedMS,
|
||||
Error: ep.Error,
|
||||
OK: ep.OK(),
|
||||
}
|
||||
if re.OK {
|
||||
re.StatusLabel = "OK"
|
||||
re.StatusClass = "ok"
|
||||
} else if ep.Reachable {
|
||||
re.StatusLabel = "partial"
|
||||
re.StatusClass = "warn"
|
||||
} else {
|
||||
re.StatusLabel = "unreachable"
|
||||
re.StatusClass = "fail"
|
||||
}
|
||||
if meta, hit := tlsByAddr[ep.Address]; hit {
|
||||
re.TLSPosture = meta
|
||||
} else if meta, hit := tlsByAddr[endpointKey(ep.Target, ep.Port)]; hit {
|
||||
re.TLSPosture = meta
|
||||
}
|
||||
view.Endpoints = append(view.Endpoints, re)
|
||||
}
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
func transportTag(t Transport) string {
|
||||
switch t {
|
||||
case TransportUDP:
|
||||
return "SIP/UDP"
|
||||
case TransportTCP:
|
||||
return "SIP/TCP"
|
||||
case TransportTLS:
|
||||
return "SIPS/TLS"
|
||||
}
|
||||
return string(t)
|
||||
}
|
||||
|
||||
func endpointKey(host string, port uint16) string {
|
||||
return net.JoinHostPort(host, strconv.Itoa(int(port)))
|
||||
}
|
||||
|
||||
// buildFixesFromStates projects the rule-produced CheckStates onto the
|
||||
// report's hint/fix list. It reads ONLY from sdk.CheckState fields:
|
||||
// Message, Meta["fix"], Status, Code, Subject. No re-derivation from raw
|
||||
// observation happens here.
|
||||
//
|
||||
// Returns the (sorted) fixes plus the overall status label/class. When
|
||||
// states is empty, callers skip the hint section entirely; the neutral
|
||||
// status returned here ("OK") is meant to be overridden by the caller in
|
||||
// that data-only path.
|
||||
func buildFixesFromStates(states []sdk.CheckState) ([]reportFix, string, string) {
|
||||
var fixes []reportFix
|
||||
worst := sdk.StatusOK
|
||||
for _, s := range states {
|
||||
// Only surface states that carry a finding (non-OK, non-Unknown).
|
||||
switch s.Status {
|
||||
case sdk.StatusCrit, sdk.StatusWarn, sdk.StatusInfo, sdk.StatusError:
|
||||
default:
|
||||
continue
|
||||
}
|
||||
sev := statusToSeverity(s.Status)
|
||||
fix, _ := s.Meta["fix"].(string)
|
||||
fixes = append(fixes, reportFix{
|
||||
Severity: sev,
|
||||
Code: s.Code,
|
||||
Message: s.Message,
|
||||
Fix: fix,
|
||||
Endpoint: s.Subject,
|
||||
})
|
||||
if statusRank(s.Status) > statusRank(worst) {
|
||||
worst = s.Status
|
||||
}
|
||||
}
|
||||
|
||||
sevRank := func(s string) int {
|
||||
switch s {
|
||||
case SeverityCrit:
|
||||
return 0
|
||||
case SeverityWarn:
|
||||
return 1
|
||||
default:
|
||||
return 2
|
||||
}
|
||||
}
|
||||
slices.SortStableFunc(fixes, func(a, b reportFix) int {
|
||||
return sevRank(a.Severity) - sevRank(b.Severity)
|
||||
})
|
||||
|
||||
var label, class string
|
||||
switch {
|
||||
case len(fixes) == 0:
|
||||
label, class = "OK", "ok"
|
||||
case worst == sdk.StatusCrit || worst == sdk.StatusError:
|
||||
label, class = "FAIL", "fail"
|
||||
case worst == sdk.StatusWarn:
|
||||
label, class = "WARN", "warn"
|
||||
default:
|
||||
label, class = "INFO", "muted"
|
||||
}
|
||||
return fixes, label, class
|
||||
}
|
||||
|
||||
func statusToSeverity(s sdk.Status) string {
|
||||
switch s {
|
||||
case sdk.StatusCrit, sdk.StatusError:
|
||||
return SeverityCrit
|
||||
case sdk.StatusWarn:
|
||||
return SeverityWarn
|
||||
case sdk.StatusInfo:
|
||||
return SeverityInfo
|
||||
default:
|
||||
return SeverityInfo
|
||||
}
|
||||
}
|
||||
|
||||
func statusRank(s sdk.Status) int {
|
||||
switch s {
|
||||
case sdk.StatusCrit, sdk.StatusError:
|
||||
return 3
|
||||
case sdk.StatusWarn:
|
||||
return 2
|
||||
case sdk.StatusInfo:
|
||||
return 1
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// indexTLSByAddress returns a map keyed by "host:port" pointing at a
|
||||
// reportTLSPosture, so the template can match a related observation to
|
||||
// the right endpoint.
|
||||
func indexTLSByAddress(related []sdk.RelatedObservation) map[string]*reportTLSPosture {
|
||||
out := map[string]*reportTLSPosture{}
|
||||
for _, r := range related {
|
||||
v := parseTLSRelated(r)
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
addr := v.address()
|
||||
if addr == "" {
|
||||
continue
|
||||
}
|
||||
posture := &reportTLSPosture{
|
||||
CheckedAt: r.CollectedAt,
|
||||
ChainValid: v.ChainValid,
|
||||
HostnameMatch: v.HostnameMatch,
|
||||
NotAfter: v.NotAfter,
|
||||
TLSVersion: v.TLSVersion,
|
||||
}
|
||||
for _, is := range v.Issues {
|
||||
sev := strings.ToLower(is.Severity)
|
||||
if sev != SeverityCrit && sev != SeverityWarn && sev != SeverityInfo {
|
||||
continue
|
||||
}
|
||||
posture.Issues = append(posture.Issues, reportFix{
|
||||
Severity: sev,
|
||||
Code: is.Code,
|
||||
Message: is.Message,
|
||||
Fix: is.Fix,
|
||||
})
|
||||
}
|
||||
out[addr] = posture
|
||||
}
|
||||
return out
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue