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.
- checksums.yaml +4 -4
- data/README.md +71 -18
- data/lib/raygatherer/api_client.rb +101 -27
- data/lib/raygatherer/cli.rb +39 -17
- data/lib/raygatherer/commands/alerts.rb +7 -12
- data/lib/raygatherer/commands/analysis/report.rb +77 -0
- data/lib/raygatherer/commands/analysis/run.rb +10 -7
- data/lib/raygatherer/commands/analysis/status.rb +1 -9
- data/lib/raygatherer/commands/base.rb +20 -1
- data/lib/raygatherer/commands/config/set.rb +1 -4
- data/lib/raygatherer/commands/config/show.rb +1 -9
- data/lib/raygatherer/commands/config/test_notification.rb +1 -4
- data/lib/raygatherer/commands/debug/display_state.rb +95 -0
- data/lib/raygatherer/commands/gps/set.rb +69 -0
- data/lib/raygatherer/commands/gps/show.rb +54 -0
- data/lib/raygatherer/commands/log.rb +38 -0
- data/lib/raygatherer/commands/recording/delete.rb +55 -11
- data/lib/raygatherer/commands/recording/download.rb +6 -9
- data/lib/raygatherer/commands/recording/list.rb +1 -9
- data/lib/raygatherer/commands/recording/start.rb +2 -5
- data/lib/raygatherer/commands/recording/stop.rb +2 -5
- data/lib/raygatherer/commands/stats.rb +4 -9
- data/lib/raygatherer/commands/time/show.rb +52 -0
- data/lib/raygatherer/commands/time/sync.rb +53 -0
- data/lib/raygatherer/commands/update/status.rb +62 -0
- data/lib/raygatherer/commands/wifi/scan.rb +54 -0
- data/lib/raygatherer/commands/wifi/show.rb +55 -0
- data/lib/raygatherer/formatters/{human.rb → alerts_human.rb} +1 -1
- data/lib/raygatherer/formatters/{json.rb → alerts_json.rb} +2 -2
- data/lib/raygatherer/formatters/analysis_report_human.rb +88 -0
- data/lib/raygatherer/formatters/analysis_report_json.rb +13 -0
- data/lib/raygatherer/formatters/config_human.rb +39 -2
- data/lib/raygatherer/formatters/gps_human.rb +13 -0
- data/lib/raygatherer/formatters/gps_json.rb +13 -0
- data/lib/raygatherer/formatters/recording_list_human.rb +6 -1
- data/lib/raygatherer/formatters/time_human.rb +15 -0
- data/lib/raygatherer/formatters/time_json.rb +13 -0
- data/lib/raygatherer/formatters/update_status_human.rb +25 -0
- data/lib/raygatherer/formatters/update_status_json.rb +13 -0
- data/lib/raygatherer/formatters/wifi_scan_human.rb +17 -0
- data/lib/raygatherer/formatters/wifi_scan_json.rb +13 -0
- data/lib/raygatherer/formatters/wifi_status_human.rb +19 -0
- data/lib/raygatherer/formatters/wifi_status_json.rb +13 -0
- data/lib/raygatherer/version.rb +6 -1
- data/lib/raygatherer.rb +24 -2
- metadata +41 -6
- data/CLAUDE.md +0 -102
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 19099779bda496591260a273c3d6db20c9203f1c046393c2ce6b7c6b90b77db5
|
|
4
|
+
data.tar.gz: 7105312abb9d3d27aafbd452e386f87343b8ebce08308e30e4b7330128d260aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
-
|
|
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
|
-
###
|
|
29
|
+
### Via RubyGems
|
|
24
30
|
|
|
25
|
-
|
|
31
|
+
```bash
|
|
32
|
+
gem install raygatherer
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Ruby >= 3.2.
|
|
36
|
+
|
|
37
|
+
## Compatibility
|
|
26
38
|
|
|
27
|
-
|
|
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
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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
|
|
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://
|
|
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://
|
|
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
|
-
|
|
141
|
-
|
|
213
|
+
with_connection_error_handling do
|
|
214
|
+
url = "#{@host}#{path}"
|
|
215
|
+
uri = URI.parse(url)
|
|
142
216
|
|
|
143
|
-
|
|
144
|
-
|
|
217
|
+
log_verbose "HTTP #{method.to_s.upcase} #{url}"
|
|
218
|
+
log_verbose "Basic Auth: user=#{@username}" if @username
|
|
145
219
|
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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
|
data/lib/raygatherer/cli.rb
CHANGED
|
@@ -16,18 +16,28 @@ module Raygatherer
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
ROUTES = {
|
|
19
|
-
["
|
|
20
|
-
["
|
|
21
|
-
["
|
|
22
|
-
["recording", "
|
|
23
|
-
["recording", "
|
|
24
|
-
["recording", "
|
|
25
|
-
["recording", "
|
|
26
|
-
["
|
|
27
|
-
["analysis", "
|
|
28
|
-
["
|
|
29
|
-
["
|
|
30
|
-
["config", "
|
|
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 =
|
|
48
|
-
cli_json =
|
|
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.
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 = @
|
|
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
|
|
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
|
|