raygatherer 0.1.0 → 0.2.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 +44 -18
- data/lib/raygatherer/api_client.rb +30 -4
- data/lib/raygatherer/cli.rb +18 -5
- data/lib/raygatherer/commands/alerts.rb +5 -4
- data/lib/raygatherer/commands/analysis/report.rb +81 -0
- data/lib/raygatherer/commands/analysis/run.rb +8 -1
- data/lib/raygatherer/commands/debug/display_state.rb +98 -0
- data/lib/raygatherer/commands/log.rb +41 -0
- data/lib/raygatherer/commands/recording/delete.rb +54 -7
- data/lib/raygatherer/commands/recording/download.rb +5 -5
- data/lib/raygatherer/commands/recording/start.rb +1 -1
- data/lib/raygatherer/commands/recording/stop.rb +1 -1
- data/lib/raygatherer/commands/time/show.rb +60 -0
- data/lib/raygatherer/commands/time/sync.rb +56 -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/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/version.rb +1 -1
- data/lib/raygatherer.rb +11 -2
- metadata +13 -5
- 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: a9324c6a164aa329514f237d51e7942f8d2399e698fcb500de86440aef9320f0
|
|
4
|
+
data.tar.gz: 5412fc1973d54829b42684ea37d73df8ab4cf6a85a2ecfd2b1248e9c45833817
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e34d2c21750b31b1d0e33c8334233748b6d7aca62eb6879912226e644f8cc7f2076b815937a3f05b1853595b8aa28f25b61ae06f5e3e13d0ce8fe794ee58fe73
|
|
7
|
+
data.tar.gz: e7e7c1bbb88a75a9b588713dfec1692fcbf2f9e0bbf126a82dc517b86d1d7279cf48ccd8a3e1fba7b210d89e2507d64777ca17b14617fba88e9bc52e27aa80f7
|
data/README.md
CHANGED
|
@@ -12,27 +12,36 @@ 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
|
|
18
20
|
- JSON output mode for scriptable commands
|
|
19
21
|
- optional basic auth and config file support
|
|
22
|
+
- debug utilities (display-state)
|
|
20
23
|
|
|
21
24
|
## Installation
|
|
22
25
|
|
|
23
|
-
###
|
|
26
|
+
### Via RubyGems
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
```bash
|
|
29
|
+
gem install raygatherer
|
|
30
|
+
```
|
|
26
31
|
|
|
27
|
-
|
|
32
|
+
Requires Ruby >= 3.2.
|
|
33
|
+
|
|
34
|
+
### From source
|
|
28
35
|
|
|
29
36
|
```bash
|
|
37
|
+
git clone https://github.com/mjstallard/raygatherer.git
|
|
38
|
+
cd raygatherer
|
|
30
39
|
bundle install
|
|
31
40
|
make build
|
|
32
41
|
make install
|
|
33
42
|
```
|
|
34
43
|
|
|
35
|
-
Or install
|
|
44
|
+
Or build and install the gem directly:
|
|
36
45
|
|
|
37
46
|
```bash
|
|
38
47
|
gem build raygatherer.gemspec
|
|
@@ -50,37 +59,49 @@ raygatherer --help
|
|
|
50
59
|
Check live alerts:
|
|
51
60
|
|
|
52
61
|
```bash
|
|
53
|
-
raygatherer --host http://
|
|
62
|
+
raygatherer --host http://192.168.1.1 alerts
|
|
54
63
|
```
|
|
55
64
|
|
|
56
65
|
Check live alerts as JSON:
|
|
57
66
|
|
|
58
67
|
```bash
|
|
59
|
-
raygatherer --host http://
|
|
68
|
+
raygatherer --host http://192.168.1.1 --json alerts
|
|
60
69
|
```
|
|
61
70
|
|
|
62
71
|
List recordings:
|
|
63
72
|
|
|
64
73
|
```bash
|
|
65
|
-
raygatherer --host http://
|
|
74
|
+
raygatherer --host http://192.168.1.1 recording list
|
|
66
75
|
```
|
|
67
76
|
|
|
68
77
|
Download a recording:
|
|
69
78
|
|
|
70
79
|
```bash
|
|
71
|
-
raygatherer --host http://
|
|
80
|
+
raygatherer --host http://192.168.1.1 recording download 1738950000
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Show analysis report for a recording:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
raygatherer --host http://192.168.1.1 analysis report 1738950000
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Show analysis report for the active recording:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
raygatherer --host http://192.168.1.1 analysis report --live
|
|
72
93
|
```
|
|
73
94
|
|
|
74
95
|
Show analysis queue status:
|
|
75
96
|
|
|
76
97
|
```bash
|
|
77
|
-
raygatherer --host http://
|
|
98
|
+
raygatherer --host http://192.168.1.1 analysis status
|
|
78
99
|
```
|
|
79
100
|
|
|
80
101
|
Show system stats:
|
|
81
102
|
|
|
82
103
|
```bash
|
|
83
|
-
raygatherer --host http://
|
|
104
|
+
raygatherer --host http://192.168.1.1 stats
|
|
84
105
|
```
|
|
85
106
|
|
|
86
107
|
## Global Flags
|
|
@@ -113,7 +134,7 @@ CLI flags always override config values.
|
|
|
113
134
|
Example:
|
|
114
135
|
|
|
115
136
|
```yaml
|
|
116
|
-
host: http://
|
|
137
|
+
host: http://192.168.1.1
|
|
117
138
|
basic_auth_user: admin
|
|
118
139
|
basic_auth_password: replace-me
|
|
119
140
|
json: false
|
|
@@ -126,16 +147,21 @@ Main commands:
|
|
|
126
147
|
|
|
127
148
|
- `alerts`
|
|
128
149
|
- `recording list`
|
|
129
|
-
- `recording download <name> [--qmdl|--pcap|--zip] [--download-dir DIR|--save-as PATH]`
|
|
130
|
-
- `recording delete <name>`
|
|
131
|
-
- `recording stop`
|
|
132
150
|
- `recording start`
|
|
151
|
+
- `recording stop`
|
|
152
|
+
- `recording download <name> [--qmdl|--pcap|--zip] [--download-dir DIR|--save-as PATH]`
|
|
153
|
+
- `recording delete <name> | --all [--force]`
|
|
133
154
|
- `analysis status`
|
|
134
|
-
- `analysis run
|
|
155
|
+
- `analysis run <name> | --all`
|
|
156
|
+
- `analysis report <name> | --live`
|
|
157
|
+
- `time show`
|
|
158
|
+
- `time sync`
|
|
135
159
|
- `config show`
|
|
136
160
|
- `config set` (reads JSON from stdin)
|
|
137
161
|
- `config test-notification`
|
|
138
162
|
- `stats`
|
|
163
|
+
- `log`
|
|
164
|
+
- `debug display-state <recording|paused|warning> [--severity low|medium|high]`
|
|
139
165
|
|
|
140
166
|
For command-specific help:
|
|
141
167
|
|
|
@@ -164,7 +190,7 @@ raygatherer analysis run --help
|
|
|
164
190
|
Example:
|
|
165
191
|
|
|
166
192
|
```bash
|
|
167
|
-
raygatherer --host http://
|
|
193
|
+
raygatherer --host http://192.168.1.1 alerts
|
|
168
194
|
code=$?
|
|
169
195
|
[ "$code" -ge 11 ] && echo "medium or high alert"
|
|
170
196
|
```
|
|
@@ -176,7 +202,7 @@ Commands that support `--json` return machine-readable output to `stdout`. This
|
|
|
176
202
|
Example:
|
|
177
203
|
|
|
178
204
|
```bash
|
|
179
|
-
raygatherer --host http://
|
|
205
|
+
raygatherer --host http://192.168.1.1 --json config show | jq '.analyzers'
|
|
180
206
|
```
|
|
181
207
|
|
|
182
208
|
## Development
|
|
@@ -73,10 +73,33 @@ 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 set_time_offset(offset_seconds)
|
|
83
|
+
post("/api/time-offset", expected_code: "200",
|
|
84
|
+
body: ::JSON.generate({offset_seconds: offset_seconds}),
|
|
85
|
+
content_type: "application/json")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def fetch_log
|
|
89
|
+
get("/api/log") do |body|
|
|
90
|
+
log_verbose "Received log (#{body.bytesize} bytes)"
|
|
91
|
+
body
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
76
95
|
def test_notification
|
|
77
96
|
post("/api/test-notification", expected_code: "200")
|
|
78
97
|
end
|
|
79
98
|
|
|
99
|
+
def set_display_state(body_json)
|
|
100
|
+
post("/api/debug/display-state", body: body_json, content_type: "application/json", expected_code: "200")
|
|
101
|
+
end
|
|
102
|
+
|
|
80
103
|
def set_config(json_body)
|
|
81
104
|
post("/api/config", body: json_body, content_type: "application/json")
|
|
82
105
|
end
|
|
@@ -95,6 +118,10 @@ module Raygatherer
|
|
|
95
118
|
post("/api/delete-recording/#{URI.encode_www_form_component(name)}")
|
|
96
119
|
end
|
|
97
120
|
|
|
121
|
+
def delete_all_recordings
|
|
122
|
+
post("/api/delete-all-recordings")
|
|
123
|
+
end
|
|
124
|
+
|
|
98
125
|
def start_analysis(name)
|
|
99
126
|
encoded = URI.encode_www_form_component(name)
|
|
100
127
|
body = post("/api/analysis/#{encoded}")
|
|
@@ -256,11 +283,10 @@ module Raygatherer
|
|
|
256
283
|
end
|
|
257
284
|
|
|
258
285
|
def normalize_host(host)
|
|
259
|
-
|
|
260
|
-
if !%r{^https?://}.match?(host)
|
|
261
|
-
"http://#{host}"
|
|
262
|
-
else
|
|
286
|
+
if %r{^https?://}.match?(host)
|
|
263
287
|
host
|
|
288
|
+
else
|
|
289
|
+
"http://#{host}"
|
|
264
290
|
end
|
|
265
291
|
end
|
|
266
292
|
end
|
data/lib/raygatherer/cli.rb
CHANGED
|
@@ -16,6 +16,7 @@ module Raygatherer
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
ROUTES = {
|
|
19
|
+
["log", nil] => {file: "commands/log", klass: "Commands::Log", json: false},
|
|
19
20
|
["stats", nil] => {file: "commands/stats", klass: "Commands::Stats", json: true},
|
|
20
21
|
["alerts", nil] => {file: "commands/alerts", klass: "Commands::Alerts", json: true},
|
|
21
22
|
["recording", "list"] => {file: "commands/recording/list", klass: "Commands::Recording::List", json: true},
|
|
@@ -25,9 +26,13 @@ module Raygatherer
|
|
|
25
26
|
["recording", "start"] => {file: "commands/recording/start", klass: "Commands::Recording::Start", json: false},
|
|
26
27
|
["analysis", "status"] => {file: "commands/analysis/status", klass: "Commands::Analysis::Status", json: true},
|
|
27
28
|
["analysis", "run"] => {file: "commands/analysis/run", klass: "Commands::Analysis::Run", json: true},
|
|
29
|
+
["analysis", "report"] => {file: "commands/analysis/report", klass: "Commands::Analysis::Report", json: true},
|
|
28
30
|
["config", "show"] => {file: "commands/config/show", klass: "Commands::Config::Show", json: true},
|
|
29
31
|
["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}
|
|
32
|
+
["config", "test-notification"] => {file: "commands/config/test_notification", klass: "Commands::Config::TestNotification", json: false},
|
|
33
|
+
["debug", "display-state"] => {file: "commands/debug/display_state", klass: "Commands::Debug::DisplayState", json: false},
|
|
34
|
+
["time", "show"] => {file: "commands/time/show", klass: "Commands::Time::Show", json: true},
|
|
35
|
+
["time", "sync"] => {file: "commands/time/sync", klass: "Commands::Time::Sync", json: false}
|
|
31
36
|
}.freeze
|
|
32
37
|
|
|
33
38
|
def self.run(argv, stdout: $stdout, stderr: $stderr, config: Config.new)
|
|
@@ -44,8 +49,8 @@ module Raygatherer
|
|
|
44
49
|
|
|
45
50
|
def run
|
|
46
51
|
# Extract global flags BEFORE processing
|
|
47
|
-
cli_verbose =
|
|
48
|
-
cli_json =
|
|
52
|
+
cli_verbose = extract_boolean_flag("--verbose")
|
|
53
|
+
cli_json = extract_boolean_flag("--json")
|
|
49
54
|
cli_host = extract_value_flag("--host")
|
|
50
55
|
cli_username = extract_value_flag("--basic-auth-user")
|
|
51
56
|
cli_password = extract_value_flag("--basic-auth-password")
|
|
@@ -145,11 +150,14 @@ module Raygatherer
|
|
|
145
150
|
name.split("::").reduce(Raygatherer) { |mod, part| mod.const_get(part) }
|
|
146
151
|
end
|
|
147
152
|
|
|
153
|
+
def extract_boolean_flag(flag)
|
|
154
|
+
@argv.delete(flag) ? true : nil
|
|
155
|
+
end
|
|
156
|
+
|
|
148
157
|
def extract_value_flag(flag)
|
|
149
158
|
index = @argv.index(flag)
|
|
150
159
|
return nil unless index
|
|
151
|
-
@argv.
|
|
152
|
-
@argv.delete_at(index) # remove the value (shifted into same index)
|
|
160
|
+
@argv.slice!(index, 2)&.last
|
|
153
161
|
end
|
|
154
162
|
|
|
155
163
|
def show_help(output = @stdout)
|
|
@@ -174,9 +182,14 @@ module Raygatherer
|
|
|
174
182
|
output.puts " analysis status Show analysis queue status"
|
|
175
183
|
output.puts " analysis run <name> Queue a recording for analysis"
|
|
176
184
|
output.puts " analysis run --all Queue all recordings for analysis"
|
|
185
|
+
output.puts " analysis report <name> Show the full analysis report for a recording"
|
|
177
186
|
output.puts " config show Show device configuration"
|
|
178
187
|
output.puts " config set Update device configuration (reads JSON from stdin)"
|
|
179
188
|
output.puts " config test-notification Send a test notification"
|
|
189
|
+
output.puts " debug display-state STATE Set device display state (recording, paused, warning)"
|
|
190
|
+
output.puts " time show Show device time (system, adjusted, offset)"
|
|
191
|
+
output.puts " time sync Sync device clock to this machine's time"
|
|
192
|
+
output.puts " log Download the device log"
|
|
180
193
|
output.puts " stats Show device system stats"
|
|
181
194
|
output.puts ""
|
|
182
195
|
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
|
|
@@ -41,7 +42,7 @@ module Raygatherer
|
|
|
41
42
|
alerts = filter_latest(alerts, data[:rows]) if @latest
|
|
42
43
|
|
|
43
44
|
# Select formatter based on --json flag
|
|
44
|
-
formatter = @json ? Formatters::
|
|
45
|
+
formatter = @json ? Formatters::AlertsJSON.new : Formatters::AlertsHuman.new
|
|
45
46
|
@stdout.puts formatter.format(alerts)
|
|
46
47
|
|
|
47
48
|
# Return severity-based exit code
|
|
@@ -105,13 +106,13 @@ module Raygatherer
|
|
|
105
106
|
end
|
|
106
107
|
|
|
107
108
|
def parse_timestamp(str)
|
|
108
|
-
Time.parse(str)
|
|
109
|
+
::Time.parse(str)
|
|
109
110
|
rescue ArgumentError
|
|
110
111
|
raise ArgumentError, "invalid timestamp: #{str}"
|
|
111
112
|
end
|
|
112
113
|
|
|
113
114
|
def filter_after(alerts)
|
|
114
|
-
alerts.select { |a| a[:packet_timestamp] && Time.parse(a[:packet_timestamp]) > @after }
|
|
115
|
+
alerts.select { |a| a[:packet_timestamp] && ::Time.parse(a[:packet_timestamp]) > @after }
|
|
115
116
|
end
|
|
116
117
|
|
|
117
118
|
def filter_latest(alerts, rows)
|
|
@@ -0,0 +1,81 @@
|
|
|
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(argv, stdout: stdout, stderr: stderr, api_client: api_client)
|
|
14
|
+
@json = json
|
|
15
|
+
@live = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
with_error_handling do
|
|
20
|
+
parse_options
|
|
21
|
+
|
|
22
|
+
name = @argv.shift
|
|
23
|
+
|
|
24
|
+
if @live && name
|
|
25
|
+
@stderr.puts "Error: cannot use --live with a recording name"
|
|
26
|
+
return EXIT_CODE_ERROR
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
if !@live && name.nil?
|
|
30
|
+
@stderr.puts "Error: recording name or --live is required"
|
|
31
|
+
return EXIT_CODE_ERROR
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
data = @live ? @api_client.fetch_live_analysis_report : @api_client.fetch_analysis_report(name)
|
|
35
|
+
|
|
36
|
+
formatter = @json ? Formatters::AnalysisReportJSON.new : Formatters::AnalysisReportHuman.new
|
|
37
|
+
@stdout.puts formatter.format(data)
|
|
38
|
+
|
|
39
|
+
EXIT_CODE_SUCCESS
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def parse_options
|
|
46
|
+
OptionParser.new do |opts|
|
|
47
|
+
opts.banner = "Usage: raygatherer analysis report [options] NAME"
|
|
48
|
+
opts.separator ""
|
|
49
|
+
opts.separator "Options:"
|
|
50
|
+
|
|
51
|
+
opts.on("--live", "Show analysis report for the currently active recording") do
|
|
52
|
+
@live = true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
56
|
+
show_help
|
|
57
|
+
raise CLI::EarlyExit, EXIT_CODE_SUCCESS
|
|
58
|
+
end
|
|
59
|
+
end.parse!(@argv)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def show_help(output = @stdout)
|
|
63
|
+
output.puts "Usage: raygatherer [global options] analysis report [options] [NAME]"
|
|
64
|
+
output.puts ""
|
|
65
|
+
output.puts "Show the full analysis report for a named recording, or the active recording."
|
|
66
|
+
output.puts ""
|
|
67
|
+
output.puts "Options:"
|
|
68
|
+
output.puts " --live Show analysis report for the currently active recording"
|
|
69
|
+
output.puts " -h, --help Show this help message"
|
|
70
|
+
output.puts ""
|
|
71
|
+
print_global_options(output, json: true)
|
|
72
|
+
output.puts ""
|
|
73
|
+
output.puts "Examples:"
|
|
74
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 analysis report 1738950000"
|
|
75
|
+
output.puts " raygatherer --host http://rayhunter --json analysis report 1738950000"
|
|
76
|
+
output.puts " raygatherer --host http://rayhunter analysis report --live"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -31,7 +31,7 @@ module Raygatherer
|
|
|
31
31
|
return EXIT_CODE_ERROR
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
-
status = @
|
|
34
|
+
status = @all ? run_all : @api_client.start_analysis(name)
|
|
35
35
|
|
|
36
36
|
formatter = @json ? Formatters::AnalysisStatusJSON.new : Formatters::AnalysisStatusHuman.new
|
|
37
37
|
@stdout.puts formatter.format(status)
|
|
@@ -42,6 +42,13 @@ module Raygatherer
|
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
|
+
def run_all
|
|
46
|
+
manifest = @api_client.fetch_manifest
|
|
47
|
+
names = manifest["entries"].map { |e| e["name"] }
|
|
48
|
+
names.each { |name| @api_client.start_analysis(name) }
|
|
49
|
+
@api_client.fetch_analysis_status
|
|
50
|
+
end
|
|
51
|
+
|
|
45
52
|
def parse_options
|
|
46
53
|
OptionParser.new do |opts|
|
|
47
54
|
opts.banner = "Usage: raygatherer analysis run [options] [NAME]"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../base"
|
|
6
|
+
|
|
7
|
+
module Raygatherer
|
|
8
|
+
module Commands
|
|
9
|
+
module Debug
|
|
10
|
+
class DisplayState < Base
|
|
11
|
+
VALID_STATES = %w[recording paused warning].freeze
|
|
12
|
+
VALID_SEVERITIES = %w[low medium high].freeze
|
|
13
|
+
|
|
14
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, api_client: nil)
|
|
15
|
+
super
|
|
16
|
+
@severity = nil
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
with_error_handling do
|
|
21
|
+
parse_options
|
|
22
|
+
|
|
23
|
+
state = @argv.shift
|
|
24
|
+
|
|
25
|
+
unless VALID_STATES.include?(state)
|
|
26
|
+
@stderr.puts "Error: state must be one of: #{VALID_STATES.join(", ")}"
|
|
27
|
+
return EXIT_CODE_ERROR
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if state == "warning" && @severity.nil?
|
|
31
|
+
@stderr.puts "Error: --severity is required when state is 'warning'"
|
|
32
|
+
return EXIT_CODE_ERROR
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
if state != "warning" && !@severity.nil?
|
|
36
|
+
@stderr.puts "Error: --severity is only valid when state is 'warning'"
|
|
37
|
+
return EXIT_CODE_ERROR
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
@api_client.set_display_state(build_body(state))
|
|
41
|
+
@stdout.puts "Display state updated."
|
|
42
|
+
|
|
43
|
+
EXIT_CODE_SUCCESS
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def build_body(state)
|
|
50
|
+
case state
|
|
51
|
+
when "recording" then JSON.generate("Recording")
|
|
52
|
+
when "paused" then JSON.generate("Paused")
|
|
53
|
+
when "warning" then JSON.generate({"WarningDetected" => {"event_type" => @severity.capitalize}})
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_options
|
|
58
|
+
OptionParser.new do |opts|
|
|
59
|
+
opts.banner = "Usage: raygatherer debug display-state [options] STATE"
|
|
60
|
+
opts.separator ""
|
|
61
|
+
opts.separator "Options:"
|
|
62
|
+
|
|
63
|
+
opts.on("--severity LEVEL", VALID_SEVERITIES, "Severity: low, medium, high (required for 'warning' state)") do |s|
|
|
64
|
+
@severity = s
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
68
|
+
show_help
|
|
69
|
+
raise CLI::EarlyExit, EXIT_CODE_SUCCESS
|
|
70
|
+
end
|
|
71
|
+
end.parse!(@argv)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def show_help(output = @stdout)
|
|
75
|
+
output.puts "Usage: raygatherer [global options] debug display-state [options] STATE"
|
|
76
|
+
output.puts ""
|
|
77
|
+
output.puts "Change the display state of the device for debugging purposes."
|
|
78
|
+
output.puts ""
|
|
79
|
+
output.puts "States:"
|
|
80
|
+
output.puts " recording Device is recording, no warnings"
|
|
81
|
+
output.puts " paused Device is not recording"
|
|
82
|
+
output.puts " warning Warning detected (requires --severity)"
|
|
83
|
+
output.puts ""
|
|
84
|
+
output.puts "Options:"
|
|
85
|
+
output.puts " --severity LEVEL Warning severity: low, medium, high"
|
|
86
|
+
output.puts " -h, --help Show this help message"
|
|
87
|
+
output.puts ""
|
|
88
|
+
print_global_options(output)
|
|
89
|
+
output.puts ""
|
|
90
|
+
output.puts "Examples:"
|
|
91
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 debug display-state recording"
|
|
92
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 debug display-state paused"
|
|
93
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 debug display-state warning --severity high"
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "base"
|
|
5
|
+
|
|
6
|
+
module Raygatherer
|
|
7
|
+
module Commands
|
|
8
|
+
class Log < Base
|
|
9
|
+
def run
|
|
10
|
+
with_error_handling do
|
|
11
|
+
parse_options
|
|
12
|
+
@stdout.print @api_client.fetch_log
|
|
13
|
+
EXIT_CODE_SUCCESS
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def parse_options
|
|
20
|
+
OptionParser.new do |opts|
|
|
21
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
22
|
+
show_help
|
|
23
|
+
raise CLI::EarlyExit, EXIT_CODE_SUCCESS
|
|
24
|
+
end
|
|
25
|
+
end.parse!(@argv)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def show_help(output = @stdout)
|
|
29
|
+
output.puts "Usage: raygatherer [global options] log [options]"
|
|
30
|
+
output.puts ""
|
|
31
|
+
output.puts "Options:"
|
|
32
|
+
output.puts " -h, --help Show this help message"
|
|
33
|
+
output.puts ""
|
|
34
|
+
print_global_options(output)
|
|
35
|
+
output.puts ""
|
|
36
|
+
output.puts "Examples:"
|
|
37
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 log"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -7,30 +7,71 @@ module Raygatherer
|
|
|
7
7
|
module Commands
|
|
8
8
|
module Recording
|
|
9
9
|
class Delete < Base
|
|
10
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr, api_client: nil, stdin: $stdin, **_kwargs)
|
|
11
|
+
super(argv, stdout: stdout, stderr: stderr, api_client: api_client)
|
|
12
|
+
@stdin = stdin
|
|
13
|
+
@all = false
|
|
14
|
+
@force = false
|
|
15
|
+
end
|
|
16
|
+
|
|
10
17
|
def run
|
|
11
18
|
with_error_handling do
|
|
12
19
|
parse_options
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@stderr.puts "Error: recording name is required"
|
|
21
|
+
if @all && !@argv.empty?
|
|
22
|
+
@stderr.puts "Error: cannot specify both a recording name and --all"
|
|
17
23
|
next EXIT_CODE_ERROR
|
|
18
24
|
end
|
|
19
25
|
|
|
20
|
-
@
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
if @all
|
|
27
|
+
delete_all
|
|
28
|
+
else
|
|
29
|
+
delete_named
|
|
30
|
+
end
|
|
23
31
|
end
|
|
24
32
|
end
|
|
25
33
|
|
|
26
34
|
private
|
|
27
35
|
|
|
36
|
+
def delete_all
|
|
37
|
+
unless @force
|
|
38
|
+
@stderr.print "Warning: This will permanently delete ALL recordings!\nAre you sure? [y/N]: "
|
|
39
|
+
response = @stdin.gets&.strip&.downcase || ""
|
|
40
|
+
unless response == "y" || response == "yes"
|
|
41
|
+
@stderr.puts "Aborted."
|
|
42
|
+
return EXIT_CODE_ERROR
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
@api_client.delete_all_recordings
|
|
46
|
+
@stdout.puts "Deleted all recordings."
|
|
47
|
+
EXIT_CODE_SUCCESS
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def delete_named
|
|
51
|
+
name = @argv.shift
|
|
52
|
+
unless name
|
|
53
|
+
@stderr.puts "Error: recording name or --all is required"
|
|
54
|
+
return EXIT_CODE_ERROR
|
|
55
|
+
end
|
|
56
|
+
@api_client.delete_recording(name)
|
|
57
|
+
@stdout.puts "Deleted recording: #{name}"
|
|
58
|
+
EXIT_CODE_SUCCESS
|
|
59
|
+
end
|
|
60
|
+
|
|
28
61
|
def parse_options
|
|
29
62
|
OptionParser.new do |opts|
|
|
30
|
-
opts.banner = "Usage: raygatherer recording delete <name>"
|
|
63
|
+
opts.banner = "Usage: raygatherer recording delete <name> | --all [--force]"
|
|
31
64
|
opts.separator ""
|
|
32
65
|
opts.separator "Options:"
|
|
33
66
|
|
|
67
|
+
opts.on("--all", "Delete all recordings (prompts for confirmation)") do
|
|
68
|
+
@all = true
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
opts.on("-f", "--force", "Skip confirmation prompt (use with --all)") do
|
|
72
|
+
@force = true
|
|
73
|
+
end
|
|
74
|
+
|
|
34
75
|
opts.on("-h", "--help", "Show this help message") do
|
|
35
76
|
show_help
|
|
36
77
|
raise CLI::EarlyExit, EXIT_CODE_SUCCESS
|
|
@@ -40,14 +81,20 @@ module Raygatherer
|
|
|
40
81
|
|
|
41
82
|
def show_help(output = @stdout)
|
|
42
83
|
output.puts "Usage: raygatherer [global options] recording delete <name>"
|
|
84
|
+
output.puts " raygatherer [global options] recording delete --all [--force]"
|
|
43
85
|
output.puts ""
|
|
44
86
|
output.puts "Options:"
|
|
87
|
+
output.puts " --all Delete all recordings (prompts for confirmation)"
|
|
88
|
+
output.puts " -f, --force Skip confirmation prompt (use with --all)"
|
|
45
89
|
output.puts " -h, --help Show this help message"
|
|
46
90
|
output.puts ""
|
|
47
91
|
print_global_options(output)
|
|
48
92
|
output.puts ""
|
|
49
93
|
output.puts "Examples:"
|
|
50
94
|
output.puts " raygatherer --host http://192.168.1.100:8080 recording delete 1738950000"
|
|
95
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 recording delete --all"
|
|
96
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 recording delete --all --force"
|
|
97
|
+
output.puts " echo y | raygatherer --host http://192.168.1.100:8080 recording delete --all"
|
|
51
98
|
end
|
|
52
99
|
end
|
|
53
100
|
end
|
|
@@ -28,21 +28,21 @@ module Raygatherer
|
|
|
28
28
|
def run
|
|
29
29
|
with_error_handling do
|
|
30
30
|
parse_options
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
return EXIT_CODE_ERROR unless validate_format_flags
|
|
32
|
+
return EXIT_CODE_ERROR unless validate_path_flags
|
|
33
33
|
|
|
34
34
|
name = @argv.shift
|
|
35
35
|
unless name
|
|
36
36
|
@stderr.puts "Error: recording name is required"
|
|
37
|
-
|
|
37
|
+
return EXIT_CODE_ERROR
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
dest_path = resolve_destination(name)
|
|
41
|
-
|
|
41
|
+
return EXIT_CODE_ERROR unless dest_path
|
|
42
42
|
|
|
43
43
|
if File.exist?(dest_path)
|
|
44
44
|
@stderr.puts "Error: file already exists: #{dest_path}"
|
|
45
|
-
|
|
45
|
+
return EXIT_CODE_ERROR
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
spinner = Spinner.new(stderr: @stderr)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require_relative "../base"
|
|
5
|
+
require_relative "../../formatters/time_json"
|
|
6
|
+
require_relative "../../formatters/time_human"
|
|
7
|
+
|
|
8
|
+
module Raygatherer
|
|
9
|
+
module Commands
|
|
10
|
+
module Time
|
|
11
|
+
class Show < Base
|
|
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
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
with_error_handling do
|
|
19
|
+
parse_options
|
|
20
|
+
|
|
21
|
+
time = @api_client.fetch_time
|
|
22
|
+
|
|
23
|
+
formatter = @json ? Formatters::TimeJSON.new : Formatters::TimeHuman.new
|
|
24
|
+
@stdout.puts formatter.format(time)
|
|
25
|
+
|
|
26
|
+
EXIT_CODE_SUCCESS
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def parse_options
|
|
33
|
+
OptionParser.new do |opts|
|
|
34
|
+
opts.banner = "Usage: raygatherer time show [options]"
|
|
35
|
+
opts.separator ""
|
|
36
|
+
opts.separator "Options:"
|
|
37
|
+
|
|
38
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
39
|
+
show_help
|
|
40
|
+
raise CLI::EarlyExit, EXIT_CODE_SUCCESS
|
|
41
|
+
end
|
|
42
|
+
end.parse!(@argv)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def show_help(output = @stdout)
|
|
46
|
+
output.puts "Usage: raygatherer [global options] time show [options]"
|
|
47
|
+
output.puts ""
|
|
48
|
+
output.puts "Options:"
|
|
49
|
+
output.puts " -h, --help Show this help message"
|
|
50
|
+
output.puts ""
|
|
51
|
+
print_global_options(output, json: true)
|
|
52
|
+
output.puts ""
|
|
53
|
+
output.puts "Examples:"
|
|
54
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 time show"
|
|
55
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 --json time show"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "time"
|
|
5
|
+
require_relative "../base"
|
|
6
|
+
|
|
7
|
+
module Raygatherer
|
|
8
|
+
module Commands
|
|
9
|
+
module Time
|
|
10
|
+
class Sync < Base
|
|
11
|
+
def run
|
|
12
|
+
with_error_handling do
|
|
13
|
+
parse_options
|
|
14
|
+
|
|
15
|
+
time_data = @api_client.fetch_time
|
|
16
|
+
device_system_time = ::Time.parse(time_data["system_time"])
|
|
17
|
+
offset = (::Time.now - device_system_time).round
|
|
18
|
+
@api_client.set_time_offset(offset)
|
|
19
|
+
@stdout.puts "Clock synced. Offset: #{offset}s"
|
|
20
|
+
|
|
21
|
+
EXIT_CODE_SUCCESS
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def parse_options
|
|
28
|
+
OptionParser.new do |opts|
|
|
29
|
+
opts.banner = "Usage: raygatherer time sync"
|
|
30
|
+
opts.separator ""
|
|
31
|
+
opts.separator "Options:"
|
|
32
|
+
|
|
33
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
34
|
+
show_help
|
|
35
|
+
raise CLI::EarlyExit, EXIT_CODE_SUCCESS
|
|
36
|
+
end
|
|
37
|
+
end.parse!(@argv)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def show_help(output = @stdout)
|
|
41
|
+
output.puts "Usage: raygatherer [global options] time sync"
|
|
42
|
+
output.puts ""
|
|
43
|
+
output.puts "Syncs the device clock to this machine's time."
|
|
44
|
+
output.puts ""
|
|
45
|
+
output.puts "Options:"
|
|
46
|
+
output.puts " -h, --help Show this help message"
|
|
47
|
+
output.puts ""
|
|
48
|
+
print_global_options(output)
|
|
49
|
+
output.puts ""
|
|
50
|
+
output.puts "Examples:"
|
|
51
|
+
output.puts " raygatherer --host http://192.168.1.100:8080 time sync"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -4,7 +4,7 @@ require "json"
|
|
|
4
4
|
|
|
5
5
|
module Raygatherer
|
|
6
6
|
module Formatters
|
|
7
|
-
class
|
|
7
|
+
class AlertsJSON
|
|
8
8
|
def format(alerts)
|
|
9
9
|
output = alerts.map do |alert|
|
|
10
10
|
{
|
|
@@ -15,7 +15,7 @@ module Raygatherer
|
|
|
15
15
|
}
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
JSON.generate(output)
|
|
19
19
|
end
|
|
20
20
|
end
|
|
21
21
|
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Raygatherer
|
|
6
|
+
module Formatters
|
|
7
|
+
class AnalysisReportHuman
|
|
8
|
+
def format(data)
|
|
9
|
+
metadata = data[:metadata] || {}
|
|
10
|
+
rows = data[:rows] || []
|
|
11
|
+
|
|
12
|
+
lines = []
|
|
13
|
+
lines << format_header(metadata)
|
|
14
|
+
lines << format_analyzers(metadata)
|
|
15
|
+
lines << ""
|
|
16
|
+
|
|
17
|
+
rows.each do |row|
|
|
18
|
+
lines.concat(format_row(row, metadata))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
lines.join("\n")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def format_header(metadata)
|
|
27
|
+
rayhunter = metadata["rayhunter"] || {}
|
|
28
|
+
version = rayhunter["rayhunter_version"]
|
|
29
|
+
os = rayhunter["system_os"]
|
|
30
|
+
arch = rayhunter["arch"]
|
|
31
|
+
report_version = metadata["report_version"]
|
|
32
|
+
|
|
33
|
+
parts = []
|
|
34
|
+
parts << "Rayhunter v#{version}" if version
|
|
35
|
+
os_arch = [os, arch].compact.join(" ")
|
|
36
|
+
parts << os_arch unless os_arch.empty?
|
|
37
|
+
parts << "Report version #{report_version}" if report_version
|
|
38
|
+
|
|
39
|
+
parts.join(" | ")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def format_analyzers(metadata)
|
|
43
|
+
analyzers = metadata["analyzers"] || []
|
|
44
|
+
return "Analyzers: (none)" if analyzers.empty?
|
|
45
|
+
|
|
46
|
+
names = analyzers.filter_map do |a|
|
|
47
|
+
next unless a
|
|
48
|
+
v = a["version"] ? " (v#{a["version"]})" : ""
|
|
49
|
+
"#{a["name"]}#{v}"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
"Analyzers: #{names.join(", ")}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def format_row(row, metadata)
|
|
56
|
+
analyzers = metadata["analyzers"] || []
|
|
57
|
+
timestamp = format_timestamp(row["packet_timestamp"])
|
|
58
|
+
|
|
59
|
+
if (reason = row["skipped_message_reason"])
|
|
60
|
+
return ["#{timestamp} Skipped: #{reason}"]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
events = row["events"] || []
|
|
64
|
+
present_events = events.each_with_index.reject { |e, _| e.nil? }
|
|
65
|
+
|
|
66
|
+
if present_events.empty?
|
|
67
|
+
return ["#{timestamp} No events"]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
present_events.map do |event, index|
|
|
71
|
+
event_type = event["event_type"] || "Unknown"
|
|
72
|
+
message = event["message"]
|
|
73
|
+
analyzer = analyzers.dig(index, "name")
|
|
74
|
+
analyzer_part = analyzer ? "(#{analyzer}) " : ""
|
|
75
|
+
"#{timestamp} #{event_type.ljust(13)} #{analyzer_part}#{message}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def format_timestamp(ts)
|
|
80
|
+
return "[no timestamp]" unless ts
|
|
81
|
+
time = Time.parse(ts).utc
|
|
82
|
+
"[#{time.strftime("%Y-%m-%d %H:%M:%S UTC")}]"
|
|
83
|
+
rescue ArgumentError
|
|
84
|
+
"[#{ts}]"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -59,13 +59,18 @@ module Raygatherer
|
|
|
59
59
|
|
|
60
60
|
lines << " Size: #{format_size(entry["qmdl_size_bytes"])}"
|
|
61
61
|
|
|
62
|
+
if !active && entry["stop_reason"]
|
|
63
|
+
lines << " Stop reason: #{entry["stop_reason"]}"
|
|
64
|
+
end
|
|
65
|
+
|
|
62
66
|
lines.join("\n")
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
def format_time(time_string)
|
|
66
70
|
return "" unless time_string
|
|
67
|
-
|
|
68
71
|
Time.parse(time_string).strftime("%Y-%m-%d %H:%M:%S")
|
|
72
|
+
rescue ArgumentError
|
|
73
|
+
time_string
|
|
69
74
|
end
|
|
70
75
|
end
|
|
71
76
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Raygatherer
|
|
4
|
+
module Formatters
|
|
5
|
+
class TimeHuman
|
|
6
|
+
def format(time)
|
|
7
|
+
lines = []
|
|
8
|
+
lines << "System time: #{time["system_time"]}"
|
|
9
|
+
lines << "Adjusted time: #{time["adjusted_time"]}"
|
|
10
|
+
lines << "Offset: #{time["offset_seconds"]}s"
|
|
11
|
+
lines.join("\n")
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/raygatherer/version.rb
CHANGED
data/lib/raygatherer.rb
CHANGED
|
@@ -4,16 +4,20 @@ require_relative "raygatherer/version"
|
|
|
4
4
|
require_relative "raygatherer/config"
|
|
5
5
|
require_relative "raygatherer/cli"
|
|
6
6
|
require_relative "raygatherer/api_client"
|
|
7
|
-
require_relative "raygatherer/formatters/
|
|
8
|
-
require_relative "raygatherer/formatters/
|
|
7
|
+
require_relative "raygatherer/formatters/alerts_human"
|
|
8
|
+
require_relative "raygatherer/formatters/alerts_json"
|
|
9
9
|
require_relative "raygatherer/formatters/recording_list_json"
|
|
10
10
|
require_relative "raygatherer/formatters/recording_list_human"
|
|
11
11
|
require_relative "raygatherer/formatters/stats_json"
|
|
12
12
|
require_relative "raygatherer/formatters/stats_human"
|
|
13
13
|
require_relative "raygatherer/formatters/analysis_status_json"
|
|
14
14
|
require_relative "raygatherer/formatters/analysis_status_human"
|
|
15
|
+
require_relative "raygatherer/formatters/analysis_report_json"
|
|
16
|
+
require_relative "raygatherer/formatters/analysis_report_human"
|
|
15
17
|
require_relative "raygatherer/formatters/config_json"
|
|
16
18
|
require_relative "raygatherer/formatters/config_human"
|
|
19
|
+
require_relative "raygatherer/formatters/time_json"
|
|
20
|
+
require_relative "raygatherer/formatters/time_human"
|
|
17
21
|
require_relative "raygatherer/spinner"
|
|
18
22
|
require_relative "raygatherer/commands/base"
|
|
19
23
|
require_relative "raygatherer/commands/alerts"
|
|
@@ -22,12 +26,17 @@ require_relative "raygatherer/commands/recording/download"
|
|
|
22
26
|
require_relative "raygatherer/commands/recording/delete"
|
|
23
27
|
require_relative "raygatherer/commands/recording/stop"
|
|
24
28
|
require_relative "raygatherer/commands/recording/start"
|
|
29
|
+
require_relative "raygatherer/commands/log"
|
|
25
30
|
require_relative "raygatherer/commands/stats"
|
|
26
31
|
require_relative "raygatherer/commands/analysis/status"
|
|
27
32
|
require_relative "raygatherer/commands/analysis/run"
|
|
33
|
+
require_relative "raygatherer/commands/analysis/report"
|
|
28
34
|
require_relative "raygatherer/commands/config/show"
|
|
29
35
|
require_relative "raygatherer/commands/config/set"
|
|
30
36
|
require_relative "raygatherer/commands/config/test_notification"
|
|
37
|
+
require_relative "raygatherer/commands/debug/display_state"
|
|
38
|
+
require_relative "raygatherer/commands/time/show"
|
|
39
|
+
require_relative "raygatherer/commands/time/sync"
|
|
31
40
|
|
|
32
41
|
module Raygatherer
|
|
33
42
|
class Error < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raygatherer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Mike Stallard
|
|
@@ -47,7 +47,6 @@ extensions: []
|
|
|
47
47
|
extra_rdoc_files: []
|
|
48
48
|
files:
|
|
49
49
|
- ".rspec"
|
|
50
|
-
- CLAUDE.md
|
|
51
50
|
- CODE_OF_CONDUCT.md
|
|
52
51
|
- LICENSE.txt
|
|
53
52
|
- Makefile
|
|
@@ -58,30 +57,39 @@ files:
|
|
|
58
57
|
- lib/raygatherer/api_client.rb
|
|
59
58
|
- lib/raygatherer/cli.rb
|
|
60
59
|
- lib/raygatherer/commands/alerts.rb
|
|
60
|
+
- lib/raygatherer/commands/analysis/report.rb
|
|
61
61
|
- lib/raygatherer/commands/analysis/run.rb
|
|
62
62
|
- lib/raygatherer/commands/analysis/status.rb
|
|
63
63
|
- lib/raygatherer/commands/base.rb
|
|
64
64
|
- lib/raygatherer/commands/config/set.rb
|
|
65
65
|
- lib/raygatherer/commands/config/show.rb
|
|
66
66
|
- lib/raygatherer/commands/config/test_notification.rb
|
|
67
|
+
- lib/raygatherer/commands/debug/display_state.rb
|
|
68
|
+
- lib/raygatherer/commands/log.rb
|
|
67
69
|
- lib/raygatherer/commands/recording/delete.rb
|
|
68
70
|
- lib/raygatherer/commands/recording/download.rb
|
|
69
71
|
- lib/raygatherer/commands/recording/list.rb
|
|
70
72
|
- lib/raygatherer/commands/recording/start.rb
|
|
71
73
|
- lib/raygatherer/commands/recording/stop.rb
|
|
72
74
|
- lib/raygatherer/commands/stats.rb
|
|
75
|
+
- lib/raygatherer/commands/time/show.rb
|
|
76
|
+
- lib/raygatherer/commands/time/sync.rb
|
|
73
77
|
- lib/raygatherer/config.rb
|
|
74
78
|
- lib/raygatherer/format_helpers.rb
|
|
79
|
+
- lib/raygatherer/formatters/alerts_human.rb
|
|
80
|
+
- lib/raygatherer/formatters/alerts_json.rb
|
|
81
|
+
- lib/raygatherer/formatters/analysis_report_human.rb
|
|
82
|
+
- lib/raygatherer/formatters/analysis_report_json.rb
|
|
75
83
|
- lib/raygatherer/formatters/analysis_status_human.rb
|
|
76
84
|
- lib/raygatherer/formatters/analysis_status_json.rb
|
|
77
85
|
- lib/raygatherer/formatters/config_human.rb
|
|
78
86
|
- lib/raygatherer/formatters/config_json.rb
|
|
79
|
-
- lib/raygatherer/formatters/human.rb
|
|
80
|
-
- lib/raygatherer/formatters/json.rb
|
|
81
87
|
- lib/raygatherer/formatters/recording_list_human.rb
|
|
82
88
|
- lib/raygatherer/formatters/recording_list_json.rb
|
|
83
89
|
- lib/raygatherer/formatters/stats_human.rb
|
|
84
90
|
- lib/raygatherer/formatters/stats_json.rb
|
|
91
|
+
- lib/raygatherer/formatters/time_human.rb
|
|
92
|
+
- lib/raygatherer/formatters/time_json.rb
|
|
85
93
|
- lib/raygatherer/spinner.rb
|
|
86
94
|
- lib/raygatherer/version.rb
|
|
87
95
|
homepage: https://github.com/mjstallard/raygatherer
|
|
@@ -98,7 +106,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
98
106
|
requirements:
|
|
99
107
|
- - ">="
|
|
100
108
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: 3.
|
|
109
|
+
version: 3.2.0
|
|
102
110
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
103
111
|
requirements:
|
|
104
112
|
- - ">="
|
data/CLAUDE.md
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# PROJECT OVERVIEW
|
|
2
|
-
|
|
3
|
-
Raygatherer: Ruby CLI for fetching and displaying alerts from Rayhunter (cell tower / IMSI catcher analysis device). Ruby >= 3.1.0.
|
|
4
|
-
|
|
5
|
-
# QUICK REFERENCE
|
|
6
|
-
|
|
7
|
-
- Run tests: `bundle exec rspec`
|
|
8
|
-
- Run CLI: `bundle exec ./exe/raygatherer`
|
|
9
|
-
- CI: GitHub Actions on push to master and all PRs
|
|
10
|
-
- `rayhunter` itself is checked out at `/Users/mike/workspace/rayhunter/`
|
|
11
|
-
|
|
12
|
-
# ARCHITECTURE
|
|
13
|
-
|
|
14
|
-
- Entry point: `exe/raygatherer` → `Raygatherer::CLI.run`
|
|
15
|
-
- CLI routing: `lib/raygatherer/cli.rb`
|
|
16
|
-
- Commands: `lib/raygatherer/commands/` (one class per subcommand)
|
|
17
|
-
- API client: `lib/raygatherer/api_client.rb` (HTTP + NDJSON parsing)
|
|
18
|
-
- Formatters: `lib/raygatherer/formatters/` (human and JSON, both accept arrays)
|
|
19
|
-
- Tests mirror `lib/` structure under `spec/`, plus `spec/integration/` for CLI end-to-end tests
|
|
20
|
-
|
|
21
|
-
# KEY CONVENTIONS
|
|
22
|
-
|
|
23
|
-
- Formatters always receive and return arrays (even when empty)
|
|
24
|
-
- JSON output is always a JSON array
|
|
25
|
-
- Exit codes encode alert severity for scripting use (see `show_help` in status.rb)
|
|
26
|
-
- Linter: `bundle exec standardrb` (Ruby Standard Style). Write new code in standardrb style to avoid rework.
|
|
27
|
-
|
|
28
|
-
# ROLE AND EXPERTISE
|
|
29
|
-
|
|
30
|
-
You are a senior software engineer who follows Kent Beck's Test-Driven Development (TDD) and Tidy First principles. Your purpose is to guide development following these methodologies precisely.
|
|
31
|
-
|
|
32
|
-
# CORE DEVELOPMENT PRINCIPLES
|
|
33
|
-
|
|
34
|
-
- Always follow the TDD cycle: Red → Green → Refactor
|
|
35
|
-
- Write the simplest failing test first
|
|
36
|
-
- Implement the minimum code needed to make tests pass
|
|
37
|
-
- Refactor only after tests are passing
|
|
38
|
-
- Follow Beck's "Tidy First" approach by separating structural changes from behavioral changes
|
|
39
|
-
- Maintain high code quality throughout development
|
|
40
|
-
|
|
41
|
-
# TDD METHODOLOGY GUIDANCE
|
|
42
|
-
|
|
43
|
-
- Start by writing a failing test that defines a small increment of functionality
|
|
44
|
-
- Use meaningful test names that describe behavior (e.g., "should sum two positive integers")
|
|
45
|
-
- Make test failures clear and informative
|
|
46
|
-
- Write just enough code to make the test pass - no more
|
|
47
|
-
- Once tests pass, consider if refactoring is needed
|
|
48
|
-
- Repeat the cycle for new functionality
|
|
49
|
-
- When fixing a defect, first write an API-level failing test then write the smallest possible test that replicates the problem then get both tests to pass.
|
|
50
|
-
|
|
51
|
-
# TIDY FIRST APPROACH
|
|
52
|
-
|
|
53
|
-
- Separate all changes into two distinct types:
|
|
54
|
-
1. STRUCTURAL CHANGES: Rearranging code without changing behavior (renaming, extracting methods, moving code)
|
|
55
|
-
2. BEHAVIORAL CHANGES: Adding or modifying actual functionality
|
|
56
|
-
- Never mix structural and behavioral changes in the same commit
|
|
57
|
-
- Always make structural changes first when both are needed
|
|
58
|
-
- Validate structural changes do not alter behavior by running tests before and after
|
|
59
|
-
|
|
60
|
-
# COMMIT DISCIPLINE
|
|
61
|
-
|
|
62
|
-
- Only commit when:
|
|
63
|
-
1. ALL tests are passing (`bundle exec rspec`)
|
|
64
|
-
2. ALL standardrb issues are resolved (`bundle exec standardrb`); fix any issues before committing
|
|
65
|
-
3. The change represents a single logical unit of work
|
|
66
|
-
4. Commit messages clearly state whether the commit contains structural or behavioral changes
|
|
67
|
-
- Use small, frequent commits rather than large, infrequent ones
|
|
68
|
-
|
|
69
|
-
# CODE QUALITY STANDARDS
|
|
70
|
-
|
|
71
|
-
- Eliminate duplication ruthlessly
|
|
72
|
-
- Express intent clearly through naming and structure
|
|
73
|
-
- Make dependencies explicit
|
|
74
|
-
- Keep methods small and focused on a single responsibility
|
|
75
|
-
- Minimize state and side effects
|
|
76
|
-
- Use the simplest solution that could possibly work
|
|
77
|
-
|
|
78
|
-
# REFACTORING GUIDELINES
|
|
79
|
-
|
|
80
|
-
- Refactor only when tests are passing (in the "Green" phase)
|
|
81
|
-
- Use established refactoring patterns with their proper names
|
|
82
|
-
- Make one refactoring change at a time
|
|
83
|
-
- Run tests after each refactoring step
|
|
84
|
-
- Prioritize refactorings that remove duplication or improve clarity
|
|
85
|
-
|
|
86
|
-
# EXAMPLE WORKFLOW
|
|
87
|
-
|
|
88
|
-
When approaching a new feature:
|
|
89
|
-
|
|
90
|
-
1. Write a simple failing test for a small part of the feature
|
|
91
|
-
2. Implement the bare minimum to make it pass
|
|
92
|
-
3. Run tests to confirm they pass (Green)
|
|
93
|
-
4. Run `bundle exec standardrb` and fix any issues
|
|
94
|
-
5. Make any necessary structural changes (Tidy First), running tests after each change
|
|
95
|
-
6. Commit structural changes separately
|
|
96
|
-
7. Add another test for the next small increment of functionality
|
|
97
|
-
8. Repeat until the feature is complete, committing behavioral changes separately from structural ones
|
|
98
|
-
|
|
99
|
-
Follow this process precisely, always prioritizing clean, well-tested code over quick implementation.
|
|
100
|
-
|
|
101
|
-
Always write one test at a time, make it run, then improve structure. Always run all the tests (except long-running tests) each time.
|
|
102
|
-
|