raygatherer 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -18
  3. data/lib/raygatherer/api_client.rb +101 -27
  4. data/lib/raygatherer/cli.rb +39 -17
  5. data/lib/raygatherer/commands/alerts.rb +7 -12
  6. data/lib/raygatherer/commands/analysis/report.rb +77 -0
  7. data/lib/raygatherer/commands/analysis/run.rb +10 -7
  8. data/lib/raygatherer/commands/analysis/status.rb +1 -9
  9. data/lib/raygatherer/commands/base.rb +20 -1
  10. data/lib/raygatherer/commands/config/set.rb +1 -4
  11. data/lib/raygatherer/commands/config/show.rb +1 -9
  12. data/lib/raygatherer/commands/config/test_notification.rb +1 -4
  13. data/lib/raygatherer/commands/debug/display_state.rb +95 -0
  14. data/lib/raygatherer/commands/gps/set.rb +69 -0
  15. data/lib/raygatherer/commands/gps/show.rb +54 -0
  16. data/lib/raygatherer/commands/log.rb +38 -0
  17. data/lib/raygatherer/commands/recording/delete.rb +55 -11
  18. data/lib/raygatherer/commands/recording/download.rb +6 -9
  19. data/lib/raygatherer/commands/recording/list.rb +1 -9
  20. data/lib/raygatherer/commands/recording/start.rb +2 -5
  21. data/lib/raygatherer/commands/recording/stop.rb +2 -5
  22. data/lib/raygatherer/commands/stats.rb +4 -9
  23. data/lib/raygatherer/commands/time/show.rb +52 -0
  24. data/lib/raygatherer/commands/time/sync.rb +53 -0
  25. data/lib/raygatherer/commands/update/status.rb +62 -0
  26. data/lib/raygatherer/commands/wifi/scan.rb +54 -0
  27. data/lib/raygatherer/commands/wifi/show.rb +55 -0
  28. data/lib/raygatherer/formatters/{human.rb → alerts_human.rb} +1 -1
  29. data/lib/raygatherer/formatters/{json.rb → alerts_json.rb} +2 -2
  30. data/lib/raygatherer/formatters/analysis_report_human.rb +88 -0
  31. data/lib/raygatherer/formatters/analysis_report_json.rb +13 -0
  32. data/lib/raygatherer/formatters/config_human.rb +39 -2
  33. data/lib/raygatherer/formatters/gps_human.rb +13 -0
  34. data/lib/raygatherer/formatters/gps_json.rb +13 -0
  35. data/lib/raygatherer/formatters/recording_list_human.rb +6 -1
  36. data/lib/raygatherer/formatters/time_human.rb +15 -0
  37. data/lib/raygatherer/formatters/time_json.rb +13 -0
  38. data/lib/raygatherer/formatters/update_status_human.rb +25 -0
  39. data/lib/raygatherer/formatters/update_status_json.rb +13 -0
  40. data/lib/raygatherer/formatters/wifi_scan_human.rb +17 -0
  41. data/lib/raygatherer/formatters/wifi_scan_json.rb +13 -0
  42. data/lib/raygatherer/formatters/wifi_status_human.rb +19 -0
  43. data/lib/raygatherer/formatters/wifi_status_json.rb +13 -0
  44. data/lib/raygatherer/version.rb +6 -1
  45. data/lib/raygatherer.rb +24 -2
  46. metadata +41 -6
  47. data/CLAUDE.md +0 -102
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aef304a404bc427e2d066db7cf8767e73d06dc8c765b15f18b78afc68a78ee1e
4
- data.tar.gz: e81748ac2ce4443e5a30ff55e63727db50148d7b1402ffebbee280c01f19a8a6
3
+ metadata.gz: 19099779bda496591260a273c3d6db20c9203f1c046393c2ce6b7c6b90b77db5
4
+ data.tar.gz: 7105312abb9d3d27aafbd452e386f87343b8ebce08308e30e4b7330128d260aa
5
5
  SHA512:
