package checker import ( "context" "encoding/json" "html/template" "sync" sdk "git.happydns.org/checker-sdk-go/checker" ) // Severity strings shared between sources, rules, and the HTML report. const ( SeverityCrit = "crit" SeverityWarn = "warn" SeverityInfo = "info" SeverityOK = "ok" ) // Source is the contract every reputation source implements. The // registry collects one Source per backend (DNSBL family, Safe // Browsing, URLhaus, VirusTotal, OpenPhish, …); Collect fans out over // the registry concurrently and folds the per-source results into the // observation payload. Adding a new source is a single file plus a // `Register(...)` call in init(). // // A Source returns *one or more* SourceResult values. Most sources // return exactly one (`{Listed, Reasons, …}`); the DNSBL family returns // one result per zone. Returning many results from one Source keeps the // definition tidy (one ID, one set of options, one rule entry) while // still surfacing per-zone detail in the report. type Source interface { ID() string Name() string // Options contributes the option fields the source needs. They are // merged into the global CheckerDefinition at startup. Options() SourceOptions // Query runs the source against `registered` (the eTLD+1 of the // target domain) and returns one result per logical sub-target. The // implementation should never return nil: when the source is // disabled, return a single SourceResult with Enabled=false. Query(ctx context.Context, domain, registered string, opts sdk.CheckerOptions) []SourceResult // Diagnose produces the action-required card for a *listed* result. // Implementations should focus on the operator's next step; the // generic report wraps it with the title bar and severity styling. // Called only when SourceResult.Listed is true. Diagnose(res SourceResult) Diagnosis } // DetailRenderer is an optional interface a Source can implement when // the generic SourceResult shape (Reasons + Evidence + URLs) cannot // fully express its output. Examples: VirusTotal's per-vendor verdict // table, URLhaus' URL list with online/offline status. The returned // HTML fragment is dropped into the source's section verbatim and is // expected to be safe (use html/template or template.HTMLEscape). type DetailRenderer interface { Source RenderDetail(res SourceResult) (template.HTML, error) } // SourceOptions describes the option fields a source contributes to the // CheckerDefinition. Audiences map directly to the SDK's // CheckerOptionsDocumentation buckets. type SourceOptions struct { Admin []sdk.CheckerOptionField User []sdk.CheckerOptionField } // SourceResult is the unified envelope every source produces. Source- // specific structured data lives in Details (json.RawMessage), so the // generic code (rules, headline, base diagnosis card, summary table) // can operate on the envelope without source-specific switches; the // rich report sections fish Details back through DetailRenderer. type SourceResult struct { SourceID string `json:"source_id"` SourceName string `json:"source_name"` Subject string `json:"subject,omitempty"` // e.g. zone label for DNSBL Enabled bool `json:"enabled"` Listed bool `json:"listed"` BlockedQuery bool `json:"blocked_query,omitempty"` // resolver blocked, not a real listing Severity string `json:"severity,omitempty"` // when Listed Reasons []string `json:"reasons,omitempty"` Evidence []Evidence `json:"evidence,omitempty"` LookupURL string `json:"lookup_url,omitempty"` RemovalURL string `json:"removal_url,omitempty"` Reference string `json:"reference,omitempty"` Error string `json:"error,omitempty"` Details json.RawMessage `json:"details,omitempty"` } // Evidence is a single observation that supports a verdict. Keeping it // loosely typed (Label/Value/Status + free-form Extra) covers DNSBL // return codes, OpenPhish URLs, URLhaus URLs, VT engine verdicts, … // without growing the schema for each source. type Evidence struct { Label string `json:"label"` Value string `json:"value"` Status string `json:"status,omitempty"` Extra map[string]string `json:"extra,omitempty"` } // Diagnosis is the action-required card surfaced at the top of the // report. Sources build it in their Diagnose method. type Diagnosis struct { Severity string Title string Detail string Fix string FixIsURL bool LookupURL string RemovalURL string } // ---------- registry ---------- var ( registryMu sync.RWMutex registry []Source ) // Register adds a Source to the global registry. Intended to be called // from init(). Panics on duplicate IDs so misconfigurations fail loudly // at startup rather than producing silently-overlapping rules/options. func Register(s Source) { registryMu.Lock() defer registryMu.Unlock() for _, existing := range registry { if existing.ID() == s.ID() { panic("checker-blacklist: duplicate source ID " + s.ID()) } } registry = append(registry, s) } // Sources returns a snapshot of the registered sources, in registration // order. Callers must not mutate the slice. func Sources() []Source { registryMu.RLock() defer registryMu.RUnlock() out := make([]Source, len(registry)) copy(out, registry) return out }