fix: anomaly detection matched a bare "MM:" prefix, but text is sender-prefixed

decodeGrpTxt (cmd/ingestor/decoder.go) builds decoded_json.text as
"<sender>: <message>", so a wardriving message's real text is
"<sender>: MM:<base64>", not a bare "MM:<base64>" 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 "<sender>: MM:" prefix. The Go test fixtures
shared the same wrong assumption and are fixed too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
dborupandClaude Sonnet 5 committed 2026-07-20 15:28:01 +02:00
1 parent 3453caee53
commit d9d3883a53
2 files changed
+20 -7

No files matched your search

+15 -6
View File
@@ -3537,10 +3537,12 @@ func (db *DB) buildWardrivingSessions(channel, since string) ([]WardrivingSessio
const wardrivingStandardPayloadBytes = 7
// detectWardrivingAnomalies scans every #wardriving message's "MM:<base64>"
// 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 "<sender>: MM:<base64>" in decoded_json.text, per
// decodeGrpTxt's "<sender>: <message>" 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
// "<sender>: 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
// "<sender>: <message>", so the wardriving payload prefix is
// "<sender>: 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++
+5 -1
View File
@@ -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 "<sender>: <message>" convention —
// text is NOT a bare "MM:<base64>", it's sender-prefixed, and
// detectWardrivingAnomalies matches on that exact "<sender>: 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+`"}`,