GNSS trust as security telemetry
kshana trust-telemetry turns the per-epoch GNSS trust stream (score 0-100, band, reasons)
into signals an operations or security centre already ingests: Prometheus metrics, syslog
events in CEF or LEEF for a SIEM, and optionally OpenTelemetry. It reads the stream; it
never changes the trust assessment and transmits nothing but telemetry.
Run it#
# live stream on stdin -> /metrics on localhost:9464
kshana receiver-trust live <args> | kshana trust-telemetry
# replay a batch result (the score is the epoch's own for a vessel platform, absent otherwise)
kshana trust-telemetry --result session.result.json --print-metrics
kshana trust-telemetry --result session.result.json --print-syslog --format leef
# SIEM over UDP, one event per band change
kshana trust-telemetry --input stream.jsonl --syslog-udp siem.example:514 --format cef --host ops-gw1
Input is the live JSON-lines schema v1.1 documented in docs/MARITIME-TRUST.md. Read:
t_s; state (calibrating|nominal|degraded|untrusted, the band); score (0-100 or
null); time (the epoch's time as stated); gate (off|passed|withheld); position (lat_deg, lon_deg, height_m, or null); reasons are the
monitors in deductions, then any further ones in alarms. Other keys are ignored, since
the schema only grows by appended keys. A line that does not parse is counted in kshana_trust_input_errors_total and
skipped; scores outside 0-100 are rejected, not clamped, so a format change is noticed.
The only code that knows this format is src/telemetry/sample.rs.
/metrics is served without authentication and defaults to 127.0.0.1:9464. Binding
elsewhere prints a warning; put it behind your own access control. The endpoint handles one
connection at a time with a 2 s read limit, so an idle client can delay a scrape by up to
that long; it is meant for one scraper, not for the open network.
From Python, WebAssembly and MCP#
The encoders are also available as text-in, text-out calls with no process, socket or clock:
Python kshana.trust_telemetry(input, format, host="", input_kind="jsonl", expose_position=False, syslog_all=False, unix_nano=0), the WebAssembly export trust_telemetry(input, format, host, input_kind, options_json), and the MCP tool trust_telemetry (plugin command
/kshana-trust-telemetry). format is prometheus, cef, leef or otlp; input_kind is
jsonl (the stream) or result (a batch result). They return the same text the command line
prints (--print-metrics, --print-syslog), with the same limits: syslog lines carry no
timestamp, host is at most 255 printable ASCII characters without spaces (refused otherwise,
here and on the command line), and a reply larger than the surface's limit (4 MiB on MCP) is
refused, because CEF and LEEF with syslog_all write one event per epoch. Nothing is sent,
served or written; the running exporters stay a process on the command line.
Delivery never stalls the assessment#
Syslog and OpenTelemetry delivery run on their own threads. The ingest loop only hands them
a message or a snapshot and never waits, so a dead, slow or non-reading collector cannot
back-pressure the live stream or freeze /metrics:
- every connect and write has a 2 s limit; a failed connection is dropped and reconnected lazily, with exponential backoff from 1 s up to 60 s;
- a message that cannot be delivered (collector down, write timed out, queue of 1024 full) is
dropped and counted in
kshana_trust_syslog_send_failures_total; alert on its rate; - the OpenTelemetry exporter is handed a copy of the registry, so no lock is held during network I/O; if an export is still in flight the new snapshot is skipped (the next one carries the same cumulative totals).
At end of input the sender threads are given their remaining time limits to finish.
Prometheus metrics#
| Metric | Type | Labels | Meaning |
|---|---|---|---|
kshana_build_info |
gauge | version |
constant 1 |
kshana_trust_score |
gauge | latest score 0-100; absent if the source gives none | |
kshana_trust_band |
gauge | band |
1 for the current band, 0 for the others |
kshana_trust_gate |
gauge | gate |
off, passed, withheld, unknown: 1 for the current state, 0 for the others; absent without a gate |
kshana_trust_position_latitude_degrees, ..._longitude_degrees, ..._height_meters |
gauge | receiver-reported position; only with --expose-position (the endpoint is unauthenticated and a position can identify a site or vessel) |
|
kshana_trust_reason_active |
gauge | reason |
1 if present at the latest epoch |
kshana_trust_epochs_total |
counter | band |
epochs received |
kshana_trust_reason_epochs_total |
counter | reason |
epochs in which the reason was present |
kshana_trust_input_errors_total |
counter | unparsable input lines | |
kshana_trust_reason_overflow_total |
counter | reason occurrences folded into reason="other" (see below) |
|
kshana_trust_syslog_send_failures_total |
counter | syslog events not delivered | |
kshana_trust_epoch_offset_seconds |
gauge | latest epoch's offset from stream start | |
kshana_trust_last_sample_timestamp_seconds |
gauge | wall-clock receipt time; alert on its age |
Reason names are cut to 128 characters, and at most 64 distinct reasons get their own series; later ones are counted under reason="other" and in kshana_trust_reason_overflow_total, so a hostile or buggy input cannot create unbounded label cardinality.
band is one of calibrating, nominal, degraded, untrusted, unknown. Reason
names are the monitor names of the stream (kinematic, heading-course, ...; for the batch
result, cn0-drop, agc, raim).
A sample dashboard is in deploy/grafana/kshana-gnss-trust.json (import it and pick your
Prometheus data source).
Syslog: CEF and LEEF#
Events are sent in an RFC 5424 envelope (facility local0, app kshana, message id
GNSSTRUST) over UDP (one datagram each; IPv4 or IPv6) or TCP, newline-terminated by
default or, with --syslog-octet-counting, framed <length> <message> as in RFC 6587. By default one event
is sent per band change; --syslog-all sends one per epoch. The vendor is written
Ashforde OU (ASCII) so no SIEM mis-decodes it.
| Concept | Syslog severity | CEF severity | LEEF sev |
|---|---|---|---|
| calibrating | 6 info | 0 | 1 |
| nominal | 6 info | 1 | 1 |
| degraded | 4 warning | 5 | 5 |
| untrusted | 2 critical | 9 | 9 |
| unknown | 5 notice | 3 | 3 |
Field mapping (event id is gnss-trust.<band>):
| Meaning | CEF | LEEF 2.0 (delimiter ^) |
|---|---|---|
| category | cat=gnss-trust |
cat |
| sending host | dvchost |
devHost |
| band | cs1 (label band) |
band |
| reasons, comma-separated | cs2 (reasons) |
reasons |
| previous band | cs3 (previousBand) |
previousBand |
| score | cfp1 (trustScore), omitted if none |
trustScore |
| epoch offset, s | cfp2 (epochOffsetSeconds) |
epochOffsetSeconds |
| log time label | cs4 (logTime) |
logTime |
| gate state | cs5 (gate) |
gate |
Empty fields are left out (no cs2= without reasons). The host name is cut to 255
characters. CEF header values escape \ and |; extension values escape \, = and line breaks.
LEEF values have ^ and line breaks replaced by spaces.
OpenTelemetry (off by default)#
Build with --features otlp and pass --otlp http://host:4318/v1/metrics. The exporter
posts OTLP/HTTP JSON (kshana.trust.score, kshana.trust.band, kshana.trust.epochs,
kshana.trust.reason.epochs; the cumulative sums carry startTimeUnixNano) every --otlp-every epochs (default 10) and at end of
input. It adds no dependency and has no TLS: send to a collector on the same host or a
trusted segment, which forwards onwards over TLS. https:// endpoints are refused.