Initial commit
This commit is contained in:
commit
66cf1fc9aa
30 changed files with 2735 additions and 0 deletions
146
checker/source.go
Normal file
146
checker/source.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue