awsquery 0.1.0__py3-none-any.whl
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.
- awsquery/__init__.py +7 -0
- awsquery/cli.py +312 -0
- awsquery/config.py +61 -0
- awsquery/core.py +698 -0
- awsquery/default_filters.yaml +76 -0
- awsquery/filters.py +308 -0
- awsquery/formatters.py +372 -0
- awsquery/policy.json +2521 -0
- awsquery/security.py +95 -0
- awsquery/utils.py +115 -0
- awsquery-0.1.0.dist-info/METADATA +447 -0
- awsquery-0.1.0.dist-info/RECORD +16 -0
- awsquery-0.1.0.dist-info/WHEEL +5 -0
- awsquery-0.1.0.dist-info/entry_points.txt +2 -0
- awsquery-0.1.0.dist-info/licenses/LICENSE +21 -0
- awsquery-0.1.0.dist-info/top_level.txt +1 -0
awsquery/__init__.py
ADDED
awsquery/cli.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Command-line interface for AWS Query Tool."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import argcomplete
|
|
8
|
+
import boto3
|
|
9
|
+
|
|
10
|
+
from .config import apply_default_filters
|
|
11
|
+
from .core import (
|
|
12
|
+
execute_aws_call,
|
|
13
|
+
execute_multi_level_call,
|
|
14
|
+
execute_multi_level_call_with_tracking,
|
|
15
|
+
execute_with_tracking,
|
|
16
|
+
show_keys_from_result,
|
|
17
|
+
)
|
|
18
|
+
from .filters import filter_resources, parse_multi_level_filters_for_mode
|
|
19
|
+
from .formatters import (
|
|
20
|
+
extract_and_sort_keys,
|
|
21
|
+
flatten_response,
|
|
22
|
+
format_json_output,
|
|
23
|
+
format_table_output,
|
|
24
|
+
show_keys,
|
|
25
|
+
)
|
|
26
|
+
from .security import action_to_policy_format, load_security_policy, validate_security
|
|
27
|
+
from .utils import create_session, debug_print, get_aws_services, sanitize_input
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def service_completer(prefix, parsed_args, **kwargs):
|
|
31
|
+
"""Autocomplete AWS service names"""
|
|
32
|
+
session = boto3.Session()
|
|
33
|
+
services = session.get_available_services()
|
|
34
|
+
return [s for s in services if s.startswith(prefix)]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def determine_column_filters(column_filters, service, action):
|
|
38
|
+
"""Determine which column filters to apply - user specified or defaults"""
|
|
39
|
+
if column_filters:
|
|
40
|
+
debug_print(f"Using user-specified column filters: {column_filters}")
|
|
41
|
+
return column_filters
|
|
42
|
+
|
|
43
|
+
# Check for defaults - normalize action name for lookup
|
|
44
|
+
from .utils import normalize_action_name
|
|
45
|
+
|
|
46
|
+
normalized_action = normalize_action_name(action)
|
|
47
|
+
default_columns = apply_default_filters(service, normalized_action)
|
|
48
|
+
if default_columns:
|
|
49
|
+
debug_print(
|
|
50
|
+
f"Applying default column filters for {service}.{normalized_action}: {default_columns}"
|
|
51
|
+
)
|
|
52
|
+
return default_columns
|
|
53
|
+
|
|
54
|
+
debug_print(f"No column filters (user or default) for {service}.{normalized_action}")
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def action_completer(prefix, parsed_args, **kwargs):
|
|
59
|
+
"""Autocomplete action names based on selected service"""
|
|
60
|
+
if not parsed_args.service:
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
client = boto3.client(parsed_args.service)
|
|
65
|
+
operations = client.meta.service_model.operation_names
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
allowed_actions = load_security_policy()
|
|
69
|
+
except:
|
|
70
|
+
allowed_actions = set()
|
|
71
|
+
for op in operations:
|
|
72
|
+
if any(op.startswith(prefix) for prefix in ["Describe", "List", "Get"]):
|
|
73
|
+
allowed_actions.add(f"{parsed_args.service}:{op}")
|
|
74
|
+
|
|
75
|
+
cli_operations = []
|
|
76
|
+
for op in operations:
|
|
77
|
+
if not validate_security(parsed_args.service, op, allowed_actions):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
kebab_case = re.sub("([a-z0-9])([A-Z])", r"\1-\2", op).lower()
|
|
81
|
+
cli_operations.append(kebab_case)
|
|
82
|
+
|
|
83
|
+
matched_ops = [op for op in cli_operations if op.startswith(prefix)]
|
|
84
|
+
return sorted(list(set(matched_ops)))
|
|
85
|
+
except:
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def main():
|
|
90
|
+
parser = argparse.ArgumentParser(
|
|
91
|
+
description="Query AWS APIs with flexible filtering and automatic parameter resolution",
|
|
92
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
93
|
+
epilog="""
|
|
94
|
+
Examples:
|
|
95
|
+
awsquery ec2 describe_instances prod web -- Tags.Name State InstanceId
|
|
96
|
+
awsquery s3 list_buckets backup
|
|
97
|
+
awsquery ec2 describe_instances (shows available keys)
|
|
98
|
+
awsquery cloudformation describe-stack-events prod -- Created -- StackName (multi-level)
|
|
99
|
+
awsquery ec2 describe_instances --keys (show all keys)
|
|
100
|
+
awsquery cloudformation describe-stack-resources workers --keys -- EKS (multi-level keys)
|
|
101
|
+
awsquery ec2 describe_instances --debug (enable debug output)
|
|
102
|
+
awsquery cloudformation describe-stack-resources workers --debug -- EKS (debug multi-level)
|
|
103
|
+
""",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"-j", "--json", action="store_true", help="Output results in JSON format instead of table"
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"-k", "--keys", action="store_true", help="Show all available keys for the command"
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument("-d", "--debug", action="store_true", help="Enable debug output")
|
|
113
|
+
parser.add_argument("--region", help="AWS region to use for requests")
|
|
114
|
+
parser.add_argument("--profile", help="AWS profile to use for requests")
|
|
115
|
+
|
|
116
|
+
service_arg = parser.add_argument("service", nargs="?", help="AWS service name")
|
|
117
|
+
service_arg.completer = service_completer # type: ignore[attr-defined]
|
|
118
|
+
|
|
119
|
+
action_arg = parser.add_argument("action", nargs="?", help="Service action name")
|
|
120
|
+
action_arg.completer = action_completer # type: ignore[attr-defined]
|
|
121
|
+
|
|
122
|
+
argcomplete.autocomplete(parser)
|
|
123
|
+
|
|
124
|
+
# First pass: parse known args to get service and action
|
|
125
|
+
args, remaining = parser.parse_known_args()
|
|
126
|
+
|
|
127
|
+
# If there are remaining args, check if any are flags that should be parsed
|
|
128
|
+
# This handles cases where flags appear after service/action or after --
|
|
129
|
+
if remaining:
|
|
130
|
+
# Re-parse with the full argument list to catch all flags
|
|
131
|
+
# We need to build a new argv that puts flags before positional args
|
|
132
|
+
reordered_argv = [sys.argv[0]] # Program name
|
|
133
|
+
flags = []
|
|
134
|
+
non_flags = []
|
|
135
|
+
|
|
136
|
+
# Separate flags from non-flags in remaining args
|
|
137
|
+
i = 0
|
|
138
|
+
while i < len(remaining):
|
|
139
|
+
arg = remaining[i]
|
|
140
|
+
if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
|
|
141
|
+
flags.append(arg)
|
|
142
|
+
elif arg in ["--region", "--profile"]:
|
|
143
|
+
# These flags take a value
|
|
144
|
+
flags.append(arg)
|
|
145
|
+
if i + 1 < len(remaining):
|
|
146
|
+
flags.append(remaining[i + 1])
|
|
147
|
+
i += 1
|
|
148
|
+
else:
|
|
149
|
+
non_flags.append(arg)
|
|
150
|
+
i += 1
|
|
151
|
+
|
|
152
|
+
# Add original flags from sys.argv that were already parsed
|
|
153
|
+
for arg in sys.argv[1:]:
|
|
154
|
+
if arg in ["-d", "--debug", "-j", "--json", "-k", "--keys"]:
|
|
155
|
+
if arg not in flags:
|
|
156
|
+
reordered_argv.append(arg)
|
|
157
|
+
elif arg == "--region" and args.region:
|
|
158
|
+
reordered_argv.extend(["--region", args.region])
|
|
159
|
+
elif arg == "--profile" and args.profile:
|
|
160
|
+
reordered_argv.extend(["--profile", args.profile])
|
|
161
|
+
|
|
162
|
+
# Add newly found flags
|
|
163
|
+
reordered_argv.extend(flags)
|
|
164
|
+
|
|
165
|
+
# Add service and action
|
|
166
|
+
if args.service:
|
|
167
|
+
reordered_argv.append(args.service)
|
|
168
|
+
if args.action:
|
|
169
|
+
reordered_argv.append(args.action)
|
|
170
|
+
|
|
171
|
+
# Re-parse with reordered arguments
|
|
172
|
+
args, remaining = parser.parse_known_args(reordered_argv[1:])
|
|
173
|
+
|
|
174
|
+
# Remaining should now only be non-flag arguments
|
|
175
|
+
remaining = non_flags
|
|
176
|
+
|
|
177
|
+
# Set debug mode globally
|
|
178
|
+
from . import utils
|
|
179
|
+
|
|
180
|
+
utils.debug_enabled = args.debug
|
|
181
|
+
|
|
182
|
+
# Build the argv for filter parsing (service, action, and remaining arguments)
|
|
183
|
+
filter_argv = []
|
|
184
|
+
if args.service:
|
|
185
|
+
filter_argv.append(args.service)
|
|
186
|
+
if args.action:
|
|
187
|
+
filter_argv.append(args.action)
|
|
188
|
+
# Add the remaining arguments (filters, --, column names, etc.)
|
|
189
|
+
filter_argv.extend(remaining)
|
|
190
|
+
|
|
191
|
+
base_command, resource_filters, value_filters, column_filters = (
|
|
192
|
+
parse_multi_level_filters_for_mode(filter_argv, mode="single")
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
if not args.service or not args.action:
|
|
196
|
+
services = get_aws_services()
|
|
197
|
+
print("Available services:", ", ".join(services))
|
|
198
|
+
sys.exit(0)
|
|
199
|
+
|
|
200
|
+
service = sanitize_input(args.service)
|
|
201
|
+
action = sanitize_input(args.action)
|
|
202
|
+
resource_filters = [sanitize_input(f) for f in resource_filters] if resource_filters else []
|
|
203
|
+
value_filters = [sanitize_input(f) for f in value_filters] if value_filters else []
|
|
204
|
+
column_filters = [sanitize_input(f) for f in column_filters] if column_filters else []
|
|
205
|
+
|
|
206
|
+
allowed_actions = load_security_policy()
|
|
207
|
+
|
|
208
|
+
policy_action = action_to_policy_format(action)
|
|
209
|
+
|
|
210
|
+
debug_print(
|
|
211
|
+
f"DEBUG: Checking security for service='{service}', "
|
|
212
|
+
f"action='{action}', policy_action='{policy_action}'"
|
|
213
|
+
)
|
|
214
|
+
debug_print(f"DEBUG: Policy has {len(allowed_actions)} allowed actions")
|
|
215
|
+
|
|
216
|
+
if not validate_security(service, policy_action, allowed_actions):
|
|
217
|
+
print(f"ERROR: Action {service}:{action} not permitted by security policy", file=sys.stderr)
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
else:
|
|
220
|
+
debug_print(f"DEBUG: Action {service}:{policy_action} IS ALLOWED by security policy")
|
|
221
|
+
|
|
222
|
+
# Create session with region/profile if specified
|
|
223
|
+
session = create_session(region=args.region, profile=args.profile)
|
|
224
|
+
debug_print(f"DEBUG: Created session with region={args.region}, profile={args.profile}")
|
|
225
|
+
|
|
226
|
+
# Determine final column filters (user-specified or defaults)
|
|
227
|
+
final_column_filters = determine_column_filters(column_filters, service, action)
|
|
228
|
+
|
|
229
|
+
if args.keys:
|
|
230
|
+
print(f"Showing all available keys for {service}.{action}:", file=sys.stderr)
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
# Use tracking to get keys from the last successful request
|
|
234
|
+
call_result = execute_with_tracking(service, action, session=session)
|
|
235
|
+
|
|
236
|
+
# If the initial call failed, try multi-level resolution
|
|
237
|
+
if not call_result.final_success:
|
|
238
|
+
debug_print("Keys mode: Initial call failed, trying multi-level resolution")
|
|
239
|
+
_, multi_resource_filters, multi_value_filters, multi_column_filters = (
|
|
240
|
+
parse_multi_level_filters_for_mode(filter_argv, mode="multi")
|
|
241
|
+
)
|
|
242
|
+
call_result, _ = execute_multi_level_call_with_tracking(
|
|
243
|
+
service,
|
|
244
|
+
action,
|
|
245
|
+
multi_resource_filters,
|
|
246
|
+
multi_value_filters,
|
|
247
|
+
multi_column_filters,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
result = show_keys_from_result(call_result)
|
|
251
|
+
print(result)
|
|
252
|
+
return
|
|
253
|
+
except Exception as e:
|
|
254
|
+
print(f"Could not retrieve keys: {e}", file=sys.stderr)
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
debug_print(f"Using single-level execution first")
|
|
259
|
+
response = execute_aws_call(service, action, session=session)
|
|
260
|
+
|
|
261
|
+
if isinstance(response, dict) and "validation_error" in response:
|
|
262
|
+
debug_print(f"ValidationError detected in single-level call, switching to multi-level")
|
|
263
|
+
_, multi_resource_filters, multi_value_filters, multi_column_filters = (
|
|
264
|
+
parse_multi_level_filters_for_mode(filter_argv, mode="multi")
|
|
265
|
+
)
|
|
266
|
+
debug_print(
|
|
267
|
+
f"Re-parsed filters for multi-level - "
|
|
268
|
+
f"Resource: {multi_resource_filters}, Value: {multi_value_filters}, "
|
|
269
|
+
f"Column: {multi_column_filters}"
|
|
270
|
+
)
|
|
271
|
+
# Apply defaults for multi-level if no user columns specified
|
|
272
|
+
final_multi_column_filters = determine_column_filters(
|
|
273
|
+
multi_column_filters, service, action
|
|
274
|
+
)
|
|
275
|
+
filtered_resources = execute_multi_level_call(
|
|
276
|
+
service,
|
|
277
|
+
action,
|
|
278
|
+
multi_resource_filters,
|
|
279
|
+
multi_value_filters,
|
|
280
|
+
final_multi_column_filters,
|
|
281
|
+
session,
|
|
282
|
+
)
|
|
283
|
+
debug_print(f"Multi-level call completed with {len(filtered_resources)} resources")
|
|
284
|
+
else:
|
|
285
|
+
resources = flatten_response(response)
|
|
286
|
+
debug_print(f"Total resources extracted: {len(resources)}")
|
|
287
|
+
|
|
288
|
+
filtered_resources = filter_resources(resources, value_filters)
|
|
289
|
+
|
|
290
|
+
if final_column_filters:
|
|
291
|
+
for filter_word in final_column_filters:
|
|
292
|
+
debug_print(f"Applying column filter: {filter_word}")
|
|
293
|
+
|
|
294
|
+
if args.keys:
|
|
295
|
+
sorted_keys = extract_and_sort_keys(filtered_resources)
|
|
296
|
+
output = "\n".join(f" {key}" for key in sorted_keys)
|
|
297
|
+
print(f"All available keys:", file=sys.stderr)
|
|
298
|
+
print(output)
|
|
299
|
+
else:
|
|
300
|
+
if args.json:
|
|
301
|
+
output = format_json_output(filtered_resources, final_column_filters)
|
|
302
|
+
else:
|
|
303
|
+
output = format_table_output(filtered_resources, final_column_filters)
|
|
304
|
+
print(output)
|
|
305
|
+
|
|
306
|
+
except KeyboardInterrupt:
|
|
307
|
+
print("\nOperation cancelled by user.", file=sys.stderr)
|
|
308
|
+
sys.exit(1)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if __name__ == "__main__":
|
|
312
|
+
main()
|
awsquery/config.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Configuration management for AWS Query Tool."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
|
|
8
|
+
from .utils import debug_print
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@lru_cache(maxsize=1)
|
|
12
|
+
def load_default_filters():
|
|
13
|
+
"""Load default filters with caching and error handling"""
|
|
14
|
+
# Load default_filters.yaml from the package directory only
|
|
15
|
+
config_path = os.path.join(os.path.dirname(__file__), "default_filters.yaml")
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
with open(config_path, "r") as f:
|
|
19
|
+
config = yaml.safe_load(f)
|
|
20
|
+
debug_print(f"Loaded default filters configuration from {config_path}")
|
|
21
|
+
return config
|
|
22
|
+
except FileNotFoundError:
|
|
23
|
+
debug_print(f"Warning: {config_path} not found, no defaults will be applied")
|
|
24
|
+
return {}
|
|
25
|
+
except yaml.YAMLError as e:
|
|
26
|
+
debug_print(f"Warning: Could not parse {config_path}: {e}")
|
|
27
|
+
return {}
|
|
28
|
+
except Exception as e:
|
|
29
|
+
debug_print(f"Warning: Could not load default filters from {config_path}: {e}")
|
|
30
|
+
return {}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_default_columns(service, action):
|
|
34
|
+
"""Get default columns for service.action combination"""
|
|
35
|
+
config = load_default_filters()
|
|
36
|
+
|
|
37
|
+
service_config = config.get(service.lower(), {})
|
|
38
|
+
action_config = service_config.get(action.lower(), {})
|
|
39
|
+
|
|
40
|
+
columns = action_config.get("columns", [])
|
|
41
|
+
if columns:
|
|
42
|
+
debug_print(f"Found default columns for {service}.{action}: {columns}")
|
|
43
|
+
else:
|
|
44
|
+
debug_print(f"No default columns configured for {service}.{action}")
|
|
45
|
+
|
|
46
|
+
return columns
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def apply_default_filters(service, action, user_columns=None):
|
|
50
|
+
"""Apply default filters if no user columns specified"""
|
|
51
|
+
if user_columns:
|
|
52
|
+
debug_print("User specified columns, skipping defaults")
|
|
53
|
+
return user_columns
|
|
54
|
+
|
|
55
|
+
defaults = get_default_columns(service, action)
|
|
56
|
+
if defaults:
|
|
57
|
+
debug_print(f"Using default columns for {service}.{action}: {defaults}")
|
|
58
|
+
return defaults
|
|
59
|
+
|
|
60
|
+
debug_print(f"No default columns found for {service}.{action}")
|
|
61
|
+
return None # No filtering applied
|