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,454 @@
|
|
|
1
|
+
"""Query utilities for AWS service definitions.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for querying AWS IAM service metadata including
|
|
4
|
+
actions, ARN formats, and condition keys. These utilities are inspired by and
|
|
5
|
+
compatible with policy_sentry's query functionality.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
Query actions for a service:
|
|
9
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
10
|
+
... actions = await query_actions(fetcher, "s3")
|
|
11
|
+
... write_actions = await query_actions(fetcher, "s3", access_level="write")
|
|
12
|
+
...
|
|
13
|
+
Query ARN formats:
|
|
14
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
15
|
+
... arns = await query_arn_formats(fetcher, "s3")
|
|
16
|
+
... bucket_arn = await query_arn_format(fetcher, "s3", "bucket")
|
|
17
|
+
...
|
|
18
|
+
Query condition keys:
|
|
19
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
20
|
+
... keys = await query_condition_keys(fetcher, "s3")
|
|
21
|
+
... prefix_key = await query_condition_key(fetcher, "s3", "s3:prefix")
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from typing import Any, Literal
|
|
25
|
+
|
|
26
|
+
from iam_validator.core.aws_service.fetcher import AWSServiceFetcher
|
|
27
|
+
|
|
28
|
+
AccessLevel = Literal["read", "write", "list", "tagging", "permissions-management"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _get_access_level(action_detail: Any) -> str:
|
|
32
|
+
"""Derive access level from action annotations.
|
|
33
|
+
|
|
34
|
+
AWS API provides Properties dict with boolean flags instead of AccessLevel string.
|
|
35
|
+
We derive the access level from these flags.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
action_detail: Action detail object with annotations
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Access level string: "permissions-management", "tagging", "write", "list", or "read"
|
|
42
|
+
"""
|
|
43
|
+
if not action_detail.annotations:
|
|
44
|
+
return "Unknown"
|
|
45
|
+
|
|
46
|
+
props = action_detail.annotations.get("Properties", {})
|
|
47
|
+
if not props:
|
|
48
|
+
return "Unknown"
|
|
49
|
+
|
|
50
|
+
# Check flags in priority order
|
|
51
|
+
if props.get("IsPermissionManagement"):
|
|
52
|
+
return "permissions-management"
|
|
53
|
+
if props.get("IsTaggingOnly"):
|
|
54
|
+
return "tagging"
|
|
55
|
+
if props.get("IsWrite"):
|
|
56
|
+
return "write"
|
|
57
|
+
if props.get("IsList"):
|
|
58
|
+
return "list"
|
|
59
|
+
|
|
60
|
+
# Default to read if none of the above
|
|
61
|
+
return "read"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
async def query_actions(
|
|
65
|
+
fetcher: AWSServiceFetcher,
|
|
66
|
+
service: str,
|
|
67
|
+
access_level: AccessLevel | None = None,
|
|
68
|
+
resource_type: str | None = None,
|
|
69
|
+
condition: str | None = None,
|
|
70
|
+
) -> list[dict[str, Any]]:
|
|
71
|
+
"""Query IAM actions for a service with optional filtering.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
fetcher: AWSServiceFetcher instance
|
|
75
|
+
service: AWS service prefix (e.g., "s3", "iam", "ec2")
|
|
76
|
+
access_level: Optional filter by access level
|
|
77
|
+
resource_type: Optional filter by resource type. Use "*" for wildcard-only actions
|
|
78
|
+
condition: Optional filter by condition key support
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
List of action dictionaries with keys: action, access_level, description
|
|
82
|
+
|
|
83
|
+
Example:
|
|
84
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
85
|
+
... # Get all S3 actions
|
|
86
|
+
... all_actions = await query_actions(fetcher, "s3")
|
|
87
|
+
...
|
|
88
|
+
... # Get only write-level S3 actions
|
|
89
|
+
... write_actions = await query_actions(fetcher, "s3", access_level="write")
|
|
90
|
+
...
|
|
91
|
+
... # Get wildcard-only actions (no resource constraint)
|
|
92
|
+
... wildcard_actions = await query_actions(fetcher, "iam", resource_type="*")
|
|
93
|
+
...
|
|
94
|
+
... # Get actions supporting specific condition key
|
|
95
|
+
... mfa_actions = await query_actions(fetcher, "iam", condition="aws:MultiFactorAuthPresent")
|
|
96
|
+
"""
|
|
97
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
98
|
+
|
|
99
|
+
filtered_actions = []
|
|
100
|
+
for action_name, action_detail in service_detail.actions.items():
|
|
101
|
+
access_lv = _get_access_level(action_detail)
|
|
102
|
+
|
|
103
|
+
# Apply filters
|
|
104
|
+
if access_level and access_lv.lower() != access_level.lower():
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
if resource_type:
|
|
108
|
+
resources = action_detail.resources or []
|
|
109
|
+
|
|
110
|
+
# If filtering for wildcard-only actions (actions with no required resources)
|
|
111
|
+
if resource_type == "*":
|
|
112
|
+
# Actions with empty resources list are wildcard-only
|
|
113
|
+
if resources:
|
|
114
|
+
continue
|
|
115
|
+
else:
|
|
116
|
+
# Filter by specific resource type name
|
|
117
|
+
resource_names = [r.get("Name", "") for r in resources]
|
|
118
|
+
if resource_type not in resource_names:
|
|
119
|
+
continue
|
|
120
|
+
|
|
121
|
+
if condition:
|
|
122
|
+
condition_keys = action_detail.action_condition_keys or []
|
|
123
|
+
if condition not in condition_keys:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
description = (
|
|
127
|
+
action_detail.annotations.get("Description", "N/A")
|
|
128
|
+
if action_detail.annotations
|
|
129
|
+
else "N/A"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
filtered_actions.append(
|
|
133
|
+
{
|
|
134
|
+
"action": f"{service}:{action_name}",
|
|
135
|
+
"access_level": access_lv,
|
|
136
|
+
"description": description,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
return filtered_actions
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
async def query_action_details(
|
|
144
|
+
fetcher: AWSServiceFetcher,
|
|
145
|
+
service: str,
|
|
146
|
+
action_name: str,
|
|
147
|
+
) -> dict[str, Any]:
|
|
148
|
+
"""Get detailed information about a specific action.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
fetcher: AWSServiceFetcher instance
|
|
152
|
+
service: AWS service prefix (e.g., "s3", "iam")
|
|
153
|
+
action_name: Action name (e.g., "GetObject", "CreateUser")
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
Dictionary with action details including resource types and condition keys
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If action is not found
|
|
160
|
+
|
|
161
|
+
Example:
|
|
162
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
163
|
+
... details = await query_action_details(fetcher, "s3", "GetObject")
|
|
164
|
+
... print(f"Access level: {details['access_level']}")
|
|
165
|
+
... print(f"Resource types: {details['resource_types']}")
|
|
166
|
+
"""
|
|
167
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
168
|
+
|
|
169
|
+
# Try case-insensitive lookup
|
|
170
|
+
action_detail = None
|
|
171
|
+
for key, detail in service_detail.actions.items():
|
|
172
|
+
if key.lower() == action_name.lower():
|
|
173
|
+
action_detail = detail
|
|
174
|
+
break
|
|
175
|
+
|
|
176
|
+
if not action_detail:
|
|
177
|
+
raise ValueError(f"Action '{action_name}' not found in service '{service}'")
|
|
178
|
+
|
|
179
|
+
access_level = _get_access_level(action_detail)
|
|
180
|
+
description = (
|
|
181
|
+
action_detail.annotations.get("Description", "N/A") if action_detail.annotations else "N/A"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
"service": service,
|
|
186
|
+
"action": action_detail.name,
|
|
187
|
+
"description": description,
|
|
188
|
+
"access_level": access_level,
|
|
189
|
+
"resource_types": [r.get("Name", "*") for r in (action_detail.resources or [])],
|
|
190
|
+
"condition_keys": action_detail.action_condition_keys or [],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
async def query_arn_formats(
|
|
195
|
+
fetcher: AWSServiceFetcher,
|
|
196
|
+
service: str,
|
|
197
|
+
) -> list[str]:
|
|
198
|
+
"""Query all ARN formats for a service.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
fetcher: AWSServiceFetcher instance
|
|
202
|
+
service: AWS service prefix (e.g., "s3", "iam")
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of unique ARN format strings
|
|
206
|
+
|
|
207
|
+
Example:
|
|
208
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
209
|
+
... arns = await query_arn_formats(fetcher, "s3")
|
|
210
|
+
... for arn in arns:
|
|
211
|
+
... print(arn)
|
|
212
|
+
"""
|
|
213
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
214
|
+
|
|
215
|
+
all_arns = []
|
|
216
|
+
for resource_type in service_detail.resources.values():
|
|
217
|
+
if resource_type.arn_formats:
|
|
218
|
+
all_arns.extend(resource_type.arn_formats)
|
|
219
|
+
|
|
220
|
+
return list(set(all_arns)) # Remove duplicates
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def query_arn_types(
|
|
224
|
+
fetcher: AWSServiceFetcher,
|
|
225
|
+
service: str,
|
|
226
|
+
) -> list[dict[str, Any]]:
|
|
227
|
+
"""Query all ARN resource types with their formats.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
fetcher: AWSServiceFetcher instance
|
|
231
|
+
service: AWS service prefix (e.g., "s3", "iam")
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
List of dictionaries with resource_type and arn_formats keys
|
|
235
|
+
|
|
236
|
+
Example:
|
|
237
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
238
|
+
... types = await query_arn_types(fetcher, "s3")
|
|
239
|
+
... for rt in types:
|
|
240
|
+
... print(f"{rt['resource_type']}: {rt['arn_formats']}")
|
|
241
|
+
"""
|
|
242
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
243
|
+
|
|
244
|
+
return [
|
|
245
|
+
{
|
|
246
|
+
"resource_type": rt.name,
|
|
247
|
+
"arn_formats": rt.arn_formats or [],
|
|
248
|
+
}
|
|
249
|
+
for rt in service_detail.resources.values()
|
|
250
|
+
]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
async def query_arn_format(
|
|
254
|
+
fetcher: AWSServiceFetcher,
|
|
255
|
+
service: str,
|
|
256
|
+
resource_type_name: str,
|
|
257
|
+
) -> dict[str, Any]:
|
|
258
|
+
"""Get ARN format details for a specific resource type.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
fetcher: AWSServiceFetcher instance
|
|
262
|
+
service: AWS service prefix (e.g., "s3", "iam")
|
|
263
|
+
resource_type_name: Resource type name (e.g., "bucket", "role")
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
Dictionary with resource type details including ARN formats and condition keys
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
ValueError: If resource type is not found
|
|
270
|
+
|
|
271
|
+
Example:
|
|
272
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
273
|
+
... details = await query_arn_format(fetcher, "s3", "bucket")
|
|
274
|
+
... print(f"ARN formats: {details['arn_formats']}")
|
|
275
|
+
"""
|
|
276
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
277
|
+
|
|
278
|
+
resource_type = None
|
|
279
|
+
for key, rt in service_detail.resources.items():
|
|
280
|
+
if key.lower() == resource_type_name.lower():
|
|
281
|
+
resource_type = rt
|
|
282
|
+
break
|
|
283
|
+
|
|
284
|
+
if not resource_type:
|
|
285
|
+
raise ValueError(
|
|
286
|
+
f"ARN resource type '{resource_type_name}' not found in service '{service}'"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
"service": service,
|
|
291
|
+
"resource_type": resource_type.name,
|
|
292
|
+
"arn_formats": resource_type.arn_formats or [],
|
|
293
|
+
"condition_keys": resource_type.condition_keys or [],
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def query_condition_keys(
|
|
298
|
+
fetcher: AWSServiceFetcher,
|
|
299
|
+
service: str,
|
|
300
|
+
) -> list[dict[str, Any]]:
|
|
301
|
+
"""Query all condition keys for a service.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
fetcher: AWSServiceFetcher instance
|
|
305
|
+
service: AWS service prefix (e.g., "s3", "iam")
|
|
306
|
+
|
|
307
|
+
Returns:
|
|
308
|
+
List of dictionaries with condition_key, description, and types keys
|
|
309
|
+
|
|
310
|
+
Example:
|
|
311
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
312
|
+
... keys = await query_condition_keys(fetcher, "s3")
|
|
313
|
+
... for key in keys:
|
|
314
|
+
... print(f"{key['condition_key']}: {key['description']}")
|
|
315
|
+
"""
|
|
316
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
317
|
+
|
|
318
|
+
return [
|
|
319
|
+
{
|
|
320
|
+
"condition_key": ck.name,
|
|
321
|
+
"description": ck.description or "N/A",
|
|
322
|
+
"types": ck.types or [],
|
|
323
|
+
}
|
|
324
|
+
for ck in service_detail.condition_keys.values()
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def query_condition_key(
|
|
329
|
+
fetcher: AWSServiceFetcher,
|
|
330
|
+
service: str,
|
|
331
|
+
condition_key_name: str,
|
|
332
|
+
) -> dict[str, Any]:
|
|
333
|
+
"""Get details for a specific condition key.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
fetcher: AWSServiceFetcher instance
|
|
337
|
+
service: AWS service prefix (e.g., "s3", "iam")
|
|
338
|
+
condition_key_name: Condition key name (e.g., "s3:prefix", "iam:PolicyArn")
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Dictionary with condition key details
|
|
342
|
+
|
|
343
|
+
Raises:
|
|
344
|
+
ValueError: If condition key is not found
|
|
345
|
+
|
|
346
|
+
Example:
|
|
347
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
348
|
+
... details = await query_condition_key(fetcher, "s3", "s3:prefix")
|
|
349
|
+
... print(f"Types: {details['types']}")
|
|
350
|
+
"""
|
|
351
|
+
service_detail = await fetcher.fetch_service_by_name(service)
|
|
352
|
+
|
|
353
|
+
condition_key = None
|
|
354
|
+
for key, ck in service_detail.condition_keys.items():
|
|
355
|
+
if key.lower() == condition_key_name.lower():
|
|
356
|
+
condition_key = ck
|
|
357
|
+
break
|
|
358
|
+
|
|
359
|
+
if not condition_key:
|
|
360
|
+
raise ValueError(f"Condition key '{condition_key_name}' not found in service '{service}'")
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
"service": service,
|
|
364
|
+
"condition_key": condition_key.name,
|
|
365
|
+
"description": condition_key.description or "N/A",
|
|
366
|
+
"types": condition_key.types or [],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
async def get_actions_by_access_level(
|
|
371
|
+
fetcher: AWSServiceFetcher,
|
|
372
|
+
service: str,
|
|
373
|
+
access_level: AccessLevel,
|
|
374
|
+
) -> list[str]:
|
|
375
|
+
"""Get action names filtered by access level.
|
|
376
|
+
|
|
377
|
+
Convenience function that returns just the action names (not full details).
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
fetcher: AWSServiceFetcher instance
|
|
381
|
+
service: AWS service prefix
|
|
382
|
+
access_level: Access level to filter by
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
List of action names (with service prefix)
|
|
386
|
+
|
|
387
|
+
Example:
|
|
388
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
389
|
+
... write_actions = await get_actions_by_access_level(fetcher, "s3", "write")
|
|
390
|
+
... print(f"Found {len(write_actions)} write actions")
|
|
391
|
+
"""
|
|
392
|
+
actions = await query_actions(fetcher, service, access_level=access_level)
|
|
393
|
+
return [action["action"] for action in actions]
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
async def get_wildcard_only_actions(
|
|
397
|
+
fetcher: AWSServiceFetcher,
|
|
398
|
+
service: str,
|
|
399
|
+
) -> list[str]:
|
|
400
|
+
"""Get actions that only support wildcard resources (no specific resource types).
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
fetcher: AWSServiceFetcher instance
|
|
404
|
+
service: AWS service prefix
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
List of action names that don't require specific resource ARNs
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
411
|
+
... wildcard_actions = await get_wildcard_only_actions(fetcher, "iam")
|
|
412
|
+
... print(f"IAM has {len(wildcard_actions)} wildcard-only actions")
|
|
413
|
+
"""
|
|
414
|
+
actions = await query_actions(fetcher, service, resource_type="*")
|
|
415
|
+
return [action["action"] for action in actions]
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
async def get_actions_supporting_condition(
|
|
419
|
+
fetcher: AWSServiceFetcher,
|
|
420
|
+
service: str,
|
|
421
|
+
condition_key: str,
|
|
422
|
+
) -> list[str]:
|
|
423
|
+
"""Get actions that support a specific condition key.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
fetcher: AWSServiceFetcher instance
|
|
427
|
+
service: AWS service prefix
|
|
428
|
+
condition_key: Condition key to search for
|
|
429
|
+
|
|
430
|
+
Returns:
|
|
431
|
+
List of action names that support the condition key
|
|
432
|
+
|
|
433
|
+
Example:
|
|
434
|
+
>>> async with AWSServiceFetcher() as fetcher:
|
|
435
|
+
... mfa_actions = await get_actions_supporting_condition(
|
|
436
|
+
... fetcher, "iam", "aws:MultiFactorAuthPresent"
|
|
437
|
+
... )
|
|
438
|
+
"""
|
|
439
|
+
actions = await query_actions(fetcher, service, condition=condition_key)
|
|
440
|
+
return [action["action"] for action in actions]
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
__all__ = [
|
|
444
|
+
"query_actions",
|
|
445
|
+
"query_action_details",
|
|
446
|
+
"query_arn_formats",
|
|
447
|
+
"query_arn_types",
|
|
448
|
+
"query_arn_format",
|
|
449
|
+
"query_condition_keys",
|
|
450
|
+
"query_condition_key",
|
|
451
|
+
"get_actions_by_access_level",
|
|
452
|
+
"get_wildcard_only_actions",
|
|
453
|
+
"get_actions_supporting_condition",
|
|
454
|
+
]
|