From d9d3883a53f13a73ac5e39bf6faf32c6a3f2eef0 Mon Sep 17 00:00:00 2001 From: dborup Date: Mon, 20 Jul 2026 15:28:01 +0200 Subject: [PATCH] fix: anomaly detection matched a bare "MM:" prefix, but text is sender-prefixed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit decodeGrpTxt (cmd/ingestor/decoder.go) builds decoded_json.text as ": ", so a wardriving message's real text is ": MM:", not a bare "MM:" at the start of the string. detectWardrivingAnomalies's HasPrefix(text, "MM:") check never matched anything live, silently returning standardPayloadCount=0 and no anomalies — caught after deploying idea 6 to stg and seeing 0/0 against traffic that should have had ~160 standard-format messages. Fixed to match the exact ": MM:" prefix. The Go test fixtures shared the same wrong assumption and are fixed too. Co-Authored-By: Claude Sonnet 5 --- cmd/server/db.go | 21 +++++++++++++++------ cmd/server/wardriving_stats_test.go | 6 +++++- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cmd/server/db.go b/cmd/server/db.go index d6f6741c..875eea2f 100644 --- a/cmd/server/db.go +++ b/cmd/server/db.go @@ -3537,10 +3537,12 @@ func (db *DB) buildWardrivingSessions(channel, since string) ([]WardrivingSessio const wardrivingStandardPayloadBytes = 7 // detectWardrivingAnomalies scans every #wardriving message's "MM:" -// payload (messages without that prefix — e.g. manual human chat on the -// channel — are ignored entirely, not counted either way) and buckets them -// into the standard-length count vs per-sender anomalies for any -// non-standard length or undecodable payload. +// payload — stored as ": MM:" in decoded_json.text, per +// decodeGrpTxt's ": " convention — and buckets them into +// the standard-length count vs per-sender anomalies for any non-standard +// length or undecodable payload. Messages that don't match that exact +// ": MM:" prefix (e.g. manual human chat on the channel) are +// ignored entirely, not counted either way. func (db *DB) detectWardrivingAnomalies(channel, since string) (int, []WardrivingAnomaly, error) { rows, err := db.conn.Query(` SELECT json_extract(decoded_json, '$.sender'), json_extract(decoded_json, '$.text'), first_seen @@ -3567,10 +3569,17 @@ func (db *DB) detectWardrivingAnomalies(channel, since string) (int, []Wardrivin if err := rows.Scan(&sender, &text, &ts); err != nil { continue } - if !text.Valid || !strings.HasPrefix(text.String, "MM:") { + if !sender.Valid || !text.Valid { continue } - payload := strings.TrimPrefix(text.String, "MM:") + // decodeGrpTxt (cmd/ingestor/decoder.go) builds Text as + // ": ", so the wardriving payload prefix is + // ": MM:" — not a bare "MM:" at the start of text. + prefix := sender.String + ": MM:" + if !strings.HasPrefix(text.String, prefix) { + continue + } + payload := strings.TrimPrefix(text.String, prefix) decoded, decErr := base64.RawURLEncoding.DecodeString(payload) if decErr == nil && len(decoded) == wardrivingStandardPayloadBytes { standardCount++ diff --git a/cmd/server/wardriving_stats_test.go b/cmd/server/wardriving_stats_test.go index 810d6a87..8908eadb 100644 --- a/cmd/server/wardriving_stats_test.go +++ b/cmd/server/wardriving_stats_test.go @@ -296,8 +296,12 @@ func TestHandleWardrivingStats_Anomalies(t *testing.T) { t.Fatalf("clear transmissions: %v", err) } - insertTx := func(hash, sender, text string, tsOffset time.Duration) { + // insertTx mirrors decodeGrpTxt's ": " convention — + // text is NOT a bare "MM:", it's sender-prefixed, and + // detectWardrivingAnomalies matches on that exact ": MM:" prefix. + insertTx := func(hash, sender, mmPayload string, tsOffset time.Duration) { ts := time.Now().UTC().Add(tsOffset).Format(time.RFC3339) + text := sender + ": " + mmPayload if _, err := srv.db.conn.Exec( `INSERT INTO transmissions (raw_hex,hash,first_seen,route_type,payload_type,channel_hash,decoded_json) VALUES (?,?,?,1,5,'#wardriving',?)`, "aa", hash, ts, `{"sender":"`+sender+`","text":"`+text+`"}`,