Skip to content

Signals API

Every worker, ingest handler, and hub-side scanner writes Trust-Score-relevant findings to a single table: signal_streams. Trust Score, Anomaly Correlation, Compliance Pareto, and every dashboard read from there. No more per-feature silo tables.

Wire-format conventions

FieldShape
sourcelowercase_snake_casecert_scan, dns_check, …
subject_typelowercase_snake_caseagent, endpoint, domain, dependency, …
signal_keydotted lowercase — cert.expiry_days, dns.dmarc_present
signal_valueJSONB; convention: {"value": <native>, …metadata}
severityNULL or one of `info
observed_atTIMESTAMPTZ; partition column on the hypertable
ttl_secondsNULL = sticky

Postgres CHECK constraints validate every regex so a typo surfaces immediately as 23514; signals.Emitter.validate() runs the same check in Go so the error lands at the call site.

Go interface

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 uses pgx.CopyFrom; all-or-nothing: one invalid signal rejects the whole batch (see signals/emitter.go rationale).

Agent-emitted signals

The agent can emit signals via a dedicated agent_signals payload type (Phase 1.4–1.6). The hub validates:

  • source ∈ allowlist (backup_check, clock_check, endpoint_posture)
  • subject_type ∈ allowlist (agent)
  • subject_id overwritten with the caller agent_id when subject_type=agent

A compromised agent can never claim findings for another agent in its tenant.

Source → Trust Score category

Mapping in hub/api/trust_score.SourceToCategory. Adding a new source = one entry; without that entry the source counts zero points in Trust Score.

RLS

signal_streams has ENABLE ROW LEVEL SECURITY with USING tenant_id = current_setting('app.current_tenant', true)::UUID. Defense in depth. Every hub-side query MUST still include an explicit WHERE tenant_id = $1set_config(..., true) does not survive pgxpool’s connection recycling. See docs/internal/decisions/2026-Q2-connected-dashboards.md D-0003.

Compression is not enabled on signal_streams (Timescale 2.18+ refuses the RLS+columnstore combination). Retention via drop_chunks, default 180d.