6
- metadata.gz: cc2ce523698c34711223d9c88a14360f98850e91650da419acb9b532e6d1e5a8f1bc8db47d418e83acf648878803a4f2811ca65403dbfcbfae071017e0ad6c3b
7
- data.tar.gz: bde55ec5165b8516d9e6295a2b6215db2c54f2399b9c299497591b9d0ebe212a1d2eac19c0d10ace5324816f1a37f477ca06587125110a858488c39a587f283d
6
+ metadata.gz: 68524f1ebfc17554e8fed101331eca036e25217f23c8ada0922e0da5c9f257257314126bebf671a37bb7a8a50ebffca355d2df24cd7dbf8d677951414d865288
7
+ data.tar.gz: d5453cb08bbe54cc9d07fbd1a32d05bce51f9e25ba866ced69bd657fcd3d067c3fe5210aec77f6e193c7a3421867fb0cc4cdfc26eefb61a9f1b8792f4e124c39
data/README.md CHANGED
@@ -12,27 +12,43 @@ Currently implemented:
12
12
 
13
13
  - alerts from live analysis, with severity-based exit codes
14
14
  - recording list/start/stop/delete/download
15
- - system stats
15
+ - analysis report for named or active recordings
16
16
  - analysis queue status and triggering analysis runs
17
+ - system stats and raw log output
18
+ - device clock show and sync
17
19
  - config show/set/test-notification
