iam-policy-validator 1.10.3__py3-none-any.whl → 1.11.1__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.
- iam_policy_validator-1.11.1.dist-info/METADATA +782 -0
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/RECORD +25 -21
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +27 -14
- iam_validator/checks/sensitive_action.py +123 -11
- iam_validator/checks/utils/policy_level_checks.py +47 -10
- iam_validator/commands/__init__.py +6 -0
- iam_validator/commands/completion.py +467 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +21 -26
- iam_validator/core/config/category_suggestions.py +77 -0
- iam_validator/core/config/condition_requirements.py +105 -54
- iam_validator/core/config/defaults.py +82 -6
- iam_validator/core/config/wildcards.py +3 -0
- iam_validator/core/diff_parser.py +321 -0
- iam_validator/core/formatters/enhanced.py +34 -27
- iam_validator/core/models.py +2 -0
- iam_validator/core/pr_commenter.py +179 -51
- iam_validator/core/report.py +19 -17
- iam_validator/integrations/github_integration.py +250 -1
- iam_validator/sdk/__init__.py +33 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_policy_validator-1.10.3.dist-info/METADATA +0 -549
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
"""Query AWS service definitions - actions, ARNs, and condition keys.
|
|
2
|
+
|
|
3
|
+
This command allows querying AWS IAM service metadata similar to policy_sentry.
|
|
4
|
+
Implementation inspired by: https://github.com/salesforce/policy_sentry
|
|
5
|
+
|
|
6
|
+
Examples:
|
|
7
|
+
# Query all actions for a service
|
|
8
|
+
iam-validator query action --service s3
|
|
9
|
+
|
|
10
|
+
# Query write-level actions
|
|
11
|
+
iam-validator query action --service s3 --access-level write
|
|
12
|
+
|
|
13
|
+
# Query actions that support wildcard resource
|
|
14
|
+
iam-validator query action --service s3 --resource-type "*"
|
|
15
|
+
|
|
16
|
+
# Query action details
|
|
17
|
+
iam-validator query action --service s3 --name GetObject
|
|
18
|
+
|
|
19
|
+
# Query ARN formats for a service
|
|
20
|
+
iam-validator query arn --service s3
|
|
21
|
+
|
|
22
|
+
# Query specific ARN type
|
|
23
|
+
iam-validator query arn --service s3 --name bucket
|
|
24
|
+
|
|
25
|
+
# Query condition keys
|
|
26
|
+
iam-validator query condition --service s3
|
|
27
|
+
|
|
28
|
+
# Query specific condition key
|
|
29
|
+
iam-validator query condition --service s3 --name s3:prefix
|
|
30
|
+
|
|
31
|
+
# Text format for simple output (great for piping)
|
|
32
|
+
iam-validator query action --service s3 --output text | grep Delete
|
|
33
|
+
iam-validator query action --service iam --access-level write --output text
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
import argparse
|
|
37
|
+
import json
|
|
38
|
+
import logging
|
|
39
|
+
import sys
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
import yaml
|
|
43
|
+
|
|
44
|
+
from iam_validator.commands.base import Command
|
|
45
|
+
from iam_validator.core.aws_service.fetcher import AWSServiceFetcher
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class QueryCommand(Command):
|
|
51
|
+
"""Query AWS service definitions."""
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def name(self) -> str:
|
|
55
|
+
"""Command name."""
|
|
56
|
+
return "query"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def help(self) -> str:
|
|
60
|
+
"""Command help text."""
|
|
61
|
+
return "Query AWS service definitions (actions, ARNs, condition keys)"
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def epilog(self) -> str:
|
|
65
|
+
"""Command epilog with examples."""
|
|
66
|
+
return """
|
|
67
|
+
examples:
|
|
68
|
+
# Query all actions for a service
|
|
69
|
+
iam-validator query action --service s3
|
|
70
|
+
|
|
71
|
+
# Query write-level actions
|
|
72
|
+
iam-validator query action --service s3 --access-level write
|
|
73
|
+
|
|
74
|
+
# Query actions that support wildcard resource
|
|
75
|
+
iam-validator query action --service s3 --resource-type "*"
|
|
76
|
+
|
|
77
|
+
# Query action details
|
|
78
|
+
iam-validator query action --service s3 --name GetObject
|
|
79
|
+
|
|
80
|
+
# Query ARN formats for a service
|
|
81
|
+
iam-validator query arn --service s3
|
|
82
|
+
|
|
83
|
+
# Query specific ARN type
|
|
84
|
+
iam-validator query arn --service s3 --name bucket
|
|
85
|
+
|
|
86
|
+
# Query condition keys
|
|
87
|
+
iam-validator query condition --service s3
|
|
88
|
+
|
|
89
|
+
# Query specific condition key
|
|
90
|
+
iam-validator query condition --service s3 --name s3:prefix
|
|
91
|
+
|
|
92
|
+
# Text format for simple output (great for piping)
|
|
93
|
+
iam-validator query action --service s3 --output text | grep Delete
|
|
94
|
+
iam-validator query action --service iam --access-level write --output text
|
|
95
|
+
|
|
96
|
+
note:
|
|
97
|
+
This feature is inspired by policy_sentry's query functionality.
|
|
98
|
+
See: https://github.com/salesforce/policy_sentry
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def add_arguments(self, parser: argparse.ArgumentParser) -> None:
|
|
102
|
+
"""Add query command arguments."""
|
|
103
|
+
# Add subparsers for different query types
|
|
104
|
+
subparsers = parser.add_subparsers(
|
|
105
|
+
dest="query_type",
|
|
106
|
+
help="Type of query to perform",
|
|
107
|
+
required=True,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Action query
|
|
111
|
+
action_parser = subparsers.add_parser(
|
|
112
|
+
"action",
|
|
113
|
+
help="Query IAM actions",
|
|
114
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
115
|
+
)
|
|
116
|
+
action_parser.add_argument(
|
|
117
|
+
"--service",
|
|
118
|
+
required=True,
|
|
119
|
+
help="AWS service prefix (e.g., s3, iam, ec2)",
|
|
120
|
+
)
|
|
121
|
+
action_parser.add_argument(
|
|
122
|
+
"--name",
|
|
123
|
+
help="Specific action name (e.g., GetObject, CreateUser)",
|
|
124
|
+
)
|
|
125
|
+
action_parser.add_argument(
|
|
126
|
+
"--access-level",
|
|
127
|
+
choices=["read", "write", "list", "tagging", "permissions-management"],
|
|
128
|
+
help="Filter by access level",
|
|
129
|
+
)
|
|
130
|
+
action_parser.add_argument(
|
|
131
|
+
"--resource-type",
|
|
132
|
+
help='Filter by resource type (use "*" for wildcard-only actions)',
|
|
133
|
+
)
|
|
134
|
+
action_parser.add_argument(
|
|
135
|
+
"--condition",
|
|
136
|
+
help="Filter actions that support specific condition key",
|
|
137
|
+
)
|
|
138
|
+
action_parser.add_argument(
|
|
139
|
+
"--output",
|
|
140
|
+
choices=["json", "yaml", "text"],
|
|
141
|
+
default="json",
|
|
142
|
+
help="Output format (default: json)",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# ARN query
|
|
146
|
+
arn_parser = subparsers.add_parser(
|
|
147
|
+
"arn",
|
|
148
|
+
help="Query ARN formats",
|
|
149
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
150
|
+
)
|
|
151
|
+
arn_parser.add_argument(
|
|
152
|
+
"--service",
|
|
153
|
+
required=True,
|
|
154
|
+
help="AWS service prefix (e.g., s3, iam, ec2)",
|
|
155
|
+
)
|
|
156
|
+
arn_parser.add_argument(
|
|
157
|
+
"--name",
|
|
158
|
+
help="Specific ARN resource type name (e.g., bucket, role)",
|
|
159
|
+
)
|
|
160
|
+
arn_parser.add_argument(
|
|
161
|
+
"--list-arn-types",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help="List all ARN types with their formats",
|
|
164
|
+
)
|
|
165
|
+
arn_parser.add_argument(
|
|
166
|
+
"--output",
|
|
167
|
+
choices=["json", "yaml", "text"],
|
|
168
|
+
default="json",
|
|
169
|
+
help="Output format (default: json)",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Condition query
|
|
173
|
+
condition_parser = subparsers.add_parser(
|
|
174
|
+
"condition",
|
|
175
|
+
help="Query condition keys",
|
|
176
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
177
|
+
)
|
|
178
|
+
condition_parser.add_argument(
|
|
179
|
+
"--service",
|
|
180
|
+
required=True,
|
|
181
|
+
help="AWS service prefix (e.g., s3, iam, ec2)",
|
|
182
|
+
)
|
|
183
|
+
condition_parser.add_argument(
|
|
184
|
+
"--name",
|
|
185
|
+
help="Specific condition key name (e.g., s3:prefix, iam:PolicyArn)",
|
|
186
|
+
)
|
|
187
|
+
condition_parser.add_argument(
|
|
188
|
+
"--output",
|
|
189
|
+
choices=["json", "yaml", "text"],
|
|
190
|
+
default="json",
|
|
191
|
+
help="Output format (default: json)",
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
async def execute(self, args: argparse.Namespace) -> int:
|
|
195
|
+
"""Execute query command."""
|
|
196
|
+
try:
|
|
197
|
+
async with AWSServiceFetcher(prefetch_common=False) as fetcher:
|
|
198
|
+
if args.query_type == "action":
|
|
199
|
+
result = await self._query_action_table(fetcher, args)
|
|
200
|
+
elif args.query_type == "arn":
|
|
201
|
+
result = await self._query_arn_table(fetcher, args)
|
|
202
|
+
elif args.query_type == "condition":
|
|
203
|
+
result = await self._query_condition_table(fetcher, args)
|
|
204
|
+
else:
|
|
205
|
+
logger.error(f"Unknown query type: {args.query_type}")
|
|
206
|
+
return 1
|
|
207
|
+
|
|
208
|
+
# Output result
|
|
209
|
+
self._print_result(result, args.output)
|
|
210
|
+
return 0
|
|
211
|
+
|
|
212
|
+
except ValueError as e:
|
|
213
|
+
logger.error(f"Query failed: {e}")
|
|
214
|
+
return 1
|
|
215
|
+
except Exception as e: # pylint: disable=broad-exception-caught
|
|
216
|
+
logger.error(f"Unexpected error during query: {e}", exc_info=True)
|
|
217
|
+
return 1
|
|
218
|
+
|
|
219
|
+
def _get_access_level(self, action_detail: Any) -> str:
|
|
220
|
+
"""Derive access level from action annotations.
|
|
221
|
+
|
|
222
|
+
AWS API provides Properties dict with boolean flags instead of AccessLevel string.
|
|
223
|
+
We derive the access level from these flags.
|
|
224
|
+
"""
|
|
225
|
+
if not action_detail.annotations:
|
|
226
|
+
return "Unknown"
|
|
227
|
+
|
|
228
|
+
props = action_detail.annotations.get("Properties", {})
|
|
229
|
+
if not props:
|
|
230
|
+
return "Unknown"
|
|
231
|
+
|
|
232
|
+
# Check flags in priority order
|
|
233
|
+
if props.get("IsPermissionManagement"):
|
|
234
|
+
return "permissions-management"
|
|
235
|
+
if props.get("IsTaggingOnly"):
|
|
236
|
+
return "tagging"
|
|
237
|
+
if props.get("IsWrite"):
|
|
238
|
+
return "write"
|
|
239
|
+
if props.get("IsList"):
|
|
240
|
+
return "list"
|
|
241
|
+
|
|
242
|
+
# Default to read if none of the above
|
|
243
|
+
return "read"
|
|
244
|
+
|
|
245
|
+
async def _query_action_table(
|
|
246
|
+
self, fetcher: AWSServiceFetcher, args: argparse.Namespace
|
|
247
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
248
|
+
"""Query action table."""
|
|
249
|
+
service_detail = await fetcher.fetch_service_by_name(args.service)
|
|
250
|
+
|
|
251
|
+
# If specific action requested, return its details
|
|
252
|
+
if args.name:
|
|
253
|
+
action_name = args.name
|
|
254
|
+
# Try case-insensitive lookup
|
|
255
|
+
action_detail = None
|
|
256
|
+
for key, detail in service_detail.actions.items():
|
|
257
|
+
if key.lower() == action_name.lower():
|
|
258
|
+
action_detail = detail
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
if not action_detail:
|
|
262
|
+
raise ValueError(f"Action '{args.name}' not found in service '{args.service}'")
|
|
263
|
+
|
|
264
|
+
access_level = self._get_access_level(action_detail)
|
|
265
|
+
description = (
|
|
266
|
+
action_detail.annotations.get("Description", "N/A")
|
|
267
|
+
if action_detail.annotations
|
|
268
|
+
else "N/A"
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
"service": args.service,
|
|
273
|
+
"action": action_detail.name,
|
|
274
|
+
"description": description,
|
|
275
|
+
"access_level": access_level,
|
|
276
|
+
"resource_types": [r.get("Name", "*") for r in (action_detail.resources or [])],
|
|
277
|
+
"condition_keys": action_detail.action_condition_keys or [],
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# Filter actions based on criteria
|
|
281
|
+
filtered_actions = []
|
|
282
|
+
for action_name, action_detail in service_detail.actions.items():
|
|
283
|
+
access_level = self._get_access_level(action_detail)
|
|
284
|
+
|
|
285
|
+
# Apply filters
|
|
286
|
+
if args.access_level:
|
|
287
|
+
if access_level.lower() != args.access_level.lower():
|
|
288
|
+
continue
|
|
289
|
+
|
|
290
|
+
if args.resource_type:
|
|
291
|
+
resources = action_detail.resources or []
|
|
292
|
+
|
|
293
|
+
# If filtering for wildcard-only actions (actions with no required resources)
|
|
294
|
+
if args.resource_type == "*":
|
|
295
|
+
# Actions with empty resources list are wildcard-only
|
|
296
|
+
if resources:
|
|
297
|
+
continue
|
|
298
|
+
else:
|
|
299
|
+
# Filter by specific resource type name
|
|
300
|
+
resource_names = [r.get("Name", "") for r in resources]
|
|
301
|
+
if args.resource_type not in resource_names:
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
if args.condition:
|
|
305
|
+
condition_keys = action_detail.action_condition_keys or []
|
|
306
|
+
if args.condition not in condition_keys:
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
description = (
|
|
310
|
+
action_detail.annotations.get("Description", "N/A")
|
|
311
|
+
if action_detail.annotations
|
|
312
|
+
else "N/A"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Add to filtered list
|
|
316
|
+
filtered_actions.append(
|
|
317
|
+
{
|
|
318
|
+
"action": f"{args.service}:{action_name}",
|
|
319
|
+
"access_level": access_level,
|
|
320
|
+
"description": description,
|
|
321
|
+
}
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return filtered_actions
|
|
325
|
+
|
|
326
|
+
async def _query_arn_table(
|
|
327
|
+
self, fetcher: AWSServiceFetcher, args: argparse.Namespace
|
|
328
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
329
|
+
"""Query ARN table."""
|
|
330
|
+
service_detail = await fetcher.fetch_service_by_name(args.service)
|
|
331
|
+
|
|
332
|
+
# If specific ARN type requested
|
|
333
|
+
if args.name:
|
|
334
|
+
resource_type = None
|
|
335
|
+
for key, rt in service_detail.resources.items():
|
|
336
|
+
if key.lower() == args.name.lower():
|
|
337
|
+
resource_type = rt
|
|
338
|
+
break
|
|
339
|
+
|
|
340
|
+
if not resource_type:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"ARN resource type '{args.name}' not found in service '{args.service}'"
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
"service": args.service,
|
|
347
|
+
"resource_type": resource_type.name,
|
|
348
|
+
"arn_formats": resource_type.arn_formats or [],
|
|
349
|
+
"condition_keys": resource_type.condition_keys or [],
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
# List all ARN types
|
|
353
|
+
if args.list_arn_types:
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
"resource_type": rt.name,
|
|
357
|
+
"arn_formats": rt.arn_formats or [],
|
|
358
|
+
}
|
|
359
|
+
for rt in service_detail.resources.values()
|
|
360
|
+
]
|
|
361
|
+
|
|
362
|
+
# Return all raw ARN formats
|
|
363
|
+
all_arns = []
|
|
364
|
+
for resource_type in service_detail.resources.values():
|
|
365
|
+
if resource_type.arn_formats:
|
|
366
|
+
all_arns.extend(resource_type.arn_formats)
|
|
367
|
+
|
|
368
|
+
return list(set(all_arns)) # Remove duplicates
|
|
369
|
+
|
|
370
|
+
async def _query_condition_table(
|
|
371
|
+
self, fetcher: AWSServiceFetcher, args: argparse.Namespace
|
|
372
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
373
|
+
"""Query condition table."""
|
|
374
|
+
service_detail = await fetcher.fetch_service_by_name(args.service)
|
|
375
|
+
|
|
376
|
+
# If specific condition key requested
|
|
377
|
+
if args.name:
|
|
378
|
+
condition_key = None
|
|
379
|
+
for key, ck in service_detail.condition_keys.items():
|
|
380
|
+
if key.lower() == args.name.lower():
|
|
381
|
+
condition_key = ck
|
|
382
|
+
break
|
|
383
|
+
|
|
384
|
+
if not condition_key:
|
|
385
|
+
raise ValueError(
|
|
386
|
+
f"Condition key '{args.name}' not found in service '{args.service}'"
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
"service": args.service,
|
|
391
|
+
"condition_key": condition_key.name,
|
|
392
|
+
"description": condition_key.description or "N/A",
|
|
393
|
+
"types": condition_key.types or [],
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
# Return all condition keys
|
|
397
|
+
return [
|
|
398
|
+
{
|
|
399
|
+
"condition_key": ck.name,
|
|
400
|
+
"description": ck.description or "N/A",
|
|
401
|
+
"types": ck.types or [],
|
|
402
|
+
}
|
|
403
|
+
for ck in service_detail.condition_keys.values()
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
def _print_result(self, result: Any, fmt: str) -> None:
|
|
407
|
+
"""Print query result in specified format."""
|
|
408
|
+
if fmt == "yaml":
|
|
409
|
+
print(yaml.dump(result, default_flow_style=False, sort_keys=False))
|
|
410
|
+
elif fmt == "text":
|
|
411
|
+
self._print_text_format(result)
|
|
412
|
+
else: # json
|
|
413
|
+
print(json.dumps(result, indent=2))
|
|
414
|
+
|
|
415
|
+
def _print_text_format(self, result: Any) -> None:
|
|
416
|
+
"""Print result in simple text format.
|
|
417
|
+
|
|
418
|
+
Text format outputs only the essential information:
|
|
419
|
+
- For lists of actions: one action per line (service:action format)
|
|
420
|
+
- For specific action: action name followed by key details
|
|
421
|
+
- For ARNs: one ARN format per line
|
|
422
|
+
- For condition keys: one condition key per line
|
|
423
|
+
"""
|
|
424
|
+
if isinstance(result, list):
|
|
425
|
+
# List of items (actions, ARNs, or condition keys)
|
|
426
|
+
if not result:
|
|
427
|
+
return
|
|
428
|
+
|
|
429
|
+
first_item = result[0]
|
|
430
|
+
if "action" in first_item:
|
|
431
|
+
# Action list
|
|
432
|
+
for item in result:
|
|
433
|
+
print(item["action"])
|
|
434
|
+
elif "condition_key" in first_item:
|
|
435
|
+
# Condition key list
|
|
436
|
+
for item in result:
|
|
437
|
+
print(item["condition_key"])
|
|
438
|
+
elif "resource_type" in first_item:
|
|
439
|
+
# ARN type list
|
|
440
|
+
for item in result:
|
|
441
|
+
print(f"{item['resource_type']}: {', '.join(item['arn_formats'])}")
|
|
442
|
+
else:
|
|
443
|
+
# Generic list (e.g., plain ARN formats)
|
|
444
|
+
for item in result:
|
|
445
|
+
print(item)
|
|
446
|
+
|
|
447
|
+
elif isinstance(result, dict):
|
|
448
|
+
# Single item details
|
|
449
|
+
if "action" in result:
|
|
450
|
+
# Action details
|
|
451
|
+
print(result["action"])
|
|
452
|
+
if result.get("resource_types"):
|
|
453
|
+
print(f" Resource types: {', '.join(result['resource_types'])}")
|
|
454
|
+
if result.get("condition_keys"):
|
|
455
|
+
print(f" Condition keys: {', '.join(result['condition_keys'])}")
|
|
456
|
+
if result.get("access_level"):
|
|
457
|
+
print(f" Access level: {result['access_level']}")
|
|
458
|
+
|
|
459
|
+
elif "resource_type" in result:
|
|
460
|
+
# ARN details
|
|
461
|
+
print(result["resource_type"])
|
|
462
|
+
if result.get("arn_formats"):
|
|
463
|
+
for arn in result["arn_formats"]:
|
|
464
|
+
print(f" {arn}")
|
|
465
|
+
if result.get("condition_keys"):
|
|
466
|
+
print(f" Condition keys: {', '.join(result['condition_keys'])}")
|
|
467
|
+
|
|
468
|
+
elif "condition_key" in result:
|
|
469
|
+
# Condition key details
|
|
470
|
+
print(result["condition_key"])
|
|
471
|
+
if result.get("types"):
|
|
472
|
+
print(f" Types: {', '.join(result['types'])}")
|
|
473
|
+
if result.get("description") and result["description"] != "N/A":
|
|
474
|
+
print(f" Description: {result['description']}")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
# For testing
|
|
478
|
+
if __name__ == "__main__":
|
|
479
|
+
import asyncio
|
|
480
|
+
|
|
481
|
+
cmd = QueryCommand()
|
|
482
|
+
arg_parser = argparse.ArgumentParser()
|
|
483
|
+
cmd.add_arguments(arg_parser)
|
|
484
|
+
parsed_args = arg_parser.parse_args()
|
|
485
|
+
sys.exit(asyncio.run(cmd.execute(parsed_args)))
|
|
@@ -187,15 +187,15 @@ Examples:
|
|
|
187
187
|
)
|
|
188
188
|
|
|
189
189
|
parser.add_argument(
|
|
190
|
-
"--
|
|
190
|
+
"--summary",
|
|
191
191
|
action="store_true",
|
|
192
|
-
help="
|
|
192
|
+
help="Show Executive Summary section in enhanced format output",
|
|
193
193
|
)
|
|
194
194
|
|
|
195
195
|
parser.add_argument(
|
|
196
|
-
"--
|
|
196
|
+
"--severity-breakdown",
|
|
197
197
|
action="store_true",
|
|
198
|
-
help="
|
|
198
|
+
help="Show Issue Severity Breakdown section in enhanced format output",
|
|
199
199
|
)
|
|
200
200
|
|
|
201
201
|
async def execute(self, args: argparse.Namespace) -> int:
|
|
@@ -285,9 +285,9 @@ Examples:
|
|
|
285
285
|
# Pass options for enhanced format
|
|
286
286
|
format_options = {}
|
|
287
287
|
if args.format == "enhanced":
|
|
288
|
-
format_options["show_summary"] =
|
|
289
|
-
format_options["show_severity_breakdown"] =
|
|
290
|
-
args, "
|
|
288
|
+
format_options["show_summary"] = getattr(args, "summary", False)
|
|
289
|
+
format_options["show_severity_breakdown"] = getattr(
|
|
290
|
+
args, "severity_breakdown", False
|
|
291
291
|
)
|
|
292
292
|
output_content = generator.format_report(report, args.format, **format_options)
|
|
293
293
|
if args.output:
|
|
@@ -351,7 +351,7 @@ Examples:
|
|
|
351
351
|
|
|
352
352
|
# Clean up old review comments at the start (before posting any new ones)
|
|
353
353
|
if getattr(args, "github_review", False):
|
|
354
|
-
await self._cleanup_old_comments()
|
|
354
|
+
await self._cleanup_old_comments(args)
|
|
355
355
|
|
|
356
356
|
logging.info(f"Starting streaming validation from {len(args.paths)} path(s)")
|
|
357
357
|
|
|
@@ -414,9 +414,9 @@ Examples:
|
|
|
414
414
|
# Pass options for enhanced format
|
|
415
415
|
format_options = {}
|
|
416
416
|
if args.format == "enhanced":
|
|
417
|
-
format_options["show_summary"] =
|
|
418
|
-
format_options["show_severity_breakdown"] =
|
|
419
|
-
args, "
|
|
417
|
+
format_options["show_summary"] = getattr(args, "summary", False)
|
|
418
|
+
format_options["show_severity_breakdown"] = getattr(
|
|
419
|
+
args, "severity_breakdown", False
|
|
420
420
|
)
|
|
421
421
|
output_content = generator.format_report(report, args.format, **format_options)
|
|
422
422
|
if args.output:
|
|
@@ -460,24 +460,19 @@ Examples:
|
|
|
460
460
|
else:
|
|
461
461
|
return 0 if report.invalid_policies == 0 else 1
|
|
462
462
|
|
|
463
|
-
async def _cleanup_old_comments(self) -> None:
|
|
463
|
+
async def _cleanup_old_comments(self, args: argparse.Namespace) -> None:
|
|
464
464
|
"""Clean up old bot review comments from previous validation runs.
|
|
465
465
|
|
|
466
|
-
This
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
from iam_validator.core.pr_commenter import PRCommenter
|
|
470
|
-
|
|
471
|
-
async with GitHubIntegration() as github:
|
|
472
|
-
if not github.is_configured():
|
|
473
|
-
return
|
|
466
|
+
Note: This method is kept for backward compatibility but cleanup is now handled
|
|
467
|
+
automatically by update_or_create_review_comments(). It will update existing
|
|
468
|
+
comments, create new ones, and delete resolved ones smartly.
|
|
474
469
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
470
|
+
Args:
|
|
471
|
+
args: Command-line arguments (kept for compatibility)
|
|
472
|
+
"""
|
|
473
|
+
# Cleanup is now handled automatically by update_or_create_review_comments()
|
|
474
|
+
# No action needed here
|
|
475
|
+
logging.debug("Comment cleanup will be handled automatically during review posting")
|
|
481
476
|
|
|
482
477
|
async def _post_file_review(self, result, args: argparse.Namespace) -> None:
|
|
483
478
|
"""Post review comments for a single file immediately.
|
|
@@ -37,6 +37,26 @@ DEFAULT_CATEGORY_SUGGESTIONS: Final[dict[str, dict[str, Any]]] = {
|
|
|
37
37
|
' "Bool": {"aws:MultiFactorAuthPresent": "true"}\n'
|
|
38
38
|
"}"
|
|
39
39
|
),
|
|
40
|
+
"action_overrides": {
|
|
41
|
+
"iam:CreateAccessKey": {
|
|
42
|
+
"suggestion": (
|
|
43
|
+
"This action creates long-term credentials that can be compromised. Restrict creation to authorized roles:\n"
|
|
44
|
+
"• Require MFA (`aws:MultiFactorAuthPresent` = `true`) - CRITICAL\n"
|
|
45
|
+
"• Limit to specific principal tags (`aws:PrincipalTag/role` = `security-admin`)\n"
|
|
46
|
+
"• Restrict to corporate networks (`aws:SourceIp`)\n"
|
|
47
|
+
"• Consider requiring approval tags (`aws:RequestTag/approved-by`)"
|
|
48
|
+
),
|
|
49
|
+
"example": (
|
|
50
|
+
'"Condition": {\n'
|
|
51
|
+
' "StringEquals": {\n'
|
|
52
|
+
' "aws:PrincipalTag/role": "security-admin"\n'
|
|
53
|
+
" },\n"
|
|
54
|
+
' "Bool": {"aws:MultiFactorAuthPresent": "true"},\n'
|
|
55
|
+
' "IpAddress": {"aws:SourceIp": ["10.0.0.0/8"]}\n'
|
|
56
|
+
"}"
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
40
60
|
},
|
|
41
61
|
"data_access": {
|
|
42
62
|
"suggestion": (
|
|
@@ -90,6 +110,63 @@ DEFAULT_CATEGORY_SUGGESTIONS: Final[dict[str, dict[str, Any]]] = {
|
|
|
90
110
|
" }\n"
|
|
91
111
|
"}"
|
|
92
112
|
),
|
|
113
|
+
"action_overrides": {
|
|
114
|
+
"s3:DeleteObject": {
|
|
115
|
+
"suggestion": (
|
|
116
|
+
"This action permanently deletes S3 objects. Apply strict controls to prevent data loss:\n"
|
|
117
|
+
"• Restrict to organization buckets (`aws:ResourceOrgID` = `${aws:PrincipalOrgID}`)\n"
|
|
118
|
+
"• Ensure account ownership (`aws:ResourceAccount` = `${aws:PrincipalAccount}`)\n"
|
|
119
|
+
"• Require MFA for additional protection (`aws:MultiFactorAuthPresent` = `true`)\n"
|
|
120
|
+
"• Consider restricting to specific environments (`aws:ResourceTag/environment` != `production`)"
|
|
121
|
+
),
|
|
122
|
+
"example": (
|
|
123
|
+
'"Condition": {\n'
|
|
124
|
+
' "StringEquals": {\n'
|
|
125
|
+
' "aws:ResourceOrgID": "${aws:PrincipalOrgID}",\n'
|
|
126
|
+
' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
|
|
127
|
+
" },\n"
|
|
128
|
+
' "Bool": {"aws:MultiFactorAuthPresent": "true"}\n'
|
|
129
|
+
"}"
|
|
130
|
+
),
|
|
131
|
+
},
|
|
132
|
+
"s3:PutBucketPolicy": {
|
|
133
|
+
"suggestion": (
|
|
134
|
+
"This action modifies S3 bucket policies, which can expose data to unauthorized parties. Strictly control policy changes:\n"
|
|
135
|
+
"• Require organization ownership (`aws:ResourceOrgID` = `${aws:PrincipalOrgID}`)\n"
|
|
136
|
+
"• Ensure account ownership (`aws:ResourceAccount` = `${aws:PrincipalAccount}`)\n"
|
|
137
|
+
"• Require MFA (`aws:MultiFactorAuthPresent` = `true`) - CRITICAL\n"
|
|
138
|
+
"• Restrict to administrative roles (`aws:PrincipalTag/role` = `security-admin`)"
|
|
139
|
+
),
|
|
140
|
+
"example": (
|
|
141
|
+
'"Condition": {\n'
|
|
142
|
+
' "StringEquals": {\n'
|
|
143
|
+
' "aws:ResourceOrgID": "${aws:PrincipalOrgID}",\n'
|
|
144
|
+
' "aws:PrincipalTag/role": "security-admin"\n'
|
|
145
|
+
" },\n"
|
|
146
|
+
' "Bool": {"aws:MultiFactorAuthPresent": "true"}\n'
|
|
147
|
+
"}"
|
|
148
|
+
),
|
|
149
|
+
},
|
|
150
|
+
"ec2:RunInstances": {
|
|
151
|
+
"suggestion": (
|
|
152
|
+
"This action launches EC2 instances, which can incur costs and create security risks. Control instance creation:\n"
|
|
153
|
+
"• Require resource tagging (`aws:RequestTag/owner`, `aws:RequestTag/environment`)\n"
|
|
154
|
+
"• Restrict instance types (`ec2:InstanceType`)\n"
|
|
155
|
+
"• Limit to specific VPCs (`ec2:Vpc`)\n"
|
|
156
|
+
"• Enforce encryption (`ec2:Encrypted` = `true`)\n"
|
|
157
|
+
"• Match principal tags to resource tags for ownership tracking"
|
|
158
|
+
),
|
|
159
|
+
"example": (
|
|
160
|
+
'"Condition": {\n'
|
|
161
|
+
' "StringEquals": {\n'
|
|
162
|
+
' "aws:RequestTag/owner": "${aws:PrincipalTag/owner}",\n'
|
|
163
|
+
' "ec2:Vpc": "vpc-xxxxxxxxx"\n'
|
|
164
|
+
" },\n"
|
|
165
|
+
' "Bool": {"ec2:Encrypted": "true"}\n'
|
|
166
|
+
"}"
|
|
167
|
+
),
|
|
168
|
+
},
|
|
169
|
+
},
|
|
93
170
|
},
|
|
94
171
|
}
|
|
95
172
|
|