Initial commit
This commit is contained in:
commit
beca2fd7eb
21 changed files with 2698 additions and 0 deletions
182
checker/types.go
Normal file
182
checker/types.go
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
// Package checker implements the LDAP server checker for happyDomain.
|
||||
//
|
||||
// It probes a domain's LDAP deployment (_ldap._tcp / _ldaps._tcp SRV
|
||||
// discovery with fallback to default ports 389/636, anonymous bind,
|
||||
// StartTLS upgrade, RootDSE introspection, plaintext-bind refusal,
|
||||
// supportedSASLMechanisms) and reports actionable findings.
|
||||
//
|
||||
// TLS certificate chain / SAN / expiry / cipher posture is intentionally
|
||||
// out of scope -- the dedicated TLS checker covers that, fed by the TLS
|
||||
// endpoints we publish as DiscoveryEntry records.
|
||||
package checker
|
||||
|
||||
import (
|
||||
sdk "git.happydns.org/checker-sdk-go/checker"
|
||||
)
|
||||
|
||||
const ObservationKeyLDAP sdk.ObservationKey = "ldap"
|
||||
|
||||
// LDAPMode distinguishes plaintext LDAP (with optional StartTLS) from
|
||||
// implicit-TLS LDAPS endpoints.
|
||||
type LDAPMode string
|
||||
|
||||
const (
|
||||
ModePlain LDAPMode = "ldap"
|
||||
ModeLDAPS LDAPMode = "ldaps"
|
||||
)
|
||||
|
||||
// LDAPData is the full observation stored per run.
|
||||
type LDAPData struct {
|
||||
Domain string `json:"domain"`
|
||||
BaseDN string `json:"base_dn,omitempty"`
|
||||
RunAt string `json:"run_at"`
|
||||
SRV SRVLookup `json:"srv"`
|
||||
Endpoints []EndpointProbe `json:"endpoints"`
|
||||
// BindTested is true when a bind DN was supplied and a bind attempt ran.
|
||||
BindTested bool `json:"bind_tested,omitempty"`
|
||||
}
|
||||
|
||||
type SRVLookup struct {
|
||||
LDAP []SRVRecord `json:"ldap,omitempty"`
|
||||
LDAPS []SRVRecord `json:"ldaps,omitempty"`
|
||||
// Errors per-set (keyed by record type like "_ldap._tcp").
|
||||
Errors map[string]string `json:"errors,omitempty"`
|
||||
// FallbackProbed is true when no SRV was published and we probed the
|
||||
// bare domain on the default ports.
|
||||
FallbackProbed bool `json:"fallback_probed,omitempty"`
|
||||
}
|
||||
|
||||
type SRVRecord struct {
|
||||
Target string `json:"target"`
|
||||
Port uint16 `json:"port"`
|
||||
Priority uint16 `json:"priority"`
|
||||
Weight uint16 `json:"weight"`
|
||||
// IPv4 and IPv6 addresses resolved for the target (at probe time).
|
||||
IPv4 []string `json:"ipv4,omitempty"`
|
||||
IPv6 []string `json:"ipv6,omitempty"`
|
||||
}
|
||||
|
||||
// EndpointProbe is the result of probing one (mode, host, port, address) tuple.
|
||||
type EndpointProbe struct {
|
||||
Mode LDAPMode `json:"mode"`
|
||||
SRVPrefix string `json:"srv_prefix,omitempty"`
|
||||
Target string `json:"target"`
|
||||
Port uint16 `json:"port"`
|
||||
Address string `json:"address"`
|
||||
IsIPv6 bool `json:"is_ipv6,omitempty"`
|
||||
|
||||
// What happened.
|
||||
TCPConnected bool `json:"tcp_connected"`
|
||||
|
||||
// StartTLSOffered is only meaningful on Mode=ldap: whether the server
|
||||
// accepted the RFC 2830 ExtendedRequest.
|
||||
StartTLSOffered bool `json:"starttls_offered"`
|
||||
StartTLSUpgraded bool `json:"starttls_upgraded"`
|
||||
// TLSEstablished is true whenever the link was encrypted (direct LDAPS
|
||||
// handshake succeeded OR StartTLS completed).
|
||||
TLSEstablished bool `json:"tls_established"`
|
||||
|
||||
TLSVersion string `json:"tls_version,omitempty"`
|
||||
TLSCipher string `json:"tls_cipher,omitempty"`
|
||||
|
||||
// RootDSE / capability fingerprint.
|
||||
RootDSERead bool `json:"rootdse_read"`
|
||||
SupportedLDAPVersion []string `json:"supported_ldap_version,omitempty"`
|
||||
SupportedSASLMechanisms []string `json:"supported_sasl_mechanisms,omitempty"`
|
||||
SupportedControl []string `json:"supported_control,omitempty"`
|
||||
SupportedExtension []string `json:"supported_extension,omitempty"`
|
||||
NamingContexts []string `json:"naming_contexts,omitempty"`
|
||||
VendorName string `json:"vendor_name,omitempty"`
|
||||
VendorVersion string `json:"vendor_version,omitempty"`
|
||||
|
||||
// AnonymousBindAllowed is true when an anonymous simple bind to DN=""
|
||||
// succeeded. Many directories accept this just to expose the RootDSE;
|
||||
// we flag it only when paired with anonymous read of naming contexts.
|
||||
AnonymousBindAllowed bool `json:"anonymous_bind_allowed,omitempty"`
|
||||
|
||||
// AnonymousSearchAllowed is true when a subtree search on the first
|
||||
// naming context with baseObject returned any entry without auth.
|
||||
// This is the information-disclosure signal.
|
||||
AnonymousSearchAllowed bool `json:"anonymous_search_allowed,omitempty"`
|
||||
|
||||
// Plaintext-bind posture (only for Mode=ldap, run before TLS upgrade).
|
||||
// PlaintextBindTested is true when we attempted a simple bind with
|
||||
// dummy credentials over cleartext to see whether the server refused
|
||||
// it (RFC 4513 requires refusing auth over insecure channels when
|
||||
// policy demands it, though in practice most deployments don't).
|
||||
PlaintextBindTested bool `json:"plaintext_bind_tested,omitempty"`
|
||||
// PlaintextBindAccepted is true when the server did NOT refuse the
|
||||
// cleartext bind with "confidentiality required" (resultCode 13).
|
||||
// A bad-credentials response (49) counts as "accepted the attempt",
|
||||
// which is the insecure-posture signal we want to flag.
|
||||
PlaintextBindAccepted bool `json:"plaintext_bind_accepted,omitempty"`
|
||||
|
||||
// Bind test (run only when bind_dn/bind_password options provided).
|
||||
BindAttempted bool `json:"bind_attempted,omitempty"`
|
||||
BindOK bool `json:"bind_ok,omitempty"`
|
||||
BindError string `json:"bind_error,omitempty"`
|
||||
|
||||
// Read access test on base_dn (run only when base_dn provided AND the
|
||||
// authenticated bind above succeeded, unless base_dn was meant to be
|
||||
// anonymously readable).
|
||||
BaseReadAttempted bool `json:"base_read_attempted,omitempty"`
|
||||
BaseReadOK bool `json:"base_read_ok,omitempty"`
|
||||
BaseReadEntries int `json:"base_read_entries,omitempty"`
|
||||
BaseReadError string `json:"base_read_error,omitempty"`
|
||||
|
||||
ElapsedMS int64 `json:"elapsed_ms"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type ReachabilitySpan struct {
|
||||
HasIPv4 bool `json:"has_ipv4"`
|
||||
HasIPv6 bool `json:"has_ipv6"`
|
||||
// EncryptedReachable is true when at least one endpoint offered encrypted
|
||||
// transport (LDAPS or LDAP+StartTLS).
|
||||
EncryptedReachable bool `json:"encrypted_reachable"`
|
||||
// PlainOnlyReachable is true when only cleartext endpoints responded.
|
||||
PlainOnlyReachable bool `json:"plain_only_reachable"`
|
||||
}
|
||||
|
||||
// Issue is a structured finding attached to the observation so the rule and
|
||||
// the HTML report can both consume them without re-deriving logic.
|
||||
type Issue struct {
|
||||
Code string `json:"code"`
|
||||
Severity string `json:"severity"` // "info" | "warn" | "crit"
|
||||
Message string `json:"message"`
|
||||
Fix string `json:"fix,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
}
|
||||
|
||||
// Severities (string for stable JSON, independent of sdk.Status numeric values).
|
||||
const (
|
||||
SeverityInfo = "info"
|
||||
SeverityWarn = "warn"
|
||||
SeverityCrit = "crit"
|
||||
)
|
||||
|
||||
// Issue codes.
|
||||
const (
|
||||
CodeNoSRV = "ldap.no_srv"
|
||||
CodeSRVServfail = "ldap.srv.servfail"
|
||||
CodeTCPUnreachable = "ldap.tcp.unreachable"
|
||||
CodeAllEndpointsDown = "ldap.all_endpoints_down"
|
||||
CodeNoEncryptedEndpoint = "ldap.no_encrypted_endpoint"
|
||||
CodeStartTLSMissing = "ldap.starttls.missing"
|
||||
CodeStartTLSFailed = "ldap.starttls.handshake_failed"
|
||||
CodeLDAPSHandshakeFailed = "ldap.ldaps.handshake_failed"
|
||||
CodePlainBindAccepted = "ldap.plain_bind.accepted"
|
||||
CodeAnonymousSearchAllowed = "ldap.anon.search_allowed"
|
||||
CodeRootDSEUnreadable = "ldap.rootdse.unreadable"
|
||||
CodeNoSASL = "ldap.sasl.none"
|
||||
CodeSASLPlainOnly = "ldap.sasl.plain_only"
|
||||
CodeSASLNoStrongMech = "ldap.sasl.no_strong_mech"
|
||||
CodeLegacyLDAPv2 = "ldap.legacy_v2"
|
||||
CodeStartTLSOnLDAPS = "ldap.starttls.on_ldaps"
|
||||
CodeNoIPv6 = "ldap.no_ipv6"
|
||||
CodeBindFailed = "ldap.bind.failed"
|
||||
CodeBindOK = "ldap.bind.ok"
|
||||
CodeBaseReadFailed = "ldap.base_read.failed"
|
||||
CodeBaseReadOK = "ldap.base_read.ok"
|
||||
CodeNoNamingContext = "ldap.rootdse.no_naming_context"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue