La confiance GNSS comme télémétrie de sécurité
kshana trust-telemetry transforme le flux de confiance GNSS par époque (score de 0 à 100, bande, raisons) en signaux qu'un centre d'exploitation ou de sécurité ingère déjà : métriques Prometheus, événements syslog au format CEF ou LEEF pour un SIEM, et éventuellement OpenTelemetry. Il lit le flux ; il ne modifie jamais l'évaluation de confiance et ne transmet rien d'autre que de la télémétrie.
L'exécuter#
# 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
L'entrée est le schéma JSON-lines en direct v1.1 documenté dans docs/MARITIME-TRUST.md. Lus : t_s ; state (calibrating|nominal|degraded|untrusted, la bande) ; score (0 à 100 ou null) ; time (l'heure de l'époque telle qu'indiquée) ; gate (off|passed|withheld) ; position (lat_deg, lon_deg, height_m, ou null) ; les raisons sont les moniteurs de deductions, puis les éventuels autres de alarms ; à partir du schéma 1,3, geometry (status, hdop, pdop, n_used) et le class de chaque déduction. Les autres clés sont ignorées, puisque le schéma ne croît que par ajout de clés. Une ligne qui ne s'analyse pas est comptée dans kshana_trust_input_errors_total et ignorée ; les scores hors de 0 à 100 sont rejetés, non ramenés aux bornes, de sorte qu'un changement de format soit remarqué. Le seul code qui connaît ce format est src/telemetry/sample.rs.
/metrics est servi sans authentification et utilise par défaut 127.0.0.1:9464. Une liaison ailleurs affiche un avertissement ; placez-le derrière votre propre contrôle d'accès. Le point d'accès traite une connexion à la fois avec une limite de lecture de 2 s, de sorte qu'un client inactif peut retarder une collecte jusqu'à cette durée ; il est prévu pour un seul collecteur, pas pour le réseau ouvert.
Home Assistant et appliances NTP (MQTT)#
--print-mqtt affiche, et --mqtt-tcp <host:port> publie, un document d'état JSON et des messages de découverte MQTT de Home Assistant. La découverte (conservée, sous homeassistant/...) crée trois entités : score de confiance, bande de confiance (avec les raisons comme attributs) et un capteur de problème untrusted. Le document d'état va vers <prefix>/state (par défaut kshana/trust/state, conservé) : state, score (null tant qu'il n'y en a pas), untrusted, t_s, time, reasons, gate et la mention à titre consultatif ; la position n'est jamais incluse. Options : --mqtt-prefix, --mqtt-node, --mqtt-discovery.
Le publieur parle MQTT 3.1.1 en QoS 0 et ne lit que le CONNACK du courtier. Il n'a ni identifiants ni TLS ; utilisez donc un courtier sur le réseau local (ou un relais local qui les ajoute). La remise s'exécute sur son propre fil derrière une file de 1024 : un courtier mort fait perdre des messages, comptés et signalés à la fin, et ne bloque jamais le flux. Une appliance NTP ou tout autre consommateur qui veut du JSON brut peut lire la même ligne d'état. Rien n'est écrit vers un récepteur ou une horloge. Python, WebAssembly et MCP acceptent format = "mqtt" et renvoient les mêmes lignes topic<TAB>payload (rien n'y est publié).
Depuis Python, WebAssembly et MCP#
Les encodeurs sont aussi disponibles sous forme d'appels texte en entrée, texte en sortie, sans processus, socket ni horloge : kshana.trust_telemetry(input, format, host="", input_kind="jsonl", expose_position=False, syslog_all=False, unix_nano=0) en Python, l'export WebAssembly trust_telemetry(input, format, host, input_kind, options_json) et l'outil MCP trust_telemetry (commande de plugin /kshana-trust-telemetry). format vaut prometheus, cef, leef ou otlp ; input_kind vaut jsonl (le flux) ou result (un résultat de lot). Ils renvoient le même texte que celui affiché par la ligne de commande (--print-metrics, --print-syslog), avec les mêmes limites : les lignes syslog ne portent pas d'horodatage, host comporte au plus 255 caractères ASCII imprimables sans espaces (refusé sinon, ici comme en ligne de commande), et une réponse plus grande que la limite de l'interface (4 MiB sur MCP) est refusée, car CEF et LEEF avec syslog_all écrivent un événement par époque. Rien n'est envoyé, servi ni écrit ; les exportateurs en fonctionnement restent un processus de la ligne de commande.
La remise ne bloque jamais l'évaluation#
La remise syslog et OpenTelemetry s'exécute sur ses propres fils. La boucle d'ingestion ne fait que leur transmettre un message ou un instantané sans jamais attendre ; un collecteur mort, lent ou qui ne lit pas ne peut donc ni exercer de contre-pression sur le flux en direct ni geler /metrics :
- chaque connexion et chaque écriture a une limite de 2 s ; une connexion échouée est abandonnée puis rétablie à la demande, avec un recul exponentiel de 1 s jusqu'à 60 s ;
- un message qui ne peut pas être remis (collecteur arrêté, écriture expirée, file de 1024 pleine) est abandonné et compté dans
kshana_trust_syslog_send_failures_total; alertez sur son taux ; - l'exportateur OpenTelemetry reçoit une copie du registre, de sorte qu'aucun verrou n'est tenu pendant les E/S réseau ; si un export est encore en cours, le nouvel instantané est ignoré (le suivant porte les mêmes totaux cumulés).
En fin d'entrée, les fils d'envoi disposent du temps limite qui leur reste pour terminer.
Métriques Prometheus#
| Métrique | Type | Libellés | Signification |
|---|---|---|---|
kshana_build_info |
jauge | version |
constante 1 |
kshana_trust_score |
jauge | dernier score de 0 à 100 ; absent si la source n'en donne pas | |
kshana_trust_band |
jauge | band |
1 pour la bande actuelle, 0 pour les autres |
kshana_trust_gate |
jauge | gate |
off, passed, withheld, unknown : 1 pour l'état actuel, 0 pour les autres ; absent sans barrière |
kshana_trust_position_latitude_degrees, ..._longitude_degrees, ..._height_meters |
jauge | position rapportée par le récepteur ; uniquement avec --expose-position (le point d'accès n'est pas authentifié et une position peut permettre d'identifier un site ou un navire) |
|
kshana_trust_reason_active |
jauge | reason |
1 si présent à la dernière époque |
kshana_trust_epochs_total |
compteur | band |
époques reçues |
kshana_trust_reason_epochs_total |
compteur | reason |
époques dans lesquelles la raison était présente |
kshana_trust_input_errors_total |
compteur | lignes d'entrée non analysables | |
kshana_trust_reason_overflow_total |
compteur | occurrences de raisons regroupées dans reason="other" (voir ci-dessous) |
|
kshana_trust_syslog_send_failures_total |
compteur | événements syslog non remis | |
kshana_trust_epoch_offset_seconds |
jauge | décalage de la dernière époque par rapport au début du flux | |
kshana_trust_last_sample_timestamp_seconds |
jauge | heure de réception en temps réel ; alertez sur son âge | |
kshana_trust_geometry |
jauge | status |
géométrie des satellites à la dernière époque, good, degraded, unknown : 1 pour la valeur actuelle (entrées de schéma 1,3) |
kshana_trust_geometry_degraded |
jauge | 1 dégradée, 0 bonne ; absent lorsque la géométrie est inconnue (1.3) | |
kshana_trust_geometry_signal_loss |
jauge | 1 lorsque des satellites en vue ne sont pas suivis alors que leur géométrie est bonne (c'est le signal qui a été perdu, pas le ciel ; geometry_degraded vaut toujours 1), sinon 0 ; absent lorsque la géométrie est inconnue ou que l'entrée ne le précise pas (1.3) |
|
kshana_trust_geometry_hdop, ..._pdop, ..._used_satellites |
jauge | HDOP, PDOP et satellites utilisés à la dernière époque ; chacun absent s'il n'y en a pas (1.3) | |
kshana_trust_geometry_epochs_total |
compteur | status |
époques par état de géométrie (1.3) |
kshana_trust_geometry_label |
jauge | label |
étiquette de géométrie à la dernière époque, good, fair, poor, unknown : 1 pour l'étiquette actuelle ; jamais notée (le score ne déduit rien sauf pour poor) ; pour les entrées qui portent l'étiquette (0,36) |
kshana_trust_geometry_vdop |
jauge | VDOP à la dernière époque ; absent s'il n'y en a pas (0.36) |
L'étiquette de géométrie est l'indicateur principal. Tracez-la et alertez sur kshana_trust_geometry_label (OTLP kshana.geometry.label, CEF le troisième champ cs6, LEEF geoLabel) : poor est la seule étiquette pour laquelle le score déduit quelque chose. L'état de la géométrie est descriptif : degraded inclut le cas informatif sky-blocked, qui peut s'accompagner d'un DOP bon ou moyen ; ne déclenchez donc pas d'appel d'astreinte sur kshana_trust_geometry_degraded seul.
Pour les entrées de schéma 1,3, kshana_trust_reason_active porte aussi une étiquette class (geometry, environment, inconsistency) sur chaque raison qui en avait une : la classe de la déduction à la dernière époque. Les séries de géométrie viennent après toutes les autres et seulement une fois qu'une époque d'entrée porte geometry (schéma en direct 1,3, résultat d'un moteur 0,36 ou ultérieur), de sorte qu'un flux ou un résultat 0,35 est rendu octet pour octet comme auparavant. Les valeurs d'étiquette sont des ensembles fixes (un état inconnu se lit unknown ; une classe inconnue est écartée). Alertez sur kshana_trust_reason_active{class="inconsistency"} plutôt que sur la bande seule, afin qu'un ciel médiocre ne déclenche pas d'appel comme une attaque ; une classe geometry est un énoncé sur le ciel, et non la constatation qu'aucune attaque n'a eu lieu (voir docs/RECEIVER-TRUST.md).
L'étiquette class fait partie de l'identité d'une série : lorsque la classe d'une raison change entre deux époques (loss-of-lock de environment à geometry lorsque les satellites perdus étaient bas, ou de geometry à inconsistency lorsque le HDOP indiqué est en désaccord avec le ciel indiqué), l'ancienne série n'est plus exportée et une nouvelle commence. Interrogez par reason avec max by (reason) lorsque la classe n'importe pas, et attendez-vous à ce qu'un tel basculement ressemble à une série qui se termine et à une autre qui commence. Les séries du schéma 1,3, l'étiquette class et les champs de géométrie CEF/LEEF sont vérifiés par les propres tests du moteur et par les références indépendantes CEF (pycef, Logstash) ; les références indépendantes externes Prometheus et OTLP n'ont pas encore de flux 1,3.
Les noms de raison sont tronqués à 128 caractères, et au plus 64 raisons distinctes obtiennent leur propre série ; les suivantes sont comptées sous reason="other" et dans kshana_trust_reason_overflow_total, de sorte qu'une entrée hostile ou boguée ne puisse pas créer une cardinalité d'étiquettes illimitée.
band est l'une de calibrating, nominal, degraded, untrusted, unknown. Les noms de raison sont les noms des moniteurs du flux (kinematic, heading-course, ... ; pour le résultat de lot, cn0-drop, agc, raim).
Un tableau de bord d'exemple se trouve dans deploy/grafana/kshana-gnss-trust.json (importez-le et choisissez votre source de données Prometheus).
Syslog : CEF et LEEF#
Les événements sont envoyés dans une enveloppe RFC 5424 (facility local0, application kshana, identifiant de message GNSSTRUST) par UDP (un datagramme chacun ; IPv4 ou IPv6) ou TCP, terminés par un saut de ligne par défaut ou, avec --syslog-octet-counting, encadrés en <length> <message> comme dans la RFC 6587. Par défaut, un événement est envoyé par changement de bande ; --syslog-all en envoie un par époque. Le fournisseur est écrit Ashforde OU (ASCII) afin qu'aucun SIEM ne le décode de travers.
| Concept | Gravité syslog | Gravité CEF | LEEF sev |
|---|---|---|---|
| calibrating | 6 info | 0 | 1 |
| nominal | 6 info | 1 | 1 |
| dégradées | 4 warning | 5 | 5 |
| untrusted | 2 critical | 9 | 9 |
| unknown | 5 notice | 3 | 3 |
Correspondance des champs (l'identifiant d'événement est gnss-trust.<band>) :
| Signification | CEF | LEEF 2,0 (délimiteur ^) |
|---|---|---|
| catégorie | cat=gnss-trust |
cat |
| hôte émetteur | dvchost |
devHost |
| bande | cs1 (étiquette band) |
band |
| raisons, séparées par des virgules | cs2 (reasons) |
reasons |
| bande précédente | cs3 (previousBand) |
previousBand |
| score | cfp1 (trustScore), omis s'il n'y en a pas |
trustScore |
| décalage d'époque, s | cfp2 (epochOffsetSeconds) |
epochOffsetSeconds |
| étiquette d'heure du journal | cs4 (logTime) |
logTime |
| état de la barrière | cs5 (gate) |
gate |
état de la géométrie, puis ; et les classes des déductions de l'époque, séparées par des virgules (par exemple degraded;environment,geometry) (entrées 1,3) ; pour les entrées qui portent l'étiquette (0,36), un troisième champ, ; et l'étiquette, le champ des classes étant alors toujours présent (par exemple degraded;;poor, good;;fair) |
cs6 (geometry) |
geoStatus (état seul), geoClass (classes) et geoLabel (étiquette) |
| HDOP (1.3) | cfp3 (geometryHdop) |
geoHdop |
| PDOP (1.3) | cfp4 (geometryPdop) |
geoPdop (0,36, avec l'étiquette) |
| VDOP (0,36, avec l'étiquette) | non porté : les quatre champs personnalisés en virgule flottante sont pris | geoVdop |
| satellites utilisés (0,36, avec l'étiquette) | cn1 (geometryUsedSatellites), un entier |
geoUsed |
| perte de signal : satellites en vue non suivis alors que leur géométrie est bonne (1.3) | non porté comme champ (aucune clé du dictionnaire CEF n'est libre pour cela) : l'état se lit degraded et la classe de la déduction de géométrie environment dans cs6 |
geoSignalLoss=1, uniquement tant qu'elle est vraie |
En CEF, la géométrie n'utilise que des champs personnalisés du dictionnaire (aucune clé inventée) ; l'étiquette passe dans cs6 plutôt que dans flexString1 parce que le codec CEF de Logstash ne met pas en correspondance les champs flexString. Les événements de géométrie 1,3, y compris les cas d'étiquette, font partie de la ligne CEF VALIDATED : les deux analyseurs de référence indépendante les relisent tels qu'écrits (tests/cef_reference.rs, amendements 4 et 5, chacun enregistré avant que l'un ou l'autre analyseur ait été exécuté sur ses cas). Les champs d'étiquette, de VDOP et de nombre de satellites sont ajoutés après tous les champs et séries précédents, et seulement pour les entrées qui portent l'étiquette, de sorte qu'une entrée sans étiquette est rendue exactement comme avant.
Les champs vides sont omis (pas de cs2= sans raisons). Le nom d'hôte est tronqué à 255 caractères. Les valeurs d'en-tête CEF échappent \ et | ; les valeurs d'extension échappent \, = et les sauts de ligne. Dans les valeurs LEEF, ^ et les sauts de ligne sont remplacés par des espaces.
OpenTelemetry (désactivé par défaut)#
Compilez avec --features otlp et passez --otlp http://host:4318/v1/metrics. L'exportateur envoie en POST du JSON OTLP/HTTP (kshana.trust.score, kshana.trust.band, kshana.trust.epochs, kshana.trust.reason.epochs, et pour les entrées de schéma 1,3 kshana.geometry.status, kshana.geometry.degraded, kshana.geometry.signal_loss, kshana.geometry.hdop, kshana.geometry.pdop, kshana.geometry.used_satellites et, pour les entrées qui portent l'étiquette (0,36), kshana.geometry.label (attribut label) et kshana.geometry.vdop ; les sommes cumulées portent startTimeUnixNano) toutes les --otlp-every époques (par défaut 10) et en fin d'entrée. Il n'ajoute aucune dépendance et n'a pas de TLS : envoyez vers un collecteur sur la même machine ou sur un segment de confiance, qui retransmet ensuite en TLS. Les points d'accès https:// sont refusés.