suppress

package
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 8, 2026 License: AGPL-3.0 Imports: 6 Imported by: 0

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

  1. 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.

  2. 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

Constants

This section is empty.

Variables

This section is empty.

Functions

func IsSubdomain

func IsSubdomain(host, suffix string) bool

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

func NormalizeCIDR(s string) (string, error)

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

func NormalizeDomain(s string) (string, error)

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

func NormalizeIP(s string) (string, error)

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

func NormalizeValue(kind Kind, value string) (string, error)

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

func ValidateKind(k Kind) bool

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

func New(rules []Rule, now time.Time) (*Matcher, error)

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) Count

func (m *Matcher) Count() int

Count reports the number of effective (enabled, unexpired) rules.

func (*Matcher) MatchesAsset

func (m *Matcher) MatchesAsset(assetKind, v string) (*Rule, error)

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

func (m *Matcher) MatchesSource(name string) *Rule

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).

func (Rule) ExpiredAt

func (r Rule) ExpiredAt(now time.Time) bool

ExpiredAt reports whether the rule is expired at the given instant.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL