check: add `--format` flag, json reporter
Pull Request Checklist
- [x] The Rayhunter team has recently expressed interest in reviewing a PR for this. If not, this PR may be closed due our limited resources and need to prioritize how we spend them.
- [x] Added or updated any documentation as needed to support the changes in this PR.
- [x] Code has been linted and run through
cargo fmt - [ ] If any new functionality has been added, unit tests were also added
- No tests are present in
./checkbut I can add them if needed
- No tests are present in
- [x] ./CONTRIBUTING.md has been read
Problem
Rayhunter check output is emitted as plain text and doesn't lend itself to easy analysis by other tools.
Solution
Implement a --report flag, preserve the existing behavior as the default (manually invoked with --report log), and add an ndjson formatter.
Fixes #570.
Verification
Example output
❯ cargo run -- --path ../../rayhunter-traces/1749319445-goodsim.ndjson --report ndjson
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.07s
Running `/Users/adrienthebo/personal/projects/radio/rayhunter-work/rayhunter/target/debug/rayhunter-check --path ../../rayhunter-traces/1749319445-goodsim.ndjson --report ndjson`
INFO [rayhunter_check] Analyzers:
INFO [rayhunter_check] - Identity (IMSI or IMEI) requested in suspicious manner (v3): Tests whether the ME sends an Identity Request NAS message without either an associated attach request or auth accept message
INFO [rayhunter_check] - Connection Release/Redirected Carrier 2G Downgrade (v1): Tests if a cell releases our connection and redirects us to a 2G cell.
INFO [rayhunter_check] - LTE SIB 6/7 Downgrade (v1): Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.
INFO [rayhunter_check] - Null Cipher (v1): Tests whether the cell suggests using a null cipher (EEA0)
INFO [rayhunter_check] - NAS Null Cipher Requested (v1): Tests whether the MME requests to use a null cipher in the NAS security mode command
INFO [rayhunter_check] - Incomplete SIB (v2): Tests whether a SIB1 message contains a full chain of followup sibs
1749319445-goodsim.ndjson
{"analyzers":[{"name":"Identity (IMSI or IMEI) requested in suspicious manner","description":"Tests whether the ME sends an Identity Request NAS message without either an associated attach request or auth accept message","version":3},{"name":"Connection Release/Redirected Carrier 2G Downgrade","description":"Tests if a cell releases our connection and redirects us to a 2G cell.","version":1},{"name":"LTE SIB 6/7 Downgrade","description":"Tests for LTE cells broadcasting a SIB type 6 and 7 which include 2G/3G frequencies with higher priorities.","version":1},{"name":"Null Cipher","description":"Tests whether the cell suggests using a null cipher (EEA0)","version":1},{"name":"NAS Null Cipher Requested","description":"Tests whether the MME requests to use a null cipher in the NAS security mode command","version":1},{"name":"Incomplete SIB","description":"Tests whether a SIB1 message contains a full chain of followup sibs","version":2}],"rayhunter":{"rayhunter_version":"0.7.1","system_os":"Darwin 24.6.0","arch":"arm64"},"report_version":2}
{"packet_timestamp":"2025-06-07T21:03:24.743Z","events":[{"event_type":"Informational","message":"Identity Request happened but its not suspicious yet. (packet 12761)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-07T21:35:41.969Z","events":[{"event_type":"Informational","message":"Identity Request happened but its not suspicious yet. (packet 15242)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-07T22:10:20.991Z","events":[{"event_type":"Informational","message":"Identity Request happened but its not suspicious yet. (packet 18097)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-07T22:10:23.290Z","events":[{"event_type":"Informational","message":"Identity Request happened but its not suspicious yet. (packet 18138)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T03:30:56.706Z","events":[{"event_type":"Informational","message":"SIB1 scheduling info list was malformed (packet 30097)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T03:30:57.067Z","events":[{"event_type":"Informational","message":"SIB1 scheduling info list was malformed (packet 30098)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T03:30:59.228Z","events":[{"event_type":"Informational","message":"SIB1 scheduling info list was malformed (packet 30102)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T08:46:57.462Z","events":[{"event_type":"High","message":"Identity requested after auth request (packet 30765)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T08:46:58.205Z","events":[{"event_type":"High","message":"Disconnected after Identity Request without Auth Accept (packet 30769)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T09:22:13.650Z","events":[{"event_type":"High","message":"Identity requested after auth request (packet 32055)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T09:22:14.535Z","events":[{"event_type":"High","message":"Disconnected after Identity Request without Auth Accept (packet 32059)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T12:20:30.716Z","events":[{"event_type":"High","message":"Identity requested after auth request (packet 35219)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T12:20:31.447Z","events":[{"event_type":"High","message":"Disconnected after Identity Request without Auth Accept (packet 35224)"}],"skipped_message_reason":null}
{"packet_timestamp":"2025-06-08T21:22:34.877Z","events":[{"event_type":"High","message":"Identity requested after auth request (packet 121675)"}],"skipped_message_reason":null}
Considerations
- [x] ~~The use of a
dyn Traitfeels a bit clumsy~~ - [x] Reconcile status logging
- log macros now write to stderr
-
--reportnow writes a file so there's no issue of redirection writing extra logs.
- [x] Deal with multiple files
- as #662 indicates the
--pathflag can be either a file or directory so this may need to emit multiple json reports in one shot - We write one report per file
- as #662 indicates the
I'm pretty slammed with work right now, will follow up on this within the next week.
so far it looks good to me. I am not worried about the use of dyn, ok to keep as is. I would recommend moving all logging to stderr though (unconditionally)
This is ready for review; I haven't written rust in a few years and I'm unattached to this implementation so I'm happy to steer this change wherever needed.
are you still interested in working on this or should we close it?