swa 0.8.6 → 1.0.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/.beads/.gitignore +18 -0
- data/.beads/issues.jsonl +5 -0
- data/.rubocop.yml +39 -0
- data/AGENTS.md +541 -0
- data/CLAUDE.md +5 -0
- data/Gemfile +9 -1
- data/README.md +0 -5
- data/Rakefile +2 -0
- data/bin/console +1 -0
- data/exe/swa +2 -1
- data/lib/swa/athena/catalog.rb +4 -0
- data/lib/swa/athena/database.rb +4 -0
- data/lib/swa/athena/query_execution.rb +4 -0
- data/lib/swa/athena/work_group.rb +4 -0
- data/lib/swa/cli/athena_command.rb +93 -42
- data/lib/swa/cli/base_command.rb +22 -14
- data/lib/swa/cli/cloud_formation_command.rb +11 -4
- data/lib/swa/cli/cloudtrail_command.rb +130 -0
- data/lib/swa/cli/collection_behaviour.rb +4 -2
- data/lib/swa/cli/data_output.rb +12 -11
- data/lib/swa/cli/ec2_command.rb +21 -37
- data/lib/swa/cli/elb_command.rb +5 -3
- data/lib/swa/cli/filter_options.rb +6 -1
- data/lib/swa/cli/glue_command.rb +84 -36
- data/lib/swa/cli/iam_command.rb +42 -33
- data/lib/swa/cli/item_behaviour.rb +4 -2
- data/lib/swa/cli/kms_command.rb +26 -5
- data/lib/swa/cli/lake_formation_command.rb +72 -11
- data/lib/swa/cli/main_command.rb +19 -9
- data/lib/swa/cli/s3_command.rb +32 -24
- data/lib/swa/cli/selector.rb +4 -0
- data/lib/swa/cli/tag_filter_options.rb +6 -2
- data/lib/swa/cloud_formation/stack.rb +5 -1
- data/lib/swa/cloud_trail/event.rb +49 -0
- data/lib/swa/data_presentation.rb +23 -22
- data/lib/swa/ec2/image.rb +7 -5
- data/lib/swa/ec2/instance.rb +5 -1
- data/lib/swa/ec2/key_pair.rb +4 -0
- data/lib/swa/ec2/security_group.rb +5 -3
- data/lib/swa/ec2/snapshot.rb +6 -4
- data/lib/swa/ec2/subnet.rb +5 -3
- data/lib/swa/ec2/tagged_resource.rb +4 -0
- data/lib/swa/ec2/volume.rb +7 -5
- data/lib/swa/ec2/vpc.rb +5 -3
- data/lib/swa/elb/load_balancer.rb +4 -0
- data/lib/swa/glue/crawl.rb +5 -1
- data/lib/swa/glue/crawler.rb +5 -1
- data/lib/swa/glue/database.rb +5 -0
- data/lib/swa/glue/job.rb +4 -0
- data/lib/swa/glue/job_bookmark_entry.rb +4 -0
- data/lib/swa/glue/job_run.rb +5 -1
- data/lib/swa/glue/partition.rb +5 -1
- data/lib/swa/glue/table.rb +4 -0
- data/lib/swa/iam/credentials.rb +7 -7
- data/lib/swa/iam/group.rb +5 -3
- data/lib/swa/iam/instance_profile.rb +5 -3
- data/lib/swa/iam/policy.rb +5 -3
- data/lib/swa/iam/role.rb +10 -3
- data/lib/swa/iam/role_policy.rb +5 -3
- data/lib/swa/iam/user.rb +5 -3
- data/lib/swa/kms/alias.rb +4 -0
- data/lib/swa/kms/key.rb +4 -0
- data/lib/swa/lake_formation/data_lake_settings.rb +15 -0
- data/lib/swa/lake_formation/permission.rb +5 -1
- data/lib/swa/lake_formation/resource_info.rb +4 -0
- data/lib/swa/lake_formation/tag.rb +25 -0
- data/lib/swa/polyfill.rb +3 -1
- data/lib/swa/record.rb +5 -3
- data/lib/swa/resource.rb +3 -1
- data/lib/swa/s3/bucket.rb +5 -3
- data/lib/swa/s3/object.rb +7 -5
- data/lib/swa/s3/object_list_entry.rb +4 -0
- data/lib/swa/s3/object_version.rb +13 -7
- data/lib/swa/version.rb +5 -1
- data/lib/swa.rb +2 -0
- data/swa.gemspec +29 -25
- metadata +63 -29
data/lib/swa/athena/catalog.rb
CHANGED
data/lib/swa/athena/database.rb
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "aws-sdk-athena"
|
|
4
|
+
require "bytesize"
|
|
2
5
|
require "csv"
|
|
3
6
|
require "swa/cli/base_command"
|
|
4
7
|
require "swa/cli/collection_behaviour"
|
|
@@ -9,12 +12,13 @@ require "swa/athena/query_execution"
|
|
|
9
12
|
require "swa/athena/work_group"
|
|
10
13
|
|
|
11
14
|
module Swa
|
|
15
|
+
|
|
12
16
|
module CLI
|
|
13
17
|
|
|
14
18
|
class AthenaCommand < BaseCommand
|
|
15
19
|
|
|
16
20
|
option "--catalog", "NAME", "Data catalog name", default: "AwsDataCatalog"
|
|
17
|
-
option %w
|
|
21
|
+
option %w[--workgroup -W], "NAME", "Workgroup name"
|
|
18
22
|
|
|
19
23
|
subcommand "catalogs", "Show catalogs" do
|
|
20
24
|
|
|
@@ -34,8 +38,6 @@ module Swa
|
|
|
34
38
|
|
|
35
39
|
include ItemBehaviour
|
|
36
40
|
|
|
37
|
-
private
|
|
38
|
-
|
|
39
41
|
def item
|
|
40
42
|
Swa::Athena::Database.new(
|
|
41
43
|
athena_client.get_database(catalog_name: catalog, database_name: database).database
|
|
@@ -48,22 +50,56 @@ module Swa
|
|
|
48
50
|
|
|
49
51
|
include CollectionBehaviour
|
|
50
52
|
|
|
51
|
-
private
|
|
52
|
-
|
|
53
53
|
def collection
|
|
54
54
|
query_for(:list_databases, :database_list, Swa::Athena::Database, catalog_name: catalog)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
end
|
|
58
58
|
|
|
59
|
+
module CanOutputResults
|
|
60
|
+
|
|
61
|
+
extend Clamp::Option::Declaration
|
|
62
|
+
|
|
63
|
+
option ["--text", "-1"], :flag, "output first column as text"
|
|
64
|
+
|
|
65
|
+
def display_query_results(query_results)
|
|
66
|
+
output_rows(
|
|
67
|
+
query_results.lazy.flat_map do |results|
|
|
68
|
+
results.result_set.rows
|
|
69
|
+
end
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def output_rows(rows)
|
|
74
|
+
if text?
|
|
75
|
+
output_rows_as_text(rows)
|
|
76
|
+
else
|
|
77
|
+
output_rows_as_csv(rows)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def output_rows_as_csv(rows)
|
|
82
|
+
CSV($stdout.dup) do |csv|
|
|
83
|
+
rows.each do |row|
|
|
84
|
+
csv << row.data.map(&:var_char_value)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def output_rows_as_text(rows)
|
|
90
|
+
rows.drop(1).each do |row|
|
|
91
|
+
puts row.data.first.var_char_value
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
end
|
|
96
|
+
|
|
59
97
|
subcommand ["execution", "query-execution"], "Inspect query execution" do
|
|
60
98
|
|
|
61
99
|
parameter "ID", "execution ID", attribute_name: :execution_id
|
|
62
100
|
|
|
63
101
|
include ItemBehaviour
|
|
64
102
|
|
|
65
|
-
private
|
|
66
|
-
|
|
67
103
|
def item
|
|
68
104
|
Swa::Athena::QueryExecution.new(
|
|
69
105
|
athena_client.get_query_execution(query_execution_id: execution_id).query_execution
|
|
@@ -72,9 +108,10 @@ module Swa
|
|
|
72
108
|
|
|
73
109
|
subcommand "results", "Show results" do
|
|
74
110
|
|
|
111
|
+
include CanOutputResults
|
|
112
|
+
|
|
75
113
|
def execute
|
|
76
|
-
|
|
77
|
-
output_results_as_csv(query_results.result_set)
|
|
114
|
+
display_query_results(athena_client.get_query_results(query_execution_id: execution_id))
|
|
78
115
|
end
|
|
79
116
|
|
|
80
117
|
end
|
|
@@ -95,10 +132,28 @@ module Swa
|
|
|
95
132
|
|
|
96
133
|
option ["--database", "-D"], "NAME", "Database name"
|
|
97
134
|
option ["--output-location", "-O"], "S3_URL", "S3 output location for query results"
|
|
135
|
+
option ["--explain", "-E"], :flag, "Explain query"
|
|
136
|
+
option ["--timeout"], "SECONDS", "Time to wait for completion", default: 120, &method(:Integer)
|
|
98
137
|
|
|
99
|
-
|
|
138
|
+
include CanOutputResults
|
|
139
|
+
|
|
140
|
+
parameter "[QUERY]", "SQL query", default: "STDIN"
|
|
100
141
|
|
|
101
142
|
def execute
|
|
143
|
+
if explain?
|
|
144
|
+
self.query = "EXPLAIN #{query}"
|
|
145
|
+
self.text = true
|
|
146
|
+
end
|
|
147
|
+
results, statistics = execute_query(query)
|
|
148
|
+
show_statistics(statistics)
|
|
149
|
+
display_query_results(results)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def default_query
|
|
153
|
+
$stdin.read
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def execute_query(query)
|
|
102
157
|
start_query_response = athena_client.start_query_execution(
|
|
103
158
|
query_execution_context: {
|
|
104
159
|
catalog: catalog,
|
|
@@ -110,31 +165,36 @@ module Swa
|
|
|
110
165
|
},
|
|
111
166
|
work_group: workgroup
|
|
112
167
|
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
168
|
+
query_execution_id = start_query_response.query_execution_id
|
|
169
|
+
logger.debug "query_execution_id = #{query_execution_id}"
|
|
170
|
+
query_still_running = true
|
|
171
|
+
query_execution_output = wait_for_query(query_execution_id)
|
|
172
|
+
query_still_running = false
|
|
173
|
+
results = athena_client.get_query_results(query_execution_id: query_execution_id)
|
|
174
|
+
[results, query_execution_output.query_execution.statistics]
|
|
175
|
+
rescue Aws::Waiters::Errors::FailureStateError => e
|
|
176
|
+
query_still_running = false
|
|
177
|
+
signal_error e.response.query_execution.status.state_change_reason
|
|
178
|
+
ensure
|
|
179
|
+
if query_still_running
|
|
180
|
+
logger.warn "Cancelling query #{query_execution_id}"
|
|
181
|
+
athena_client.stop_query_execution(query_execution_id: query_execution_id)
|
|
119
182
|
end
|
|
120
183
|
end
|
|
121
184
|
|
|
122
|
-
private
|
|
123
|
-
|
|
124
|
-
def default_query
|
|
125
|
-
$stdin.read
|
|
126
|
-
end
|
|
127
|
-
|
|
128
185
|
def wait_for_query(query_execution_id)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
186
|
+
poll_interval = 5
|
|
187
|
+
max_attempts = timeout / poll_interval
|
|
188
|
+
QueryCompletionWaiter.new(
|
|
189
|
+
client: athena_client,
|
|
190
|
+
max_attempts: max_attempts,
|
|
191
|
+
delay: poll_interval
|
|
192
|
+
).wait(query_execution_id: query_execution_id)
|
|
132
193
|
end
|
|
133
194
|
|
|
134
|
-
def
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
end
|
|
195
|
+
def show_statistics(statistics)
|
|
196
|
+
logger.debug "Total execution time = #{statistics.total_execution_time_in_millis} ms"
|
|
197
|
+
logger.debug "Data scanned = #{ByteSize.bytes(statistics.data_scanned_in_bytes)}"
|
|
138
198
|
end
|
|
139
199
|
|
|
140
200
|
end
|
|
@@ -143,8 +203,6 @@ module Swa
|
|
|
143
203
|
|
|
144
204
|
include CollectionBehaviour
|
|
145
205
|
|
|
146
|
-
private
|
|
147
|
-
|
|
148
206
|
def collection
|
|
149
207
|
query_for(:list_work_groups, :work_groups, Swa::Athena::WorkGroup)
|
|
150
208
|
end
|
|
@@ -161,14 +219,6 @@ module Swa
|
|
|
161
219
|
model.list_from_query(athena_client, query_method, response_key, **query_args)
|
|
162
220
|
end
|
|
163
221
|
|
|
164
|
-
def output_results_as_csv(result_set)
|
|
165
|
-
CSV($stdout.dup) do |csv|
|
|
166
|
-
result_set.rows.each do |row|
|
|
167
|
-
csv << row.data.map(&:var_char_value)
|
|
168
|
-
end
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
|
|
172
222
|
class QueryCompletionWaiter
|
|
173
223
|
|
|
174
224
|
def initialize(options)
|
|
@@ -183,19 +233,19 @@ module Swa
|
|
|
183
233
|
"matcher" => "path",
|
|
184
234
|
"argument" => "query_execution.status.state",
|
|
185
235
|
"expected" => "SUCCEEDED",
|
|
186
|
-
"state" => "success"
|
|
236
|
+
"state" => "success"
|
|
187
237
|
},
|
|
188
238
|
{
|
|
189
239
|
"matcher" => "path",
|
|
190
240
|
"argument" => "query_execution.status.state",
|
|
191
241
|
"expected" => "FAILED",
|
|
192
|
-
"state" => "failure"
|
|
242
|
+
"state" => "failure"
|
|
193
243
|
},
|
|
194
244
|
{
|
|
195
245
|
"matcher" => "path",
|
|
196
246
|
"argument" => "query_execution.status.state",
|
|
197
247
|
"expected" => "CANCELLED",
|
|
198
|
-
"state" => "error"
|
|
248
|
+
"state" => "error"
|
|
199
249
|
}
|
|
200
250
|
]
|
|
201
251
|
)
|
|
@@ -211,4 +261,5 @@ module Swa
|
|
|
211
261
|
end
|
|
212
262
|
|
|
213
263
|
end
|
|
264
|
+
|
|
214
265
|
end
|
data/lib/swa/cli/base_command.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "chronic"
|
|
2
4
|
require "clamp"
|
|
3
5
|
require "console_logger"
|
|
@@ -7,26 +9,29 @@ require "swa/cli/data_output"
|
|
|
7
9
|
require "yaml"
|
|
8
10
|
|
|
9
11
|
module Swa
|
|
12
|
+
|
|
10
13
|
module CLI
|
|
11
14
|
|
|
12
15
|
class BaseCommand < Clamp::Command
|
|
13
16
|
|
|
14
17
|
option "--region", "REGION", "AWS region"
|
|
15
18
|
option "--access-key", "KEY", "AWS access key",
|
|
16
|
-
:
|
|
19
|
+
attribute_name: :access_key_id
|
|
17
20
|
option "--secret-key", "KEY", "AWS secret key",
|
|
18
|
-
:
|
|
21
|
+
attribute_name: :secret_access_key
|
|
19
22
|
option "--session-token", "KEY", "AWS security token",
|
|
20
|
-
:
|
|
23
|
+
attribute_name: :session_token
|
|
21
24
|
|
|
22
25
|
include DataOutput
|
|
23
26
|
|
|
24
27
|
option ["--debug"], :flag, "enable debugging"
|
|
25
28
|
|
|
26
29
|
def run(arguments)
|
|
27
|
-
super
|
|
30
|
+
super
|
|
28
31
|
rescue Aws::Errors::MissingCredentialsError
|
|
29
32
|
signal_error "no credentials provided"
|
|
33
|
+
rescue Aws::Errors::InvalidProcessCredentialsPayload => e
|
|
34
|
+
signal_error e.message
|
|
30
35
|
rescue Aws::Errors::MissingRegionError, Aws::Errors::InvalidRegionError => e
|
|
31
36
|
signal_error e.message
|
|
32
37
|
rescue Aws::Errors::ServiceError => e
|
|
@@ -37,6 +42,7 @@ module Swa
|
|
|
37
42
|
|
|
38
43
|
def parse_subcommand
|
|
39
44
|
return false unless self.class.has_subcommands?
|
|
45
|
+
|
|
40
46
|
request_help if subcommand_name == "?"
|
|
41
47
|
super
|
|
42
48
|
end
|
|
@@ -47,28 +53,30 @@ module Swa
|
|
|
47
53
|
|
|
48
54
|
def aws_config
|
|
49
55
|
{
|
|
50
|
-
:
|
|
51
|
-
:
|
|
52
|
-
:
|
|
53
|
-
:
|
|
54
|
-
:
|
|
55
|
-
}.
|
|
56
|
+
access_key_id: access_key_id,
|
|
57
|
+
secret_access_key: secret_access_key,
|
|
58
|
+
session_token: session_token,
|
|
59
|
+
region: region,
|
|
60
|
+
logger: logger, log_level: :debug
|
|
61
|
+
}.compact
|
|
56
62
|
end
|
|
57
63
|
|
|
58
64
|
def parse(arguments)
|
|
59
|
-
if arguments.first =~ /^(\w+)-[0-9a-f]+$/
|
|
60
|
-
arguments = [
|
|
65
|
+
if (arguments.first =~ /^(\w+)-[0-9a-f]+$/) && self.class.find_subcommand(::Regexp.last_match(1))
|
|
66
|
+
arguments = [::Regexp.last_match(1)] + arguments
|
|
61
67
|
end
|
|
62
|
-
super
|
|
68
|
+
super
|
|
63
69
|
end
|
|
64
70
|
|
|
65
71
|
def parse_datetime(datetime_string)
|
|
66
|
-
result = Chronic.parse(datetime_string, :
|
|
72
|
+
result = Chronic.parse(datetime_string, guess: false, endian_precedence: :little)
|
|
67
73
|
raise ArgumentError, "unrecognised date/time #{datetime_string.inspect}" unless result
|
|
74
|
+
|
|
68
75
|
result
|
|
69
76
|
end
|
|
70
77
|
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
end
|
|
81
|
+
|
|
74
82
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "aws-sdk-cloudformation"
|
|
2
4
|
require "swa/cli/base_command"
|
|
3
5
|
require "swa/cli/collection_behaviour"
|
|
@@ -5,6 +7,7 @@ require "swa/cli/item_behaviour"
|
|
|
5
7
|
require "swa/cloud_formation/stack"
|
|
6
8
|
|
|
7
9
|
module Swa
|
|
10
|
+
|
|
8
11
|
module CLI
|
|
9
12
|
|
|
10
13
|
class CloudFormationCommand < BaseCommand
|
|
@@ -23,8 +26,13 @@ module Swa
|
|
|
23
26
|
|
|
24
27
|
end
|
|
25
28
|
|
|
26
|
-
%w
|
|
27
|
-
|
|
29
|
+
%w[parameters outputs resources].each do |thing|
|
|
30
|
+
# subcommand "parameters", "Show parameters" do
|
|
31
|
+
# def execute
|
|
32
|
+
# display_data(stack.parameters)
|
|
33
|
+
# end
|
|
34
|
+
# end
|
|
35
|
+
class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition
|
|
28
36
|
subcommand "#{thing}", "Show #{thing}" do
|
|
29
37
|
def execute
|
|
30
38
|
display_data(stack.#{thing})
|
|
@@ -47,8 +55,6 @@ module Swa
|
|
|
47
55
|
|
|
48
56
|
include CollectionBehaviour
|
|
49
57
|
|
|
50
|
-
private
|
|
51
|
-
|
|
52
58
|
def collection
|
|
53
59
|
query_for(:stacks, Swa::CloudFormation::Stack)
|
|
54
60
|
end
|
|
@@ -69,4 +75,5 @@ module Swa
|
|
|
69
75
|
end
|
|
70
76
|
|
|
71
77
|
end
|
|
78
|
+
|
|
72
79
|
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "aws-sdk-cloudtrail"
|
|
4
|
+
require "swa/cli/base_command"
|
|
5
|
+
require "swa/cli/collection_behaviour"
|
|
6
|
+
require "swa/cloud_trail/event"
|
|
7
|
+
|
|
8
|
+
module Swa
|
|
9
|
+
|
|
10
|
+
module CLI
|
|
11
|
+
|
|
12
|
+
class CloudtrailCommand < BaseCommand
|
|
13
|
+
|
|
14
|
+
subcommand "events", "CloudTrail events" do
|
|
15
|
+
|
|
16
|
+
include CollectionBehaviour
|
|
17
|
+
|
|
18
|
+
option ["-n", "--max"], "N", "number of events to return", default: 50 do |n|
|
|
19
|
+
Integer(n)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
option "--source", "SERVICE", "filter by event source (e.g. kms)" do |value|
|
|
23
|
+
value = "#{value}.amazonaws.com" unless value.include?(".")
|
|
24
|
+
compile_pattern(value)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
option "--name", "EVENT_NAME", "filter by event name (e.g. Decrypt)" do |value|
|
|
28
|
+
compile_pattern(value)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
option %w[-a --after], "TIME", "filter events after this time" do |value|
|
|
32
|
+
parse_datetime(value).end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
option %w[-b --before], "TIME", "filter events before this time" do |value|
|
|
36
|
+
parse_datetime(value).begin
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
option "--where", "FIELD=VALUE", "filter by field (can be specified multiple times)",
|
|
40
|
+
multivalued: true do |spec|
|
|
41
|
+
field, value = spec.split("=", 2)
|
|
42
|
+
raise ArgumentError, "invalid --where format, expected FIELD=VALUE" if field.nil? || value.nil?
|
|
43
|
+
|
|
44
|
+
{ field: field, pattern: compile_pattern(value) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def collection
|
|
50
|
+
query_args = {}
|
|
51
|
+
|
|
52
|
+
# CloudTrail API only supports ONE lookup attribute at a time
|
|
53
|
+
# Use API filter for exact matches (not patterns with wildcards)
|
|
54
|
+
if name.is_a?(String)
|
|
55
|
+
query_args[:lookup_attributes] = [{ attribute_key: "EventName", attribute_value: name }]
|
|
56
|
+
elsif source.is_a?(String)
|
|
57
|
+
query_args[:lookup_attributes] = [{ attribute_key: "EventSource", attribute_value: source }]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Add time range filters
|
|
61
|
+
query_args[:start_time] = after if after
|
|
62
|
+
query_args[:end_time] = before if before
|
|
63
|
+
|
|
64
|
+
events = query_for(:lookup_events, :events, Swa::CloudTrail::Event, **query_args)
|
|
65
|
+
|
|
66
|
+
# Apply programmatic filters
|
|
67
|
+
events = events.select { |event| name === event.event_name } if name # rubocop:disable Style/CaseEquality
|
|
68
|
+
events = events.select { |event| source === event.event_source } if source # rubocop:disable Style/CaseEquality
|
|
69
|
+
|
|
70
|
+
# Apply --where filters
|
|
71
|
+
if where_list && !where_list.empty?
|
|
72
|
+
events = events.select do |event|
|
|
73
|
+
where_list.all? { |condition| matches_where_condition?(event, condition) }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
events.take(max)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def compile_pattern(value)
|
|
81
|
+
if value.include?("*") || value.include?("?")
|
|
82
|
+
# Convert shell-style wildcards to regex
|
|
83
|
+
regex_pattern = Regexp.escape(value).gsub('\*', ".*").gsub('\?', ".")
|
|
84
|
+
Regexp.new("^#{regex_pattern}$", Regexp::IGNORECASE)
|
|
85
|
+
else
|
|
86
|
+
# Return as string for exact match and API filtering
|
|
87
|
+
value
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def matches_where_condition?(event, condition)
|
|
92
|
+
field_path = condition[:field]
|
|
93
|
+
pattern = condition[:pattern]
|
|
94
|
+
|
|
95
|
+
# Get the event data as a hash
|
|
96
|
+
event_data = event.data
|
|
97
|
+
|
|
98
|
+
# Extract field value using JMESPath
|
|
99
|
+
begin
|
|
100
|
+
field_value = JMESPath.search(field_path, event_data)
|
|
101
|
+
rescue JMESPath::Errors::SyntaxError
|
|
102
|
+
signal_error("invalid field path in --where: #{field_path}")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Convert field value to string for matching
|
|
106
|
+
return false if field_value.nil?
|
|
107
|
+
|
|
108
|
+
field_value_str = field_value.to_s
|
|
109
|
+
|
|
110
|
+
# Match using pattern (either string or regex)
|
|
111
|
+
pattern === field_value_str # rubocop:disable Style/CaseEquality
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
protected
|
|
117
|
+
|
|
118
|
+
def cloudtrail_client
|
|
119
|
+
::Aws::CloudTrail::Client.new(aws_config)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def query_for(query_method, response_key, model, **query_args)
|
|
123
|
+
model.list_from_query(cloudtrail_client, query_method, response_key, **query_args)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
end
|
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "swa/cli/selector"
|
|
2
4
|
|
|
3
5
|
module Swa
|
|
6
|
+
|
|
4
7
|
module CLI
|
|
5
8
|
|
|
6
9
|
module CollectionBehaviour
|
|
7
10
|
|
|
8
11
|
def self.included(target)
|
|
9
|
-
|
|
10
12
|
target.default_subcommand = "summary"
|
|
11
13
|
|
|
12
14
|
target.subcommand ["summary", "s"], "One-line summary" do
|
|
@@ -36,7 +38,6 @@ module Swa
|
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
end
|
|
39
|
-
|
|
40
41
|
end
|
|
41
42
|
|
|
42
43
|
def selector
|
|
@@ -50,4 +51,5 @@ module Swa
|
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
end
|
|
54
|
+
|
|
53
55
|
end
|
data/lib/swa/cli/data_output.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require "clamp"
|
|
2
4
|
|
|
3
5
|
module Swa
|
|
6
|
+
|
|
4
7
|
module CLI
|
|
5
8
|
|
|
6
9
|
module DataOutput
|
|
@@ -8,9 +11,9 @@ module Swa
|
|
|
8
11
|
extend Clamp::Option::Declaration
|
|
9
12
|
|
|
10
13
|
option "--format", "FORMAT", "format for data output",
|
|
11
|
-
:
|
|
12
|
-
:
|
|
13
|
-
:
|
|
14
|
+
attribute_name: :output_format,
|
|
15
|
+
environment_variable: "SWA_OUTPUT_FORMAT",
|
|
16
|
+
default: "YAML"
|
|
14
17
|
|
|
15
18
|
option ["--json", "-J"], :flag, "output data in JSON format" do
|
|
16
19
|
self.output_format = "JSON"
|
|
@@ -22,9 +25,8 @@ module Swa
|
|
|
22
25
|
|
|
23
26
|
def output_format=(arg)
|
|
24
27
|
arg = arg.upcase
|
|
25
|
-
unless %w
|
|
26
|
-
|
|
27
|
-
end
|
|
28
|
+
raise ArgumentError, "unrecognised data format: #{arg.inspect}" unless %w[JSON YAML].member?(arg)
|
|
29
|
+
|
|
28
30
|
@output_format = arg
|
|
29
31
|
end
|
|
30
32
|
|
|
@@ -33,7 +35,7 @@ module Swa
|
|
|
33
35
|
def format_data(data)
|
|
34
36
|
case output_format
|
|
35
37
|
when "JSON"
|
|
36
|
-
MultiJson.dump(data, :
|
|
38
|
+
MultiJson.dump(data, pretty: true)
|
|
37
39
|
when "YAML"
|
|
38
40
|
YAML.dump(data)
|
|
39
41
|
else
|
|
@@ -42,15 +44,14 @@ module Swa
|
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def display_data(data, jmespath_expression = nil)
|
|
45
|
-
unless jmespath_expression.nil?
|
|
46
|
-
data = JMESPath.search(jmespath_expression, data)
|
|
47
|
-
end
|
|
47
|
+
data = JMESPath.search(jmespath_expression, data) unless jmespath_expression.nil?
|
|
48
48
|
puts format_data(data)
|
|
49
|
-
rescue JMESPath::Errors::SyntaxError
|
|
49
|
+
rescue JMESPath::Errors::SyntaxError
|
|
50
50
|
signal_error("invalid JMESPath expression")
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
end
|
|
56
|
+
|
|
56
57
|
end
|