20
+ - rayhunter update status, with a dedicated exit code for scripting
21
+ - GPS position show/set (requires the device's gps_mode config to be set to Api)
22
+ - wifi client status and network scan
18
23
  - JSON output mode for scriptable commands
19
24
  - optional basic auth and config file support
25
+ - debug utilities (display-state)
20
26
 
21
27
  ## Installation
22
28
 
23
- ### Ruby version
29
+ ### Via RubyGems
24
30
 
25
- `raygatherer` requires Ruby `>= 3.1.0`.
31
+ ```bash
32
+ gem install raygatherer
33
+ ```
34
+
35
+ Requires Ruby >= 3.2.
36
+
37
+ ## Compatibility
26
38
 
27
- ### Install from this repo
39
+ Tested against [rayhunter](https://github.com/EFForg/rayhunter/) v0.12.0 (last synced 2026-09-07). Older rayhunter versions may be missing some fields or endpoints raygatherer expects — commands that already have the connected daemon's version on hand (`stats`, `update status`) print a one-line warning to stderr when it's older than that, without blocking anything.
40
+
41
+ ### From source
28
42
 
29
43
  ```bash
44
+ git clone https://github.com/mjstallard/raygatherer.git
45
+ cd raygatherer
30
46
  bundle install
31
47
  make build
32
48
  make install
33
49
  ```
34
50
 
35
- Or install directly with RubyGems:
51
+ Or build and install the gem directly:
36
52
 
37
53
  ```bash
38
54
  gem build raygatherer.gemspec
@@ -50,37 +66,49 @@ raygatherer --help
50
66
  Check live alerts:
51
67
 
52
68
  ```bash
53
- raygatherer --host http://rayhunter.local alerts
69
+ raygatherer --host http://192.168.1.1 alerts
54
70
  ```
55
71
 
56
72
  Check live alerts as JSON:
57
73
 
58
74
  ```bash
59
- raygatherer --host http://rayhunter.local --json alerts
75
+ raygatherer --host http://192.168.1.1 --json alerts
60
76
  ```
61
77
 
62
78
  List recordings:
63
79
 
64
80
  ```bash
65
- raygatherer --host http://rayhunter.local recording list
81
+ raygatherer --host http://192.168.1.1 recording list
66
82
  ```
67
83
 
68
84
  Download a recording:
69
85
 
70
86
  ```bash
71
- raygatherer --host http://rayhunter.local recording download 1738950000
87
+ raygatherer --host http://192.168.1.1 recording download 1738950000
88
+ ```
89
+
90
+ Show analysis report for a recording:
91
+
92
+ ```bash
93
+ raygatherer --host http://192.168.1.1 analysis report 1738950000
94
+ ```
95
+
96
+ Show analysis report for the active recording:
97
+
98
+ ```bash
99
+ raygatherer --host http://192.168.1.1 analysis report --live
72
100
  ```
73
101
 
74
102
  Show analysis queue status:
75
103
 
76
104
  ```bash
77
- raygatherer --host http://rayhunter.local analysis status
105
+ raygatherer --host http://192.168.1.1 analysis status
78
106
  ```
79
107
 
80
108
  Show system stats:
81
109
 
82
110
  ```bash
83
- raygatherer --host http://rayhunter.local stats
111
+ raygatherer --host http://192.168.1.1 stats
84
112
  ```
85
113
 
86
114
  ## Global Flags
@@ -113,7 +141,7 @@ CLI flags always override config values.
113
141
  Example:
114
142
 
115
143
  ```yaml
116
- host: http://rayhunter.local
144
+ host: http://192.168.1.1
117
145
  basic_auth_user: admin
118
146
  basic_auth_password: replace-me
119
147
  json: false
@@ -126,16 +154,26 @@ Main commands:
126
154
 
127
155
  - `alerts`
128
156
  - `recording list`
129
- - `recording download <name> [--qmdl|--pcap|--zip] [--download-dir DIR|--save-as PATH]`
130
- - `recording delete <name>`
131
- - `recording stop`
132
157
  - `recording start`
158
+ - `recording stop`
159
+ - `recording download <name> [--qmdl|--pcap|--zip] [--download-dir DIR|--save-as PATH]`
160
+ - `recording delete <name> | --all [--force]`
133
161
  - `analysis status`
134
- - `analysis run [NAME|--all]`
162
+ - `analysis run <name> | --all`
163
+ - `analysis report <name> | --live`
164
+ - `time show`
165
+ - `time sync`
135
166
  - `config show`
136
167
  - `config set` (reads JSON from stdin)
137
168
  - `config test-notification`
138
169
  - `stats`
170
+ - `log`
171
+ - `update status`
172
+ - `gps show`
173
+ - `gps set LAT LON` (requires gps_mode=Api on the device)
174
+ - `wifi show`
175
+ - `wifi scan`
176
+ - `debug display-state <recording|paused|warning> [--severity low|medium|high]`
139
177
 
140
178
  For command-specific help:
141
179
 
@@ -164,11 +202,26 @@ raygatherer analysis run --help
164
202
  Example:
165
203
 
166
204
  ```bash
167
- raygatherer --host http://rayhunter.local alerts
205
+ raygatherer --host http://192.168.1.1 alerts
168
206
  code=$?
169
207
  [ "$code" -ge 11 ] && echo "medium or high alert"
170
208
  ```
171
209
 
210
+ ## Update Status Exit Codes
211
+
212
+ `update status` returns a dedicated code so a cron job can react without parsing output:
213
+
214
+ - `0`: up to date (or `auto_check_updates` is disabled on the device)
215
+ - `1`: error
216
+ - `20`: update available
217
+
218
+ Example:
219
+
220
+ ```bash
221
+ raygatherer --host http://192.168.1.1 update status
222
+ [ $? -eq 20 ] && echo "rayhunter update available"
223
+ ```
224
+
172
225
  ## JSON Output
173
226
 
174
227
  Commands that support `--json` return machine-readable output to `stdout`. This is intended for `jq` and/or scripts.
@@ -176,7 +229,7 @@ Commands that support `--json` return machine-readable output to `stdout`. This
176
229
  Example:
177
230
 
178
231
  ```bash
179
- raygatherer --host http://rayhunter.local --json config show | jq '.analyzers'
232
+ raygatherer --host http://192.168.1.1 --json config show | jq '.analyzers'
180
233
  ```
181
234
 
182
235
  ## Development
@@ -73,10 +73,59 @@ module Raygatherer
73
73
  end
74
74
  end
75
75
 
76
+ def fetch_time
77
+ get("/api/time") do |body|
78
+ parse_json(body)
79
+ end
80
+ end
81
+
82
+ def fetch_gps
83
+ get_optional("/api/gps") { |body| parse_json(body) }
84
+ end
85
+
86
+ def set_gps(latitude, longitude)
87
+ post("/api/gps", expected_code: "200",
88
+ body: ::JSON.generate({latitude: latitude, longitude: longitude}),
89
+ content_type: "application/json")
90
+ end
91
+
92
+ def fetch_wifi_status
93
+ get("/api/wifi-status") do |body|
94
+ parse_json(body)
95
+ end
96
+ end
97
+
98
+ def scan_wifi
99
+ parse_json(post("/api/wifi-scan", expected_code: "200"))
100
+ end
101
+
102
+ def fetch_update_status
103
+ get("/api/update-status") do |body|
104
+ parse_json(body)
105
+ end
106
+ end
107
+
108
+ def set_time_offset(offset_seconds)
109
+ post("/api/time-offset", expected_code: "200",
110
+ body: ::JSON.generate({offset_seconds: offset_seconds}),
111
+ content_type: "application/json")
112
+ end
113
+
114
+ def fetch_log
115
+ get("/api/log") do |body|
116
+ log_verbose "Received log (#{body.bytesize} bytes)"
117
+ body
118
+ end
119
+ end
120
+
76
121
  def test_notification
77
122
  post("/api/test-notification", expected_code: "200")
78
123
  end
79
124
 
125
+ def set_display_state(body_json)
126
+ post("/api/debug/display-state", body: body_json, content_type: "application/json", expected_code: "200")
127
+ end
128
+
80
129
  def set_config(json_body)
81
130
  post("/api/config", body: json_body, content_type: "application/json")
82
131
  end
@@ -95,6 +144,10 @@ module Raygatherer
95
144
  post("/api/delete-recording/#{URI.encode_www_form_component(name)}")
96
145
  end
97
146
 
147
+ def delete_all_recordings
148
+ post("/api/delete-all-recordings")
149
+ end
150
+
98
151
  def start_analysis(name)
99
152
  encoded = URI.encode_www_form_component(name)
100
153
  body = post("/api/analysis/#{encoded}")
@@ -124,6 +177,26 @@ module Raygatherer
124
177
  raise
125
178
  end
126
179
 
180
+ def get_optional(path, not_found_code: "404")
181
+ response, body = request(:get, path, ok_code: "200", ok_status_text: "OK")
182
+
183
+ return nil if response.code == not_found_code
184
+
185
+ unless response.is_a?(Net::HTTPSuccess)
186
+ raise ApiError, server_error_message(response, body)
187
+ end
188
+
189
+ yield body
190
+ rescue ParseError => e
191
+ log_verbose "Parse failed: #{e.message}"
192
+ raise
193
+ end
194
+
195
+ # expected_code defaults to "202" (Accepted), matching the daemon's behavior for most POST
196
+ # actions (recordings, analysis, config). A handful of endpoints (gps, wifi-scan, time-offset,
197
+ # test-notification, display-state) respond "200" instead — pass expected_code: "200" for
198
+ # those. This isn't documented by rayhunter anywhere; it's observed per-endpoint behavior, so
199
+ # verify against the real device if you're adding a new one and unsure which applies.
127
200
  def post(path, expected_code: "202", body: nil, content_type: nil)
128
201
  ok_status_text = (expected_code == "202") ? "Accepted" : "OK"
129
202
  response, resp_body = request(:post, path, ok_code: expected_code,
@@ -137,16 +210,20 @@ module Raygatherer
137
210
  end
138
211
 
139
212
  def request(method, path, ok_code:, ok_status_text:, body: nil, content_type: nil)
140
- url = "#{@host}#{path}"
141
- uri = URI.parse(url)
213
+ with_connection_error_handling do
214
+ url = "#{@host}#{path}"
215
+ uri = URI.parse(url)
142
216
 
143
- log_verbose "HTTP #{method.to_s.upcase} #{url}"
144
- log_verbose "Basic Auth: user=#{@username}" if @username
217
+ log_verbose "HTTP #{method.to_s.upcase} #{url}"
218
+ log_verbose "Basic Auth: user=#{@username}" if @username
145
219
 
146
- req = build_request(method, uri, body: body, content_type: content_type)
147
- response, resp_body = execute_request(uri, req, ok_code: ok_code, ok_status_text: ok_status_text)
220
+ req = build_request(method, uri, body: body, content_type: content_type)
221
+ execute_request(uri, req, ok_code: ok_code, ok_status_text: ok_status_text)
222
+ end
223
+ end
148
224
 
149
- [response, resp_body]
225
+ def with_connection_error_handling
226
+ yield
150
227
  rescue SocketError, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
151
228
  log_verbose "Connection error: #{e.class} - #{e.message}"
152
229
  raise ConnectionError, "Failed to connect to #{@host}: #{e.message}"
@@ -193,28 +270,26 @@ module Raygatherer
193
270
  start_time = Time.now
194
271
  log_verbose "Request started at: #{start_time.utc}"
195
272
 
196
- Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
197
- request = Net::HTTP::Get.new(uri.request_uri)
198
- request.basic_auth(@username, @password) if @username && @password
273
+ with_connection_error_handling do
274
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
275
+ req = build_request(:get, uri)
199
276
 
200
- http.request(request) do |response|
201
- elapsed = Time.now - start_time
202
- status_text = (response.code == "200") ? "OK" : response.message.to_s
203
- log_verbose "Response received: #{response.code} #{status_text} (#{format("%.3f", elapsed)}s)"
277
+ http.request(req) do |response|
278
+ elapsed = Time.now - start_time
279
+ status_text = (response.code == "200") ? "OK" : response.message.to_s
280
+ log_verbose "Response received: #{response.code} #{status_text} (#{format("%.3f", elapsed)}s)"
204
281
 
205
- unless response.is_a?(Net::HTTPSuccess)
206
- error_body = response.read_body
207
- raise ApiError, server_error_message(response, error_body)
208
- end
282
+ unless response.is_a?(Net::HTTPSuccess)
283
+ error_body = response.read_body
284
+ raise ApiError, server_error_message(response, error_body)
285
+ end
209
286
 
210
- response.read_body do |chunk|
211
- io.write(chunk)
287
+ response.read_body do |chunk|
288
+ io.write(chunk)
289
+ end
212
290
  end
213
291
  end
214
292
  end
215
- rescue SocketError, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
216
- log_verbose "Connection error: #{e.class} - #{e.message}"
217
- raise ConnectionError, "Failed to connect to #{@host}: #{e.message}"
218
293
  end
219
294
 
220
295
  def server_error_message(response, body)
@@ -256,11 +331,10 @@ module Raygatherer
256
331
  end
257
332
 
258
333
  def normalize_host(host)
259
- # Add http:// if no scheme is present
260
- if !%r{^https?://}.match?(host)
261
- "http://#{host}"
262
- else
334
+ if %r{^https?://}.match?(host)
263
335
  host
336
+ else
337
+ "http://#{host}"
264
338
  end
265
339
  end
266
340
  end
@@ -16,18 +16,28 @@ module Raygatherer
16
16
  end
17
17
 
18
18
  ROUTES = {
19
- ["stats", nil] => {file: "commands/stats", klass: "Commands::Stats", json: true},
20
- ["alerts", nil] => {file: "commands/alerts", klass: "Commands::Alerts", json: true},
21
- ["recording", "list"] => {file: "commands/recording/list", klass: "Commands::Recording::List", json: true},
22
- ["recording", "download"] => {file: "commands/recording/download", klass: "Commands::Recording::Download", json: false},
23
- ["recording", "delete"] => {file: "commands/recording/delete", klass: "Commands::Recording::Delete", json: false},
24
- ["recording", "stop"] => {file: "commands/recording/stop", klass: "Commands::Recording::Stop", json: false},
25
- ["recording", "start"] => {file: "commands/recording/start", klass: "Commands::Recording::Start", json: false},
26
- ["analysis", "status"] => {file: "commands/analysis/status", klass: "Commands::Analysis::Status", json: true},
27
- ["analysis", "run"] => {file: "commands/analysis/run", klass: "Commands::Analysis::Run", json: true},
28
- ["config", "show"] => {file: "commands/config/show", klass: "Commands::Config::Show", json: true},
29
- ["config", "set"] => {file: "commands/config/set", klass: "Commands::Config::Set", json: false},
30
- ["config", "test-notification"] => {file: "commands/config/test_notification", klass: "Commands::Config::TestNotification", json: false}
19
+ ["log", nil] => {klass: "Commands::Log", json: false},
20
+ ["stats", nil] => {klass: "Commands::Stats", json: true},
21
+ ["alerts", nil] => {klass: "Commands::Alerts", json: true},
22
+ ["recording", "list"] => {klass: "Commands::Recording::List", json: true},
23
+ ["recording", "download"] => {klass: "Commands::Recording::Download", json: false},
24
+ ["recording", "delete"] => {klass: "Commands::Recording::Delete", json: false},
25
+ ["recording", "stop"] => {klass: "Commands::Recording::Stop", json: false},
26
+ ["recording", "start"] => {klass: "Commands::Recording::Start", json: false},
27
+ ["analysis", "status"] => {klass: "Commands::Analysis::Status", json: true},
28
+ ["analysis", "run"] => {klass: "Commands::Analysis::Run", json: true},
29
+ ["analysis", "report"] => {klass: "Commands::Analysis::Report", json: true},
30
+ ["config", "show"] => {klass: "Commands::Config::Show", json: true},
31
+ ["config", "set"] => {klass: "Commands::Config::Set", json: false},
32
+ ["config", "test-notification"] => {klass: "Commands::Config::TestNotification", json: false},
33
+ ["debug", "display-state"] => {klass: "Commands::Debug::DisplayState", json: false},
34
+ ["time", "show"] => {klass: "Commands::Time::Show", json: true},
35
+ ["time", "sync"] => {klass: "Commands::Time::Sync", json: false},
36
+ ["update", "status"] => {klass: "Commands::Update::Status", json: true},
37
+ ["gps", "show"] => {klass: "Commands::Gps::Show", json: true},
38
+ ["gps", "set"] => {klass: "Commands::Gps::Set", json: false},
39
+ ["wifi", "show"] => {klass: "Commands::Wifi::Show", json: true},
40
+ ["wifi", "scan"] => {klass: "Commands::Wifi::Scan", json: true}
31
41
  }.freeze
32
42
 
33
43
  def self.run(argv, stdout: $stdout, stderr: $stderr, config: Config.new)
@@ -44,8 +54,8 @@ module Raygatherer
44
54
 
45
55
  def run
46
56
  # Extract global flags BEFORE processing
47
- cli_verbose = @argv.delete("--verbose") ? true : nil
48
- cli_json = @argv.delete("--json") ? true : nil
57
+ cli_verbose = extract_boolean_flag("--verbose")
58
+ cli_json = extract_boolean_flag("--json")
49
59
  cli_host = extract_value_flag("--host")
50
60
  cli_username = extract_value_flag("--basic-auth-user")
51
61
  cli_password = extract_value_flag("--basic-auth-password")
@@ -86,7 +96,6 @@ module Raygatherer
86
96
  return Commands::EXIT_CODE_ERROR
87
97
  end
88
98
 
89
- require_relative route[:file]
90
99
  return Commands::EXIT_CODE_ERROR unless require_host!
91
100
 
92
101
  kwargs = {stdout: @stdout, stderr: @stderr, api_client: build_api_client}
@@ -145,11 +154,14 @@ module Raygatherer
145
154
  name.split("::").reduce(Raygatherer) { |mod, part| mod.const_get(part) }
146
155
  end
147
156
 
157
+ def extract_boolean_flag(flag)
158
+ @argv.delete(flag) ? true : nil
159
+ end
160
+
148
161
  def extract_value_flag(flag)
149
162
  index = @argv.index(flag)
150
163
  return nil unless index
151
- @argv.delete_at(index) # remove the flag
152
- @argv.delete_at(index) # remove the value (shifted into same index)
164
+ @argv.slice!(index, 2)&.last
153
165
  end
154
166
 
155
167
  def show_help(output = @stdout)
@@ -174,9 +186,19 @@ module Raygatherer
174
186
  output.puts " analysis status Show analysis queue status"
175
187
  output.puts " analysis run <name> Queue a recording for analysis"
176
188
  output.puts " analysis run --all Queue all recordings for analysis"
189
+ output.puts " analysis report <name> Show the full analysis report for a recording"
177
190
  output.puts " config show Show device configuration"
178
191
  output.puts " config set Update device configuration (reads JSON from stdin)"
179
192
  output.puts " config test-notification Send a test notification"
193
+ output.puts " debug display-state STATE Set device display state (recording, paused, warning)"
194
+ output.puts " time show Show device time (system, adjusted, offset)"
195
+ output.puts " time sync Sync device clock to this machine's time"
196
+ output.puts " update status Show rayhunter daemon update status"
197
+ output.puts " gps show Show last known GPS position"
198
+ output.puts " gps set LAT LON Submit a GPS position (requires gps_mode=Api on device)"
199
+ output.puts " wifi show Show wifi client connection status"
200
+ output.puts " wifi scan Scan for available wifi networks"
201
+ output.puts " log Download the device log"
180
202
  output.puts " stats Show device system stats"
181
203
  output.puts ""
182
204
  output.puts "Configuration:"
@@ -3,7 +3,8 @@
3
3
  require "optparse"
4
4
  require "time"
5
5
  require_relative "base"
6
- require_relative "../formatters/json"
6
+ require_relative "../formatters/alerts_json"
7
+ require_relative "../formatters/alerts_human"
7
8
 
8
9
  module Raygatherer
9
10
  module Commands
@@ -20,8 +21,7 @@ module Raygatherer
20
21
  EXIT_CODE_HIGH_SEVERITY = 12
21
22
 
22
23
  def initialize(argv, stdout: $stdout, stderr: $stderr, api_client: nil, json: false)
23
- super(argv, stdout: stdout, stderr: stderr, api_client: api_client)
24
- @json = json
24
+ super
25
25
  @latest = false
26
26
  @after = nil
27
27
  @recording = nil
@@ -40,11 +40,9 @@ module Raygatherer
40
40
  alerts = filter_after(alerts) if @after
41
41
  alerts = filter_latest(alerts, data[:rows]) if @latest
42
42
 
43
- # Select formatter based on --json flag
44
- formatter = @json ? Formatters::JSON.new : Formatters::Human.new
43
+ formatter = @json ? Formatters::AlertsJSON.new : Formatters::AlertsHuman.new
45
44
  @stdout.puts formatter.format(alerts)
46
45
 
47
- # Return severity-based exit code
48
46
  severity_exit_code(alerts)
49
47
  end
50
48
  end
@@ -69,10 +67,7 @@ module Raygatherer
69
67
  @after = parse_timestamp(ts)
70
68
  end
71
69
 
72
- opts.on("-h", "--help", "Show this help message") do
73
- show_help
74
- raise CLI::EarlyExit, EXIT_CODE_SUCCESS
75
- end
70
+ add_help_option(opts)
76
71
  end.parse!(@argv)
77
72
  end
78
73
 
@@ -105,13 +100,13 @@ module Raygatherer
105
100
  end
106
101
 
107
102
  def parse_timestamp(str)
108
- Time.parse(str)
103
+ ::Time.parse(str)
109
104
  rescue ArgumentError
110
105
  raise ArgumentError, "invalid timestamp: #{str}"
111
106
  end
112
107
 
113
108
  def filter_after(alerts)
114
- alerts.select { |a| a[:packet_timestamp] && Time.parse(a[:packet_timestamp]) > @after }
109
+ alerts.select { |a| a[:packet_timestamp] && ::Time.parse(a[:packet_timestamp]) > @after }
115
110
  end
116
111
 
117
112
  def filter_latest(alerts, rows)
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "../base"
5
+ require_relative "../../formatters/analysis_report_json"
6
+ require_relative "../../formatters/analysis_report_human"
7
+
8
+ module Raygatherer
9
+ module Commands
10
+ module Analysis
11
+ class Report < Base
12
+ def initialize(argv, stdout: $stdout, stderr: $stderr, api_client: nil, json: false)
13
+ super
14
+ @live = false
15
+ end
16
+
17
+ def run
18
+ with_error_handling do
19
+ parse_options
20
+
21
+ name = @argv.shift
22
+
23
+ if @live && name
24
+ @stderr.puts "Error: cannot use --live with a recording name"
25
+ return EXIT_CODE_ERROR
26
+ end
27
+
28
+ if !@live && name.nil?
29
+ @stderr.puts "Error: recording name or --live is required"
30
+ return EXIT_CODE_ERROR
31
+ end
32
+
33
+ data = @live ? @api_client.fetch_live_analysis_report : @api_client.fetch_analysis_report(name)
34
+
35
+ formatter = @json ? Formatters::AnalysisReportJSON.new : Formatters::AnalysisReportHuman.new
36
+ @stdout.puts formatter.format(data)
37
+
38
+ EXIT_CODE_SUCCESS
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ def parse_options
45
+ OptionParser.new do |opts|
46
+ opts.banner = "Usage: raygatherer analysis report [options] NAME"
47
+ opts.separator ""
48
+ opts.separator "Options:"
49
+
50
+ opts.on("--live", "Show analysis report for the currently active recording") do
51
+ @live = true
52
+ end
53
+
54
+ add_help_option(opts)
55
+ end.parse!(@argv)
56
+ end
57
+
58
+ def show_help(output = @stdout)
59
+ output.puts "Usage: raygatherer [global options] analysis report [options] [NAME]"
60
+ output.puts ""
61
+ output.puts "Show the full analysis report for a named recording, or the active recording."
62
+ output.puts ""
63
+ output.puts "Options:"
64
+ output.puts " --live Show analysis report for the currently active recording"
65
+ output.puts " -h, --help Show this help message"
66
+ output.puts ""
67
+ print_global_options(output, json: true)
68
+ output.puts ""
69
+ output.puts "Examples:"
70
+ output.puts " raygatherer --host http://192.168.1.100:8080 analysis report 1738950000"
71
+ output.puts " raygatherer --host http://rayhunter --json analysis report 1738950000"
72
+ output.puts " raygatherer --host http://rayhunter analysis report --live"
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -10,8 +10,7 @@ module Raygatherer
10
10
  module Analysis
11
11
  class Run < Base
12
12
  def initialize(argv, stdout: $stdout, stderr: $stderr, api_client: nil, json: false)
13
- super(argv, stdout: stdout, stderr: stderr, api_client: api_client)
14
- @json = json
13
+ super
15
14
  @all = false
16
15
  end
17
16
 
@@ -31,7 +30,7 @@ module Raygatherer
31
30
  return EXIT_CODE_ERROR
32
31
  end
33
32
 
34
- status = @api_client.start_analysis(@all ? "" : name)
33
+ status = @all ? run_all : @api_client.start_analysis(name)
35
34
 
36
35
  formatter = @json ? Formatters::AnalysisStatusJSON.new : Formatters::AnalysisStatusHuman.new
37
36
  @stdout.puts formatter.format(status)
@@ -42,6 +41,13 @@ module Raygatherer
42
41
 
43
42
  private
44
43
 
44
+ def run_all
45
+ manifest = @api_client.fetch_manifest
46
+ names = manifest["entries"].map { |e| e["name"] }
47
+ names.each { |name| @api_client.start_analysis(name) }
48
+ @api_client.fetch_analysis_status
49
+ end
50
+
45
51
  def parse_options
46
52
  OptionParser.new do |opts|
47
53
  opts.banner = "Usage: raygatherer analysis run [options] [NAME]"
@@ -52,10 +58,7 @@ module Raygatherer
52
58
  @all = true
53
59
  end
54
60
 
55
- opts.on("-h", "--help", "Show this help message") do
56
- show_help
57
- raise CLI::EarlyExit, EXIT_CODE_SUCCESS
58
- end
61
+ add_help_option(opts)
59
62
  end.parse!(@argv)
60
63
  end
61
64