Documentation
¶
Overview ¶
Package suppress is the centralized suppression policy layer (v0.0.2 Passive Intelligence, milestone M13, owner directive §3).
A suppression rule removes classes of records from GSEER's data plane:
KindIP exact IP address (canonical netip form; IPv4 + IPv6)
KindCIDR network range (masked prefix; containment match)
KindDomain exact domain or hostname (canonical IDNA/lowercase)
KindDomainSuffix the exact name plus every descendant that matches at
a STRICT LABEL boundary (sub.example.com matches
suffix example.com; notexample.com does not)
KindSource a registered data_sources.name: everything that
source delivers is refused before ingestion
Rules carry reason + created_by + created_at audit metadata, an optional expiration (expired rules stop matching but remain listed for audit) and an enabled flag.
Enforcement model — two layers, fail-closed ¶
Go gate (this package + internal/storage/postgres): every ingestion path (IngestFact, IngestFacts, UpsertAsset, UpsertService) compiles the rule set into a Matcher and refuses matching facts BEFORE any write. Suppressed facts are skipped cleanly (IngestStats.Suppressed) and audited in suppression_applications.
DB backstop (migration 0005): a BEFORE INSERT/UPDATE trigger on assets re-evaluates the rules inside PostgreSQL with the exact same matching semantics. Any future ingestion path that forgets the Go gate — a new collector, a bulk loader, a psql session — is rejected by the database itself (custom ERRCODE GSUP1, mapped to E_DB_SUPPRESSED).
Canonical values ¶
Matching is exact-string and therefore only correct if both the rules and the stored asset values are CANONICAL:
- domains/hostnames: lowercase, no trailing root dot, IDNA punycode (A-labels) for non-ASCII labels, non-empty labels
- IPs: netip canonical form, zone stripped (IPv4 + IPv6)
- CIDRs: masked (10.1.2.3/8 → 10.0.0.0/8)
The store normalizes every asset write through NormalizeDomain / NormalizeIP / NormalizeCIDR, so the search index, the DB trigger and the Go matcher all see the same byte sequences. NormalizeXxx are exported here because v0.0.2 data-quality (M14) applies the same functions in the collector ingestion pipeline — one normalization definition, one place.
Deliberate non-features ¶
- Cross-type propagation is NOT automatic: a domain rule does not block the IPs the domain resolves to, and an IP rule does not block reverse-DNS names. Identifier types are suppressed by identifier type; propagation is a policy decision for the operator.
- Matching is name-based, never content-based; there is no regex rule kind (regex rules are un-auditable at enforcement time).
- Expired rules are never auto-deleted; they stay for audit.
Layering (AC-3.1): this package imports only the standard library and golang.org/x/net/idna. It never imports storage, pgx, collectors or egress; the storage layer imports IT.
Index ¶
- func IsSubdomain(host, suffix string) bool
- func NormalizeCIDR(s string) (string, error)
- func NormalizeDomain(s string) (string, error)
- func NormalizeIP(s string) (string, error)
- func NormalizeValue(kind Kind, value string) (string, error)
- func ValidateKind(k Kind) bool
- type Kind
- type Matcher
- type Rule
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsSubdomain ¶
IsSubdomain reports whether host equals suffix or is a descendant of suffix at a STRICT LABEL boundary. Both arguments must already be canonical (NormalizeDomain): trailing dots are gone, case is folded.
IsSubdomain("example.com", "example.com") → true
IsSubdomain("sub.example.com", "example.com") → true
IsSubdomain("deep.a.b.example.com", "example.com") → true
IsSubdomain("notexample.com", "example.com") → false
IsSubdomain("example.com.evil.net", "example.com") → false
IsSubdomain("badexample.com", "badexample.co") → false (no partial label)
func NormalizeCIDR ¶
NormalizeCIDR canonicalizes an IPv4 or IPv6 CIDR prefix: host bits are masked off ("10.1.2.3/8" → "10.0.0.0/8", "2001:db8::1/32" → "2001:db8::/32") and the address half is canonicalized as in NormalizeIP. Idempotent.
func NormalizeDomain ¶
NormalizeDomain canonicalizes a domain or hostname:
- surrounding whitespace trimmed; embedded whitespace rejected
- exactly one trailing root dot stripped ("example.com." → "example.com"); a trailing dot is the FQDN root marker, so "a..b" (which leaves an empty label) is rejected
- case-folded; non-ASCII labels mapped to punycode A-labels via the UTS-46 lookup mappings ("München.de" → "xn--mnchen-3ya.de"); IDNA mapping is applied PER LABEL with an ASCII fast path so that legal-in-the-wild ASCII service labels (_dmarc, _acme-challenge) survive — IDNA2008 disallows U+005F even in its lookup mappings
- every label validated: non-empty, 1..63 chars, only [a-z0-9_-], no leading/trailing hyphen
- total length ≤ 253
Already-punycode input is returned unchanged (idempotent). The empty string is an error.
func NormalizeIP ¶
NormalizeIP canonicalizes an IPv4 or IPv6 address with netip: zero compression and lowercase for IPv6 ("2001:0DB8:0000::1" → "2001:db8::1"), dotted-quad for IPv4, the IPv6 scope zone stripped ("fe80::1%eth0" → "fe80::1"), IPv4-mapped forms printed in their mixed notation consistently. Idempotent: canonical input round-trips.
func NormalizeValue ¶
NormalizeValue canonicalizes by rule kind: KindIP → NormalizeIP, KindCIDR → NormalizeCIDR, KindDomain/KindDomainSuffix → NormalizeDomain, KindSource → trimmed exact name (source names are registry identifiers, not DNS names: case-preserved, no IDNA).
func ValidateKind ¶
ValidateKind reports whether k is one of the five rule kinds.
Types ¶
type Kind ¶
type Kind string
Kind enumerates the suppression rule kinds (migration 0005 suppressions.kind CHECK constraint).
const ( // KindIP matches kind='ip' assets by exact canonical address. KindIP Kind = "ip" // KindCIDR matches kind='ip' assets whose address is inside the // (masked) prefix. KindCIDR Kind = "cidr" // KindDomain matches kind='domain' and kind='hostname' assets by // exact canonical name. KindDomain Kind = "domain" // KindDomainSuffix matches kind='domain' and kind='hostname' assets // whose canonical name equals the value or ends with "." + value // (strict label boundary). KindDomainSuffix Kind = "domain_suffix" // KindSource excludes everything a registered data source delivers. // The value is the data_sources.name. KindSource Kind = "source" )
type Matcher ¶
type Matcher struct {
// contains filtered or unexported fields
}
Matcher is a compiled rule set. Create with New; zero value matches nothing.
func New ¶
New compiles rules into a Matcher at instant now. Disabled and expired rules are skipped (they remain in the database for audit but never match). A rule whose value does not canonicalize is a hard error: a typo must not silently disable a suppression (fail-closed).
func (*Matcher) MatchesAsset ¶
MatchesAsset returns the rule suppressing an asset of kind assetKind ("ip" | "domain" | "hostname") with value v (any case, trailing dots and non-punycode accepted — the value is normalized here exactly as it will be normalized for storage). Returns (nil, nil) when not suppressed. An un-normalizable value is an error: the caller (the store) rejects such records on validation grounds anyway, and the gate must never silently pass a value it could not evaluate (fail-closed).
func (*Matcher) MatchesSource ¶
MatchesSource returns the rule excluding the named data source (data_sources.name), or nil. Source names are registry identifiers: exact match after trimming, case-preserved.
type Rule ¶
type Rule struct {
ID int64 `json:"id"`
Kind Kind `json:"kind"`
Value string `json:"value"` // canonical form
Reason string `json:"reason"`
CreatedBy string `json:"created_by,omitempty"`
CreatedAt time.Time `json:"created_at"`
ExpiresAt *time.Time `json:"expires_at,omitempty"` // nil = never expires
Enabled bool `json:"enabled"`
}
Rule is one suppression policy row (suppressions table).