Signals API
Chaque worker, handler d’ingest et scanner côté hub écrit ses findings
pertinents pour le Trust Score dans une seule table : signal_streams.
Trust Score, Anomaly Correlation, Compliance Pareto et tous les dashboards
y lisent. Plus de tables silo par feature.
Conventions du wire-format
| Champ | Forme |
|---|---|
source | lowercase_snake_case — cert_scan, dns_check, … |
subject_type | lowercase_snake_case — agent, endpoint, domain, dependency, … |
signal_key | dotted lowercase — cert.expiry_days, dns.dmarc_present |
signal_value | JSONB ; convention : {"value": <native>, …metadata} |
severity | NULL ou l’une de `info |
observed_at | TIMESTAMPTZ ; colonne de partition sur l’hypertable |
ttl_seconds | NULL = sticky |
Les CHECK-constraints Postgres valident chaque regex pour qu’une faute de
frappe remonte immédiatement en 23514 ; signals.Emitter.validate() fait
le même contrôle en Go pour que l’erreur atterrisse au call site.
Interface Go
import "github.com/gotrust/monsys/hub/api/signals"
emitter := signals.NewSQLEmitter(pool)
err := emitter.Emit(ctx, tenantID, signals.Signal{ Source: "my_check", SubjectType: "agent", SubjectID: agentID.String(), Key: "my.finding", Value: map[string]any{"value": 42, "extra": "context"}, Severity: signals.SeverityHigh, ObservedAt: time.Now().UTC(),})EmitBatch utilise pgx.CopyFrom ; all-or-nothing : un seul signal
invalide rejette tout le batch (voir le rationale dans signals/emitter.go).
Signaux émis par l’agent
L’agent peut émettre des signaux via un payload dédié agent_signals
(Phase 1.4–1.6). Le hub valide :
source∈ allowlist (backup_check,clock_check,endpoint_posture)subject_type∈ allowlist (agent)subject_idécrasé par le agent_id de l’appelant quandsubject_type=agent
Un agent compromis ne peut jamais réclamer des findings pour un autre agent de son tenant.
Source → catégorie Trust Score
Mapping dans hub/api/trust_score.SourceToCategory. Ajouter une nouvelle
source = une entrée de plus ; sans cette entrée la source compte zéro
point au Trust Score.
RLS
signal_streams a ENABLE ROW LEVEL SECURITY avec
USING tenant_id = current_setting('app.current_tenant', true)::UUID.
Défense en profondeur. Toute requête côté hub DOIT quand même inclure
un WHERE tenant_id = $1 explicite — set_config(..., true) ne survit
pas au connection-recycling de pgxpool. Voir
docs/internal/decisions/2026-Q2-connected-dashboards.md D-0003.
La compression n’est pas activée sur signal_streams (Timescale 2.18+
refuse la combinaison RLS+columnstore). Rétention via drop_chunks,
180j par défaut.