Documentation
¶
Overview ¶
Package collect is the FIRST-PARTY PASSIVE data connector architecture (v0.0.2, milestones M14/M15, owner directive §2).
What a connector is ¶
A connector turns one data provider's records into GSEER facts — (asset, optional service, observation) triples — through the SAME ingestion path every other writer uses (store.IngestFacts). There is no side door: the suppression gate (M13) and the assets backstop trigger see everything a collector produces.
The four honesty contracts ¶
Search ≠ Scan. A collector only QUERIES providers the operator configured about identifiers the operator or the index already named (seeds, or assets already in the index). Nothing here port- scans, banner-grabs, or enumerates the internet. The P2 active observation regime (gseer scan, PAC gates, scopes table) is a different milestone with different gates.
Terms before traffic. Every connector carries a Review verdict. A connector whose provider terms do not clearly permit automated collection and indexing is DISABLED — the runner refuses to run it (E_SOURCE_TERMS) even if a database row claims enabled=true. docs/SOURCES.md is the review register; each connector's Review.DocRef points at its section.
Discovery ≠ Observation. Passive data (DNS answers, certificate log entries, registry objects) proves an identifier EXISTS or a mapping was PUBLISHED — never that a service answered on a port. P1 connectors therefore write ASSET-LEVEL facts only (Service == nil): no port, protocol, HTTP, TLS or banner information is ever invented from passive sources. In search_index every such record appears as row_kind='asset-only'. Service observations require a service observation, which is P2 with its own gates.
Provenance on every record. Facts carry the delivering source's identity (observation.source_id), the connector category in provenance, the observation timestamp (the provider's where it has one; the lookup time otherwise) and evidence with a confidence label. Nothing a connector writes can be mistaken for the labeled synthetic fixture corpus (provenance=fixture is reserved for it).
Data quality (owner directive §4) ¶
- Normalization: every identifier passes the suppress package's canonicalizers (lowercase IDNA, trailing-dot strip, netip form) — the same functions the suppression gate uses, so normalization can never diverge from suppression.
- Idempotency: L1 delivery identity (source, cursor) + L2 fact identity (source, asset, service, observed_at, payload_hash) make re-runs no-ops at the store level; connectors additionally dedupe within a batch.
- First-seen / last-seen / freshness: the asset/service upserts keep first_seen, GREATEST last_seen and clear stale on fresh sightings — a re-observed identifier ages forward, a missing one ages stale.
- Retry + checkpoint: the Runner retries transport failures with backoff, stores per-source cursors (source_checkpoints), and parks poison records in ingestion_dead_letter instead of dropping them.
Layering ¶
collect → storage/postgres → storage, suppress collect → suppress (normalizers only)
The package never imports internal/gql (search stays read-only over the index) and never opens listening sockets. The governance tests pin both.
Index ¶
- Constants
- func Fatal(err error) error
- func Registry() map[string]Collector
- type BadRecord
- type Batch
- type Client
- type ClientConfig
- type Collector
- type Ctx
- type Fact
- type FatalError
- type RawFact
- type RedriveOptions
- type RedriveStats
- type Review
- type RunStats
- type Runner
- type SourceConfig
- type SourceRef
Constants ¶
const ( DefaultRPS = 1.0 DefaultTimeout = 15 * time.Second DefaultMaxBodyBytes = 8 << 20 // 8 MiB — no provider answer is that big DefaultUserAgent = "GSEER/0.0.2 (passive connector; https://github.com/Lutfifakee-Project/GSEER)" )
Politeness defaults (documented in docs/SOURCES.md per source; a source's own documented limit may be lower — the connector applies it through its ClientConfig).
const ( ReviewEnabled = "reviewed-enabled" ReviewDisabled = "disabled" )
Review verdicts.
const ( DefaultMaxAttempts = 3 DefaultBackoff = 2 * time.Second DefaultMaxBatches = 100 )
Runner defaults: passive-polite transport behavior.
const DefaultRunLimit = 500
DefaultRunLimit bounds one run when the config says nothing: passive politeness by default.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type BadRecord ¶
BadRecord is a raw record the pipeline refused (dead-lettered, never silently dropped).
func BuildFacts ¶
BuildFacts canonicalizes and dedupes raw records into ingestible asset-level facts (data-quality pipeline; owner directive §4):
- identifiers normalize through the suppression package's canonicalizers — ONE normalization definition shared with the gate;
- invalid records come back as BadRecords (dead-letter fodder), they are never silently dropped;
- the batch is deduped on (asset kind, canonical value) so one provider response yields one fact per identifier;
- every fact is asset-level: no service rows are ever built here (discovery ≠ observation);
- provenance is the connector kind; confidence lands in evidence.
type Batch ¶
type Batch struct {
Raws []RawFact
Cursor string
Done bool
// Notes are connector-reported counters surfaced in the job stats.
Notes map[string]int
}
Batch is one connector's output: RAW provider records only. The Runner owns the fact pipeline (normalize → dedupe → ingest) so normalization can never diverge between connectors, and every record passes the same suppression gate at the store. Records MUST be asset-level (P1 honesty contract 3): a RawFact can never express a service, and the runner would refuse one that tried. Cursor resumes the next run (” = begin; Done means the connector has nothing further this pass).
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is the shared, rate-limited HTTP fetcher for passive connectors. Defaults are deliberately conservative: passive means the provider never feels us.
type ClientConfig ¶
type ClientConfig struct {
// RequestsPerSecond caps the request rate (0 = DefaultRPS).
RequestsPerSecond float64
// Timeout bounds one request (0 = DefaultTimeout).
Timeout time.Duration
// MaxBodyBytes bounds one response body (0 = DefaultMaxBodyBytes).
MaxBodyBytes int64
// UserAgent identifies GSEER honestly (0 value = DefaultUserAgent).
UserAgent string
}
ClientConfig tunes one Client.
type Collector ¶
type Collector interface {
// Kind is the connector type ("dns", "rdap", "asn", "ct").
Kind() string
// Review is the terms-compliance verdict (see doc.go contract 2).
Review() Review
// Collect runs one passive pass and returns the batch. A connector
// NEVER ingests: it only produces facts (the Runner owns ingestion,
// checkpoints, dead letters and job bookkeeping).
Collect(ctx Ctx) (*Batch, error)
}
Collector is one data provider's passive connector. Implementations are stateless: everything they need arrives in CollectInput.
type Ctx ¶
type Ctx struct {
Context context.Context // the run's context; never nil from the Runner
Source SourceRef
Config SourceConfig
Cursor string
Limit int // per-run record budget (0 = DefaultRunLimit)
Client *Client // shared rate-limited HTTP client
// IndexSeeds loads seed identifiers already in the index (seed_mode
// "index"): no third-party target lists are ever used.
IndexSeeds func(kind string, limit int) ([]string, error)
// Now is injectable for tests.
Now func() time.Time
}
Ctx is the connector's view of one run: its configuration, the resume cursor, the shared HTTP client (nil for DNS-only connectors), the run context (cancellation flows to provider calls) and the seed loader for index-driven enrichment.
type Fact ¶
Fact aliases the storage-level ingest unit so connector code and the runner read naturally ([]storage.Fact IS []Fact — identical alias).
type FatalError ¶
type FatalError struct{ Err error }
FatalError marks a connector error that retries cannot fix (a refused query, a malformed request). Wrap at the point the failure is known permanent; the runner's retry loop passes it through unwrapped.
func (FatalError) Error ¶
func (e FatalError) Error() string
func (FatalError) Unwrap ¶
func (e FatalError) Unwrap() error
Unwrap gives errors.Is/As access to the wrapped cause.
type RawFact ¶
type RawFact struct {
AssetKind string // "ip" | "domain" | "hostname"
Value string // raw surface form; canonicalized by the pipeline
ObservedAt time.Time
Confidence string // "high" | "medium" (provider-dependent)
// Enrichment fields (asset metadata the upsert merges without
// blanking stored values): the ASN/BGP connector fills ASN/ASNOrg/
// GeoCountry; RDAP fills GeoCountry. They ride on the asset row —
// never invented beyond what the provider stated.
ASN int
ASNOrg string
GeoCountry string
// Extra lands inside observation.evidence under the connector's key.
Extra map[string]any
}
RawFact is one provider record a connector derived from a response, pre-ingestion (the fact pipeline turns these into storage.Fact values, dead-lettering the invalid ones). A raw record is ALWAYS asset-level: there is no way to express a service here (P1 honesty contract 3).
type RedriveOptions ¶
type RedriveOptions struct {
// SourceID restricts to one source; 0 = all sources.
SourceID int64
// Limit bounds the pass (0 = 500).
Limit int
// Discard marks the pending rows discarded instead of re-driving.
Discard bool
}
RedriveOptions bounds one re-drive pass.
type RedriveStats ¶
type RedriveStats struct {
Considered int `json:"considered"`
Redriven int `json:"redriven"` // ingestion attempted
Resolved int `json:"resolved"` // applied successfully
StillFailing int `json:"still_failing"` // attempted, failed again (kept pending, attempts bumped)
Suppressed int `json:"suppressed"` // refused by the suppression gate (kept pending)
Discarded int `json:"discarded"` // only with Discard
}
RedriveStats reports one re-drive pass.
func Redrive ¶
func Redrive(ctx context.Context, store *postgres.Store, opts RedriveOptions) (*RedriveStats, error)
Redrive runs one pass over pending dead letters.
type Review ¶
type Review struct {
// Verdict is "reviewed-enabled" (clear compatible terms: the
// connector may be enabled by the operator) or "disabled"
// (credentials/payment/unclear permission for indexing or reuse —
// the runner refuses to run it, period).
Verdict string `json:"verdict"`
// Why is the one-line justification (what the terms say or lack).
Why string `json:"why"`
// DocRef anchors the full review in docs/SOURCES.md.
DocRef string `json:"doc_ref"`
}
Review is the terms-compliance verdict for one connector's provider.
type RunStats ¶
type RunStats struct {
Source string `json:"source"`
Connector string `json:"connector"`
JobID int64 `json:"job_id"`
Records int `json:"records"`
Facts int `json:"facts"`
Ingested int `json:"ingested"`
Updated int `json:"updated"`
Deduped int `json:"deduped"`
Suppressed int `json:"suppressed"`
DeadLetter int `json:"dead_lettered"`
Batches int `json:"batches"`
Cursor string `json:"cursor"`
Done bool `json:"done"`
// Skipped is set (with the reason) when nothing ran.
Skipped string `json:"skipped,omitempty"`
}
RunStats reports one run; marshalled into the collection_jobs.stats JSONB and rendered by the CLI.
type Runner ¶
type Runner struct {
Store *postgres.Store
Client *Client // shared default client (per-source config may override)
// Now is injectable for tests; zero value = time.Now.
Now func() time.Time
// MaxAttempts bounds transport retries per batch (0 = DefaultMaxAttempts).
MaxAttempts int
// Backoff is the base delay between retries, doubled per attempt
// (0 = DefaultBackoff).
Backoff time.Duration
// MaxBatches bounds one run's batch loop (0 = DefaultMaxBatches) —
// a stuck cursor must not spin forever.
MaxBatches int
// contains filtered or unexported fields
}
Runner executes connector passes against the store.
func (*Runner) Run ¶
Run executes one passive pass for the named registered source.
Gate order (fail-closed):
- unknown connector kind → E_SOURCE_CONNECTOR (hard error)
- connector review verdict disabled → E_SOURCE_TERMS (hard error: the terms gate outranks a hand-flipped enabled=true row)
- source enabled=false → clean skip (Skipped="disabled")
type SourceConfig ¶
type SourceConfig struct {
// Endpoint is the provider's API base URL (HTTP connectors).
Endpoint string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
// Seeds are operator-configured identifiers (seed_mode "config").
Seeds []string `json:"seeds,omitempty" yaml:"seeds,omitempty"`
// SeedMode: "config" (use Seeds) or "index" (use identifiers already
// in the GSEER index — the connector enriches the operator's own
// corpus; it never consults a third-party target list).
SeedMode string `json:"seed_mode,omitempty" yaml:"seed_mode,omitempty"`
// SeedKind is the asset kind index seeds are drawn from
// ("ip" for rdap/asn enrichment; unused for "config" mode).
SeedKind string `json:"seed_kind,omitempty" yaml:"seed_kind,omitempty"`
// Resolver is an optional DNS server address ("host:port") for the
// dns/asn connectors; empty = the system resolver.
Resolver string `json:"resolver,omitempty" yaml:"resolver,omitempty"`
// RequestsPerSecond caps the HTTP client (0 = connector default).
RequestsPerSecond float64 `json:"requests_per_second,omitempty" yaml:"requests_per_second,omitempty"`
// TimeoutMs bounds one provider request (0 = connector default).
TimeoutMs int `json:"timeout_ms,omitempty" yaml:"timeout_ms,omitempty"`
// LimitPerRun bounds one run's record budget (0 = DefaultRunLimit).
LimitPerRun int `json:"limit_per_run,omitempty" yaml:"limit_per_run,omitempty"`
}
SourceConfig is the decoded data_sources.config / YAML source block.
func DecodeSourceConfig ¶
func DecodeSourceConfig(raw []byte) (SourceConfig, error)
DecodeSourceConfig parses the raw JSONB config; unknown keys are kept out — configs are small and explicit.