577 lines
19 KiB
Go
577 lines
19 KiB
Go
package checker
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
sdk "git.happydns.org/checker-sdk-go/checker"
|
|
)
|
|
|
|
type reportFix struct {
|
|
Severity string
|
|
Code string
|
|
Message string
|
|
Fix string
|
|
Endpoint string
|
|
}
|
|
|
|
type reportEndpoint struct {
|
|
Mode string
|
|
ModeLabel string
|
|
SRVPrefix string
|
|
Target string
|
|
Port uint16
|
|
Address string
|
|
IsIPv6 bool
|
|
TCPConnected bool
|
|
StartTLSOffered bool
|
|
StartTLSUpgraded bool
|
|
TLSEstablished bool
|
|
TLSVersion string
|
|
TLSCipher string
|
|
RootDSERead bool
|
|
SupportedLDAPVersion []string
|
|
SupportedSASLMechanisms []string
|
|
SupportedControl []string
|
|
SupportedExtension []string
|
|
NamingContexts []string
|
|
VendorName string
|
|
VendorVersion string
|
|
AnonymousBindAllowed bool
|
|
AnonymousSearchAllowed bool
|
|
PlaintextBindTested bool
|
|
PlaintextBindAccepted bool
|
|
BindAttempted bool
|
|
BindOK bool
|
|
BindError string
|
|
BaseReadAttempted bool
|
|
BaseReadOK bool
|
|
BaseReadEntries int
|
|
BaseReadError string
|
|
ElapsedMS int64
|
|
Error string
|
|
|
|
// TLS posture (from a related tls_probes observation, when available).
|
|
TLSPosture *reportTLSPosture
|
|
|
|
// Rendering helpers.
|
|
AnyFail bool
|
|
StatusLabel string
|
|
StatusClass string
|
|
}
|
|
|
|
type reportTLSPosture struct {
|
|
CheckedAt time.Time
|
|
ChainValid *bool
|
|
HostnameMatch *bool
|
|
NotAfter time.Time
|
|
Issues []reportFix
|
|
}
|
|
|
|
type reportSRVEntry struct {
|
|
Prefix string
|
|
Target string
|
|
Port uint16
|
|
Priority uint16
|
|
Weight uint16
|
|
IPv4 []string
|
|
IPv6 []string
|
|
}
|
|
|
|
type reportData struct {
|
|
Domain string
|
|
BaseDN string
|
|
RunAt string
|
|
StatusLabel string
|
|
StatusClass string
|
|
HasIssues bool
|
|
Fixes []reportFix
|
|
SRV []reportSRVEntry
|
|
FallbackProbed bool
|
|
Endpoints []reportEndpoint
|
|
HasIPv4 bool
|
|
HasIPv6 bool
|
|
EncryptedReachable bool
|
|
PlainOnlyReachable bool
|
|
BindTested bool
|
|
HasTLSPosture bool
|
|
}
|
|
|
|
var reportTpl = template.Must(template.New("ldap").Funcs(template.FuncMap{
|
|
"hasPrefix": strings.HasPrefix,
|
|
"deref": func(b *bool) bool { return b != nil && *b },
|
|
"join": func(sep string, list []string) string { return strings.Join(list, sep) },
|
|
"upper": strings.ToUpper,
|
|
}).Parse(`<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LDAP 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; }
|
|
|
|
.badge {
|
|
display: inline-flex; align-items: center;
|
|
padding: .2em .65em; border-radius: 9999px;
|
|
font-size: .78rem; font-weight: 700; letter-spacing: .02em;
|
|
}
|
|
.ok { background: #d1fae5; color: #065f46; }
|
|
.warn { background: #fef3c7; color: #92400e; }
|
|
.fail { background: #fee2e2; color: #991b1b; }
|
|
.muted { background: #e5e7eb; color: #374151; }
|
|
.info { background: #dbeafe; color: #1e40af; }
|
|
|
|
.meta { color: #6b7280; font-size: .82rem; margin-top: .35rem; }
|
|
|
|
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); }
|
|
.conn-addr { font-weight: 600; flex: 1; font-size: .9rem; font-family: ui-monospace, monospace; }
|
|
.details-body { padding: .6rem 1rem .85rem; border-top: 1px solid #f3f4f6; }
|
|
|
|
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; }
|
|
|
|
.fix {
|
|
border-left: 3px solid #dc2626;
|
|
padding: .5rem .75rem; margin-bottom: .5rem;
|
|
background: #fef2f2; border-radius: 0 6px 6px 0;
|
|
}
|
|
.fix.warn { border-color: #f59e0b; background: #fffbeb; }
|
|
.fix.info { border-color: #3b82f6; background: #eff6ff; }
|
|
.fix .code { font-family: ui-monospace, monospace; font-size: .75rem; color: #6b7280; }
|
|
.fix .msg { font-weight: 600; margin: .1rem 0 .2rem; }
|
|
.fix .how { font-size: .88rem; }
|
|
.fix .ep { font-size: .78rem; color: #6b7280; font-family: ui-monospace, monospace; }
|
|
|
|
.chiprow { display: flex; flex-wrap: wrap; gap: .25rem; }
|
|
.chip {
|
|
display: inline-block; padding: .12em .5em;
|
|
background: #e0e7ff; color: #3730a3;
|
|
border-radius: 4px; font-size: .78rem; font-family: ui-monospace, monospace;
|
|
}
|
|
.chip.plain { background: #fee2e2; color: #991b1b; }
|
|
.chip.scram { background: #d1fae5; color: #065f46; }
|
|
.chip.strong { background: #d1fae5; color: #065f46; }
|
|
|
|
.kv { display: grid; grid-template-columns: auto 1fr; gap: .3rem 1rem; font-size: .86rem; }
|
|
.kv dt { color: #6b7280; }
|
|
.kv dd { margin: 0; }
|
|
|
|
.note { color: #6b7280; font-size: .85rem; }
|
|
.footer { color: #6b7280; font-size: .78rem; text-align: center; margin-top: 1rem; padding-bottom: 2rem; }
|
|
.check-ok { color: #059669; }
|
|
.check-fail { color: #dc2626; }
|
|
.check-warn { color: #b45309; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="hd">
|
|
<h1>LDAP -- <code>{{.Domain}}</code></h1>
|
|
<span class="badge {{.StatusClass}}">{{.StatusLabel}}</span>
|
|
<div class="meta">
|
|
{{if .EncryptedReachable}}<span class="badge ok" style="margin-right:.25rem">encrypted OK</span>{{else}}<span class="badge fail" style="margin-right:.25rem">no encryption</span>{{end}}
|
|
{{if .HasIPv4}}<span class="badge muted">IPv4</span>{{end}}
|
|
{{if .HasIPv6}}<span class="badge muted">IPv6</span>{{end}}
|
|
{{if .BindTested}}<span class="badge info" style="margin-left:.25rem">bind test</span>{{end}}
|
|
</div>
|
|
<div class="meta">Checked {{.RunAt}}{{if .BaseDN}} · base <code>{{.BaseDN}}</code>{{end}}</div>
|
|
</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}}
|
|
|
|
<div class="section">
|
|
<h2>DNS / SRV</h2>
|
|
{{if .FallbackProbed}}
|
|
<p class="note">No SRV records published -- fell back to probing the bare domain on default ports 389 / 636.</p>
|
|
{{else if .SRV}}
|
|
<table>
|
|
<tr><th>Record</th><th>Target</th><th>Port</th><th>Prio/Weight</th><th>IPv4</th><th>IPv6</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}}</td>
|
|
<td>{{range .IPv6}}<code>{{.}}</code> {{end}}</td>
|
|
</tr>
|
|
{{end}}
|
|
</table>
|
|
{{else}}
|
|
<p class="note">No SRV records found.</p>
|
|
{{end}}
|
|
</div>
|
|
|
|
{{if .Endpoints}}
|
|
<div class="section">
|
|
<h2>Endpoints ({{len .Endpoints}})</h2>
|
|
{{range .Endpoints}}
|
|
<details{{if .AnyFail}} open{{end}}>
|
|
<summary>
|
|
<span class="conn-addr">{{.ModeLabel}} · {{.Address}}</span>
|
|
<span class="badge {{.StatusClass}}">{{.StatusLabel}}</span>
|
|
</summary>
|
|
<div class="details-body">
|
|
<dl class="kv">
|
|
{{if .SRVPrefix}}<dt>SRV</dt><dd><code>{{.SRVPrefix}}</code> → <code>{{.Target}}:{{.Port}}</code></dd>{{end}}
|
|
<dt>Family</dt><dd>{{if .IsIPv6}}IPv6{{else}}IPv4{{end}}</dd>
|
|
<dt>TCP</dt><dd>{{if .TCPConnected}}<span class="check-ok">✓ connected</span>{{else}}<span class="check-fail">✗ failed</span>{{end}}</dd>
|
|
|
|
{{if eq .Mode "ldap"}}
|
|
<dt>StartTLS</dt><dd>
|
|
{{if .StartTLSOffered}}<span class="check-ok">✓ offered</span>{{else}}<span class="check-fail">✗ not offered</span>{{end}}
|
|
{{if .StartTLSUpgraded}} · <span class="check-ok">upgraded</span>{{else if .StartTLSOffered}} · <span class="check-fail">upgrade failed</span>{{end}}
|
|
</dd>
|
|
<dt>Cleartext bind</dt><dd>
|
|
{{if not .PlaintextBindTested}}<span class="note">not tested</span>
|
|
{{else if .PlaintextBindAccepted}}<span class="check-fail">✗ accepted (insecure)</span>
|
|
{{else}}<span class="check-ok">✓ refused (confidentiality required)</span>{{end}}
|
|
</dd>
|
|
{{end}}
|
|
|
|
<dt>TLS</dt><dd>
|
|
{{if .TLSEstablished}}<span class="check-ok">✓ {{.TLSVersion}}{{if .TLSCipher}} — {{.TLSCipher}}{{end}}</span>
|
|
{{else}}<span class="check-fail">✗ plaintext</span>{{end}}
|
|
</dd>
|
|
|
|
<dt>RootDSE</dt><dd>
|
|
{{if .RootDSERead}}<span class="check-ok">✓ read</span>
|
|
{{else}}<span class="check-warn">✗ unreadable</span>{{end}}
|
|
{{if .VendorName}} · <code>{{.VendorName}}{{if .VendorVersion}} {{.VendorVersion}}{{end}}</code>{{end}}
|
|
{{if .SupportedLDAPVersion}} · LDAPv{{join "," .SupportedLDAPVersion}}{{end}}
|
|
</dd>
|
|
|
|
{{if .NamingContexts}}
|
|
<dt>Naming contexts</dt><dd>
|
|
<div class="chiprow">{{range .NamingContexts}}<span class="chip"><code>{{.}}</code></span>{{end}}</div>
|
|
</dd>
|
|
{{end}}
|
|
|
|
{{if .SupportedSASLMechanisms}}
|
|
<dt>SASL</dt><dd>
|
|
<div class="chiprow">
|
|
{{range .SupportedSASLMechanisms}}
|
|
{{$u := upper .}}
|
|
{{if or (eq $u "PLAIN") (eq $u "LOGIN")}}<span class="chip plain">{{.}}</span>
|
|
{{else if or (hasPrefix $u "SCRAM-") (eq $u "EXTERNAL") (eq $u "GSSAPI") (eq $u "GSS-SPNEGO")}}<span class="chip strong">{{.}}</span>
|
|
{{else}}<span class="chip">{{.}}</span>{{end}}
|
|
{{end}}
|
|
</div>
|
|
</dd>
|
|
{{end}}
|
|
|
|
<dt>Anonymous</dt><dd>
|
|
{{if .AnonymousBindAllowed}}<span class="check-warn">bind allowed</span>
|
|
{{else}}<span class="check-ok">✓ bind refused</span>{{end}}
|
|
{{if .AnonymousSearchAllowed}} · <span class="check-fail">search allowed (DIT enumerable)</span>{{end}}
|
|
</dd>
|
|
|
|
{{if .BindAttempted}}
|
|
<dt>Bind as DN</dt><dd>
|
|
{{if .BindOK}}<span class="check-ok">✓ succeeded</span>
|
|
{{else}}<span class="check-fail">✗ {{.BindError}}</span>{{end}}
|
|
</dd>
|
|
{{end}}
|
|
{{if .BaseReadAttempted}}
|
|
<dt>Base read</dt><dd>
|
|
{{if .BaseReadOK}}<span class="check-ok">✓ {{.BaseReadEntries}} entry/entries</span>
|
|
{{else}}<span class="check-fail">✗ {{.BaseReadError}}</span>{{end}}
|
|
</dd>
|
|
{{end}}
|
|
|
|
{{with .TLSPosture}}
|
|
<dt>TLS cert</dt><dd>
|
|
{{if .ChainValid}}
|
|
{{if deref .ChainValid}}<span class="check-ok">✓ chain valid</span>{{else}}<span class="check-fail">✗ chain invalid</span>{{end}}
|
|
{{end}}
|
|
{{if .HostnameMatch}}
|
|
· {{if deref .HostnameMatch}}<span class="check-ok">✓ hostname match</span>{{else}}<span class="check-fail">✗ hostname mismatch</span>{{end}}
|
|
{{end}}
|
|
{{if not .NotAfter.IsZero}}
|
|
· expires <code>{{.NotAfter.Format "2006-01-02"}}</code>
|
|
{{end}}
|
|
{{if not .CheckedAt.IsZero}}
|
|
<div class="note">TLS checked {{.CheckedAt.Format "2006-01-02 15:04 MST"}}</div>
|
|
{{end}}
|
|
{{range .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}}Certificate posture above comes from the TLS checker, which probed the same endpoints after we discovered them.{{else}}For certificate chain, SAN match, expiry, and cipher posture, run the TLS checker on the same ports.{{end}}</p>
|
|
|
|
</body>
|
|
</html>`))
|
|
|
|
// GetHTMLReport implements sdk.CheckerHTMLReporter. It folds in related TLS
|
|
// observations so the LDAP service page shows cert posture directly.
|
|
func (p *ldapProvider) GetHTMLReport(rctx sdk.ReportContext) (string, error) {
|
|
var d LDAPData
|
|
if err := json.Unmarshal(rctx.Data(), &d); err != nil {
|
|
return "", fmt.Errorf("unmarshal ldap observation: %w", err)
|
|
}
|
|
view := buildReportData(&d, rctx.Related(TLSRelatedKey))
|
|
return renderReport(view)
|
|
}
|
|
|
|
func renderReport(view reportData) (string, error) {
|
|
var buf strings.Builder
|
|
if err := reportTpl.Execute(&buf, view); err != nil {
|
|
return "", fmt.Errorf("render ldap report: %w", err)
|
|
}
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func buildReportData(d *LDAPData, related []sdk.RelatedObservation) reportData {
|
|
tlsIssues := tlsIssuesFromRelated(related)
|
|
tlsByAddr := indexTLSByAddress(related)
|
|
|
|
allIssues := append([]Issue(nil), d.Issues...)
|
|
allIssues = append(allIssues, tlsIssues...)
|
|
|
|
view := reportData{
|
|
Domain: d.Domain,
|
|
BaseDN: d.BaseDN,
|
|
RunAt: d.RunAt,
|
|
FallbackProbed: d.SRV.FallbackProbed,
|
|
HasIPv4: d.Coverage.HasIPv4,
|
|
HasIPv6: d.Coverage.HasIPv6,
|
|
EncryptedReachable: d.Coverage.EncryptedReachable,
|
|
PlainOnlyReachable: d.Coverage.PlainOnlyReachable,
|
|
BindTested: d.BindTested,
|
|
HasIssues: len(allIssues) > 0,
|
|
HasTLSPosture: len(tlsByAddr) > 0,
|
|
}
|
|
|
|
// Status banner.
|
|
worst := ""
|
|
for _, is := range allIssues {
|
|
if is.Severity == SeverityCrit {
|
|
worst = SeverityCrit
|
|
break
|
|
}
|
|
if is.Severity == SeverityWarn {
|
|
worst = SeverityWarn
|
|
} else if worst == "" && is.Severity == SeverityInfo {
|
|
worst = SeverityInfo
|
|
}
|
|
}
|
|
if len(allIssues) == 0 {
|
|
view.StatusLabel = "OK"
|
|
view.StatusClass = "ok"
|
|
} else {
|
|
switch worst {
|
|
case SeverityCrit:
|
|
view.StatusLabel = "FAIL"
|
|
view.StatusClass = "fail"
|
|
case SeverityWarn:
|
|
view.StatusLabel = "WARN"
|
|
view.StatusClass = "warn"
|
|
default:
|
|
view.StatusLabel = "INFO"
|
|
view.StatusClass = "info"
|
|
}
|
|
}
|
|
|
|
// Fix list: sort crit → warn → info, preserving order within each severity.
|
|
sevRank := func(s string) int {
|
|
switch s {
|
|
case SeverityCrit:
|
|
return 0
|
|
case SeverityWarn:
|
|
return 1
|
|
default:
|
|
return 2
|
|
}
|
|
}
|
|
sort.SliceStable(allIssues, func(i, j int) bool { return sevRank(allIssues[i].Severity) < sevRank(allIssues[j].Severity) })
|
|
for _, is := range allIssues {
|
|
view.Fixes = append(view.Fixes, reportFix{
|
|
Severity: is.Severity,
|
|
Code: is.Code,
|
|
Message: is.Message,
|
|
Fix: is.Fix,
|
|
Endpoint: is.Endpoint,
|
|
})
|
|
}
|
|
|
|
// SRV rows.
|
|
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("_ldap._tcp", d.SRV.LDAP)
|
|
addSRV("_ldaps._tcp", d.SRV.LDAPS)
|
|
|
|
// Endpoint rows.
|
|
for _, ep := range d.Endpoints {
|
|
re := reportEndpoint{
|
|
Mode: string(ep.Mode),
|
|
ModeLabel: modeLabel(ep.Mode),
|
|
SRVPrefix: ep.SRVPrefix,
|
|
Target: ep.Target,
|
|
Port: ep.Port,
|
|
Address: ep.Address,
|
|
IsIPv6: ep.IsIPv6,
|
|
TCPConnected: ep.TCPConnected,
|
|
StartTLSOffered: ep.StartTLSOffered,
|
|
StartTLSUpgraded: ep.StartTLSUpgraded,
|
|
TLSEstablished: ep.TLSEstablished,
|
|
TLSVersion: ep.TLSVersion,
|
|
TLSCipher: ep.TLSCipher,
|
|
RootDSERead: ep.RootDSERead,
|
|
SupportedLDAPVersion: ep.SupportedLDAPVersion,
|
|
SupportedSASLMechanisms: ep.SupportedSASLMechanisms,
|
|
SupportedControl: ep.SupportedControl,
|
|
SupportedExtension: ep.SupportedExtension,
|
|
NamingContexts: ep.NamingContexts,
|
|
VendorName: ep.VendorName,
|
|
VendorVersion: ep.VendorVersion,
|
|
AnonymousBindAllowed: ep.AnonymousBindAllowed,
|
|
AnonymousSearchAllowed: ep.AnonymousSearchAllowed,
|
|
PlaintextBindTested: ep.PlaintextBindTested,
|
|
PlaintextBindAccepted: ep.PlaintextBindAccepted,
|
|
BindAttempted: ep.BindAttempted,
|
|
BindOK: ep.BindOK,
|
|
BindError: ep.BindError,
|
|
BaseReadAttempted: ep.BaseReadAttempted,
|
|
BaseReadOK: ep.BaseReadOK,
|
|
BaseReadEntries: ep.BaseReadEntries,
|
|
BaseReadError: ep.BaseReadError,
|
|
ElapsedMS: ep.ElapsedMS,
|
|
Error: ep.Error,
|
|
}
|
|
if meta, hit := tlsByAddr[ep.Address]; hit {
|
|
re.TLSPosture = meta
|
|
} else if meta, hit := tlsByAddr[endpointKey(ep.Target, ep.Port)]; hit {
|
|
re.TLSPosture = meta
|
|
}
|
|
ok := ep.TCPConnected && ep.TLSEstablished
|
|
if ep.Mode == ModePlain {
|
|
ok = ok && ep.StartTLSUpgraded
|
|
}
|
|
re.AnyFail = !ok
|
|
if ok {
|
|
re.StatusLabel = "OK"
|
|
re.StatusClass = "ok"
|
|
} else if ep.TCPConnected {
|
|
re.StatusLabel = "partial"
|
|
re.StatusClass = "warn"
|
|
} else {
|
|
re.StatusLabel = "unreachable"
|
|
re.StatusClass = "fail"
|
|
}
|
|
view.Endpoints = append(view.Endpoints, re)
|
|
}
|
|
|
|
return view
|
|
}
|
|
|
|
func modeLabel(m LDAPMode) string {
|
|
switch m {
|
|
case ModePlain:
|
|
return "ldap"
|
|
case ModeLDAPS:
|
|
return "ldaps"
|
|
default:
|
|
return string(m)
|
|
}
|
|
}
|
|
|
|
// indexTLSByAddress returns a map keyed by "host:port" pointing at a
|
|
// reportTLSPosture. This lets the template 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,
|
|
}
|
|
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
|
|
}
|