iam-policy-validator 1.7.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.

Potentially problematic release.


This version of iam-policy-validator might be problematic. Click here for more details.

Files changed (83) hide show
  1. iam_policy_validator-1.7.0.dist-info/METADATA +1057 -0
  2. iam_policy_validator-1.7.0.dist-info/RECORD +83 -0
  3. iam_policy_validator-1.7.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.7.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.7.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +7 -0
  9. iam_validator/checks/__init__.py +43 -0
  10. iam_validator/checks/action_condition_enforcement.py +884 -0
  11. iam_validator/checks/action_resource_matching.py +441 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +92 -0
  14. iam_validator/checks/condition_type_mismatch.py +259 -0
  15. iam_validator/checks/full_wildcard.py +71 -0
  16. iam_validator/checks/mfa_condition_check.py +112 -0
  17. iam_validator/checks/policy_size.py +147 -0
  18. iam_validator/checks/policy_type_validation.py +305 -0
  19. iam_validator/checks/principal_validation.py +776 -0
  20. iam_validator/checks/resource_validation.py +138 -0
  21. iam_validator/checks/sensitive_action.py +254 -0
  22. iam_validator/checks/service_wildcard.py +107 -0
  23. iam_validator/checks/set_operator_validation.py +157 -0
  24. iam_validator/checks/sid_uniqueness.py +170 -0
  25. iam_validator/checks/utils/__init__.py +1 -0
  26. iam_validator/checks/utils/policy_level_checks.py +143 -0
  27. iam_validator/checks/utils/sensitive_action_matcher.py +294 -0
  28. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  29. iam_validator/checks/wildcard_action.py +67 -0
  30. iam_validator/checks/wildcard_resource.py +135 -0
  31. iam_validator/commands/__init__.py +25 -0
  32. iam_validator/commands/analyze.py +531 -0
  33. iam_validator/commands/base.py +48 -0
  34. iam_validator/commands/cache.py +392 -0
  35. iam_validator/commands/download_services.py +255 -0
  36. iam_validator/commands/post_to_pr.py +86 -0
  37. iam_validator/commands/validate.py +600 -0
  38. iam_validator/core/__init__.py +14 -0
  39. iam_validator/core/access_analyzer.py +671 -0
  40. iam_validator/core/access_analyzer_report.py +640 -0
  41. iam_validator/core/aws_fetcher.py +940 -0
  42. iam_validator/core/check_registry.py +607 -0
  43. iam_validator/core/cli.py +134 -0
  44. iam_validator/core/condition_validators.py +626 -0
  45. iam_validator/core/config/__init__.py +81 -0
  46. iam_validator/core/config/aws_api.py +35 -0
  47. iam_validator/core/config/aws_global_conditions.py +160 -0
  48. iam_validator/core/config/category_suggestions.py +104 -0
  49. iam_validator/core/config/condition_requirements.py +155 -0
  50. iam_validator/core/config/config_loader.py +472 -0
  51. iam_validator/core/config/defaults.py +523 -0
  52. iam_validator/core/config/principal_requirements.py +421 -0
  53. iam_validator/core/config/sensitive_actions.py +672 -0
  54. iam_validator/core/config/service_principals.py +95 -0
  55. iam_validator/core/config/wildcards.py +124 -0
  56. iam_validator/core/constants.py +74 -0
  57. iam_validator/core/formatters/__init__.py +27 -0
  58. iam_validator/core/formatters/base.py +147 -0
  59. iam_validator/core/formatters/console.py +59 -0
  60. iam_validator/core/formatters/csv.py +170 -0
  61. iam_validator/core/formatters/enhanced.py +440 -0
  62. iam_validator/core/formatters/html.py +672 -0
  63. iam_validator/core/formatters/json.py +33 -0
  64. iam_validator/core/formatters/markdown.py +63 -0
  65. iam_validator/core/formatters/sarif.py +251 -0
  66. iam_validator/core/models.py +327 -0
  67. iam_validator/core/policy_checks.py +656 -0
  68. iam_validator/core/policy_loader.py +396 -0
  69. iam_validator/core/pr_commenter.py +424 -0
  70. iam_validator/core/report.py +872 -0
  71. iam_validator/integrations/__init__.py +28 -0
  72. iam_validator/integrations/github_integration.py +815 -0
  73. iam_validator/integrations/ms_teams.py +442 -0
  74. iam_validator/sdk/__init__.py +187 -0
  75. iam_validator/sdk/arn_matching.py +382 -0
  76. iam_validator/sdk/context.py +222 -0
  77. iam_validator/sdk/exceptions.py +48 -0
  78. iam_validator/sdk/helpers.py +177 -0
  79. iam_validator/sdk/policy_utils.py +425 -0
  80. iam_validator/sdk/shortcuts.py +283 -0
  81. iam_validator/utils/__init__.py +31 -0
  82. iam_validator/utils/cache.py +105 -0
  83. iam_validator/utils/regex.py +206 -0
@@ -0,0 +1,177 @@
1
+ """
2
+ Helper utilities for custom check development.
3
+
4
+ This module provides high-level helper classes and functions that make it
5
+ easy to develop custom IAM policy checks.
6
+ """
7
+
8
+ from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
9
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
10
+ from iam_validator.core.models import ValidationIssue
11
+ from iam_validator.sdk.arn_matching import arn_matches, arn_strictly_valid
12
+
13
+
14
+ class CheckHelper:
15
+ """
16
+ All-in-one helper class for custom check development.
17
+
18
+ This class provides convenient methods for common check operations like
19
+ ARN matching, action expansion, and issue creation.
20
+
21
+ Example:
22
+ >>> helper = CheckHelper(fetcher)
23
+ >>> actions = await helper.expand_actions(["s3:Get*"])
24
+ >>> if helper.arn_matches("arn:*:s3:::secret-*", resource):
25
+ ... issue = helper.create_issue(
26
+ ... severity="high",
27
+ ... statement_idx=0,
28
+ ... message="Sensitive bucket access"
29
+ ... )
30
+ """
31
+
32
+ def __init__(self, fetcher: AWSServiceFetcher):
33
+ """
34
+ Initialize helper with AWS service fetcher.
35
+
36
+ Args:
37
+ fetcher: AWS service fetcher for retrieving service definitions
38
+ """
39
+ self.fetcher = fetcher
40
+
41
+ async def expand_actions(
42
+ self,
43
+ actions: list[str],
44
+ ) -> list[str]:
45
+ """
46
+ Expand action wildcards to concrete actions.
47
+
48
+ Args:
49
+ actions: List of actions that may contain wildcards (e.g., ["s3:Get*"])
50
+
51
+ Returns:
52
+ List of expanded action strings (e.g., ["s3:GetObject", "s3:GetObjectVersion"])
53
+
54
+ Example:
55
+ >>> actions = await helper.expand_actions(["s3:Get*"])
56
+ >>> # Returns: ["s3:GetObject", "s3:GetObjectVersion", ...]
57
+ """
58
+ return await expand_wildcard_actions(actions, self.fetcher)
59
+
60
+ def arn_matches(
61
+ self,
62
+ pattern: str,
63
+ arn: str,
64
+ resource_type: str | None = None,
65
+ ) -> bool:
66
+ """
67
+ Check if ARN matches pattern with glob support.
68
+
69
+ Args:
70
+ pattern: ARN pattern (can have wildcards)
71
+ arn: ARN to check (can have wildcards)
72
+ resource_type: Optional resource type for special handling
73
+
74
+ Returns:
75
+ True if ARN matches pattern
76
+
77
+ Example:
78
+ >>> helper.arn_matches("arn:*:s3:::secret-*", "arn:aws:s3:::secret-bucket/key")
79
+ True
80
+ """
81
+ return arn_matches(pattern, arn, resource_type)
82
+
83
+ def arn_strictly_valid(
84
+ self,
85
+ pattern: str,
86
+ arn: str,
87
+ resource_type: str | None = None,
88
+ ) -> bool:
89
+ """
90
+ Strictly validate ARN against pattern.
91
+
92
+ Args:
93
+ pattern: ARN pattern from AWS service definition
94
+ arn: ARN to validate
95
+ resource_type: Optional resource type
96
+
97
+ Returns:
98
+ True if ARN strictly matches pattern
99
+ """
100
+ return arn_strictly_valid(pattern, arn, resource_type)
101
+
102
+ def create_issue(
103
+ self,
104
+ severity: str,
105
+ statement_idx: int,
106
+ message: str,
107
+ statement_sid: str | None = None,
108
+ issue_type: str = "custom",
109
+ action: str | None = None,
110
+ resource: str | None = None,
111
+ condition_key: str | None = None,
112
+ suggestion: str | None = None,
113
+ line_number: int | None = None,
114
+ ) -> ValidationIssue:
115
+ """
116
+ Create a validation issue with all necessary fields.
117
+
118
+ Args:
119
+ severity: Severity level (critical, high, medium, low, error, warning, info)
120
+ statement_idx: Index of the statement in the policy
121
+ message: Human-readable error message
122
+ statement_sid: Optional statement ID
123
+ issue_type: Type of issue (default: "custom")
124
+ action: Optional action that caused the issue
125
+ resource: Optional resource that caused the issue
126
+ condition_key: Optional condition key that caused the issue
127
+ suggestion: Optional suggestion for fixing the issue
128
+ line_number: Optional line number in source file
129
+
130
+ Returns:
131
+ ValidationIssue object
132
+ """
133
+ return ValidationIssue(
134
+ severity=severity,
135
+ statement_sid=statement_sid,
136
+ statement_index=statement_idx,
137
+ issue_type=issue_type,
138
+ message=message,
139
+ action=action,
140
+ resource=resource,
141
+ condition_key=condition_key,
142
+ suggestion=suggestion,
143
+ line_number=line_number,
144
+ )
145
+
146
+
147
+ async def expand_actions(
148
+ actions: list[str],
149
+ fetcher: AWSServiceFetcher | None = None,
150
+ ) -> list[str]:
151
+ """
152
+ Expand action wildcards to concrete actions.
153
+
154
+ This is a standalone function that can be used without CheckHelper.
155
+
156
+ Args:
157
+ actions: List of actions that may contain wildcards
158
+ fetcher: Optional AWS service fetcher (created if not provided)
159
+
160
+ Returns:
161
+ List of expanded action strings (e.g., ["s3:GetObject", "s3:GetObjectVersion"])
162
+
163
+ Example:
164
+ >>> from iam_validator.sdk import expand_actions
165
+ >>> actions = await expand_actions(["s3:Get*"])
166
+ >>> # Returns: ["s3:GetObject", "s3:GetObjectVersion", ...]
167
+
168
+ Note:
169
+ If no fetcher is provided, a temporary one will be created.
170
+ For better performance when making multiple calls, create a
171
+ fetcher once and pass it to this function or use CheckHelper.
172
+ """
173
+ if fetcher is None:
174
+ # Create temporary fetcher
175
+ fetcher = AWSServiceFetcher()
176
+
177
+ return await expand_wildcard_actions(actions, fetcher)
@@ -0,0 +1,425 @@
1
+ """
2
+ Utilities for working with IAM policies.
3
+
4
+ This module provides functions for parsing, manipulating, and inspecting
5
+ IAM policy documents programmatically.
6
+ """
7
+
8
+ import json
9
+ from typing import Any
10
+
11
+ from iam_validator.core.models import IAMPolicy, Statement
12
+
13
+
14
+ def parse_policy(policy: str | dict) -> IAMPolicy:
15
+ """
16
+ Parse a policy from JSON string or dict.
17
+
18
+ Args:
19
+ policy: IAM policy as JSON string or Python dict
20
+
21
+ Returns:
22
+ Parsed IAMPolicy object
23
+
24
+ Raises:
25
+ ValueError: If policy is invalid JSON or missing required fields
26
+
27
+ Example:
28
+ >>> policy_str = '{"Version": "2012-10-17", "Statement": [...]}'
29
+ >>> policy = parse_policy(policy_str)
30
+ >>> print(f"Version: {policy.version}")
31
+ """
32
+ if isinstance(policy, str):
33
+ try:
34
+ policy_dict = json.loads(policy)
35
+ except json.JSONDecodeError as e:
36
+ raise ValueError(f"Invalid JSON: {e}") from e
37
+ else:
38
+ policy_dict = policy
39
+
40
+ try:
41
+ return IAMPolicy(**policy_dict)
42
+ except Exception as e:
43
+ raise ValueError(f"Invalid IAM policy format: {e}") from e
44
+
45
+
46
+ def normalize_policy(policy: IAMPolicy) -> IAMPolicy:
47
+ """
48
+ Normalize policy format (ensure statements are in list format).
49
+
50
+ AWS allows Statement to be a single object or an array. This function
51
+ ensures it's always an array for consistent processing.
52
+
53
+ Args:
54
+ policy: IAMPolicy to normalize
55
+
56
+ Returns:
57
+ Normalized IAMPolicy with Statement as list
58
+
59
+ Example:
60
+ >>> policy = parse_policy(policy_json)
61
+ >>> normalized = normalize_policy(policy)
62
+ >>> assert isinstance(normalized.statement, list)
63
+ """
64
+ # Pydantic model already handles this via Field(alias="Statement")
65
+ # which expects a list, but we can ensure it's always a list
66
+ statements: list[Statement] = (
67
+ policy.statement if isinstance(policy.statement, list) else [policy.statement]
68
+ )
69
+
70
+ # Normalize actions and resources in each statement
71
+ normalized_statements: list[Statement] = []
72
+ for stmt in statements:
73
+ action = [stmt.action] if isinstance(stmt.action, str) else stmt.action
74
+ resource = [stmt.resource] if isinstance(stmt.resource, str) else stmt.resource
75
+ not_action = [stmt.not_action] if isinstance(stmt.not_action, str) else stmt.not_action
76
+ not_resource = (
77
+ [stmt.not_resource] if isinstance(stmt.not_resource, str) else stmt.not_resource
78
+ )
79
+
80
+ # Create a new statement with normalized fields
81
+ # Use capitalized field names (aliases) for Pydantic model construction
82
+ normalized_stmt = Statement(
83
+ Sid=stmt.sid,
84
+ Effect=stmt.effect,
85
+ Action=action,
86
+ NotAction=not_action,
87
+ Resource=resource,
88
+ NotResource=not_resource,
89
+ Condition=stmt.condition,
90
+ Principal=stmt.principal,
91
+ NotPrincipal=stmt.not_principal,
92
+ )
93
+ normalized_statements.append(normalized_stmt)
94
+
95
+ # Return a new policy with normalized statements
96
+ # Use capitalized field names (aliases) for Pydantic model construction
97
+ return IAMPolicy(
98
+ Version=policy.version,
99
+ Statement=normalized_statements,
100
+ Id=policy.id,
101
+ )
102
+
103
+
104
+ def extract_actions(policy: IAMPolicy) -> list[str]:
105
+ """
106
+ Extract all actions from a policy.
107
+
108
+ Args:
109
+ policy: IAMPolicy to extract actions from
110
+
111
+ Returns:
112
+ List of all unique actions in the policy
113
+
114
+ Example:
115
+ >>> policy = parse_policy(policy_json)
116
+ >>> actions = extract_actions(policy)
117
+ >>> print(f"Policy uses {len(actions)} actions")
118
+ """
119
+ actions = set()
120
+
121
+ for stmt in policy.statement:
122
+ # Handle Action field
123
+ if stmt.action:
124
+ stmt_actions = [stmt.action] if isinstance(stmt.action, str) else stmt.action
125
+ actions.update(stmt_actions)
126
+
127
+ # Handle NotAction field
128
+ if stmt.not_action:
129
+ not_actions = [stmt.not_action] if isinstance(stmt.not_action, str) else stmt.not_action
130
+ actions.update(not_actions)
131
+
132
+ return sorted(actions)
133
+
134
+
135
+ def extract_resources(policy: IAMPolicy) -> list[str]:
136
+ """
137
+ Extract all resources from a policy.
138
+
139
+ Args:
140
+ policy: IAMPolicy to extract resources from
141
+
142
+ Returns:
143
+ List of all unique resources in the policy
144
+
145
+ Example:
146
+ >>> policy = parse_policy(policy_json)
147
+ >>> resources = extract_resources(policy)
148
+ >>> for arn in resources:
149
+ ... print(f"Resource: {arn}")
150
+ """
151
+ resources = set()
152
+
153
+ for stmt in policy.statement:
154
+ # Handle Resource field
155
+ if stmt.resource:
156
+ stmt_resources = [stmt.resource] if isinstance(stmt.resource, str) else stmt.resource
157
+ resources.update(stmt_resources)
158
+
159
+ # Handle NotResource field
160
+ if stmt.not_resource:
161
+ not_resources = (
162
+ [stmt.not_resource] if isinstance(stmt.not_resource, str) else stmt.not_resource
163
+ )
164
+ resources.update(not_resources)
165
+
166
+ return sorted(resources)
167
+
168
+
169
+ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
170
+ """
171
+ Extract all condition keys used in a policy.
172
+
173
+ Args:
174
+ policy: IAMPolicy to extract condition keys from
175
+
176
+ Returns:
177
+ List of all unique condition keys in the policy
178
+
179
+ Example:
180
+ >>> policy = parse_policy(policy_json)
181
+ >>> keys = extract_condition_keys(policy)
182
+ >>> print(f"Policy uses condition keys: {', '.join(keys)}")
183
+ """
184
+ condition_keys = set()
185
+
186
+ for stmt in policy.statement:
187
+ if stmt.condition:
188
+ # Condition format: {"StringEquals": {"aws:username": "johndoe"}}
189
+ for operator, key_values in stmt.condition.items():
190
+ if isinstance(key_values, dict):
191
+ condition_keys.update(key_values.keys())
192
+
193
+ return sorted(condition_keys)
194
+
195
+
196
+ def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statement]:
197
+ """
198
+ Find all statements containing a specific action.
199
+
200
+ Supports exact match and wildcard patterns.
201
+
202
+ Args:
203
+ policy: IAMPolicy to search
204
+ action: Action to search for (e.g., "s3:GetObject" or "s3:*")
205
+
206
+ Returns:
207
+ List of Statement objects containing the action
208
+
209
+ Example:
210
+ >>> policy = parse_policy(policy_json)
211
+ >>> stmts = find_statements_with_action(policy, "s3:GetObject")
212
+ >>> for stmt in stmts:
213
+ ... print(f"Statement {stmt.sid} allows s3:GetObject")
214
+ """
215
+ import fnmatch
216
+
217
+ matching_statements = []
218
+
219
+ for stmt in policy.statement:
220
+ stmt_actions = stmt.get_actions()
221
+
222
+ # Check if action matches any statement action (with wildcard support)
223
+ for stmt_action in stmt_actions:
224
+ if fnmatch.fnmatch(action, stmt_action) or fnmatch.fnmatch(stmt_action, action):
225
+ matching_statements.append(stmt)
226
+ break
227
+
228
+ return matching_statements
229
+
230
+
231
+ def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Statement]:
232
+ """
233
+ Find all statements containing a specific resource.
234
+
235
+ Supports exact match and wildcard patterns.
236
+
237
+ Args:
238
+ policy: IAMPolicy to search
239
+ resource: Resource ARN to search for
240
+
241
+ Returns:
242
+ List of Statement objects containing the resource
243
+
244
+ Example:
245
+ >>> policy = parse_policy(policy_json)
246
+ >>> stmts = find_statements_with_resource(policy, "arn:aws:s3:::my-bucket/*")
247
+ >>> print(f"Found {len(stmts)} statements with this resource")
248
+ """
249
+ import fnmatch
250
+
251
+ matching_statements = []
252
+
253
+ for stmt in policy.statement:
254
+ stmt_resources = stmt.get_resources()
255
+
256
+ # Check if resource matches any statement resource (with wildcard support)
257
+ for stmt_resource in stmt_resources:
258
+ if fnmatch.fnmatch(resource, stmt_resource) or fnmatch.fnmatch(stmt_resource, resource):
259
+ matching_statements.append(stmt)
260
+ break
261
+
262
+ return matching_statements
263
+
264
+
265
+ def merge_policies(*policies: IAMPolicy) -> IAMPolicy:
266
+ """
267
+ Merge multiple policies into one.
268
+
269
+ Combines all statements from multiple policies into a single policy document.
270
+ Uses the version from the first policy.
271
+
272
+ Args:
273
+ *policies: IAMPolicy objects to merge
274
+
275
+ Returns:
276
+ New IAMPolicy with all statements combined
277
+
278
+ Example:
279
+ >>> policy1 = parse_policy(json1)
280
+ >>> policy2 = parse_policy(json2)
281
+ >>> merged = merge_policies(policy1, policy2)
282
+ >>> print(f"Merged policy has {len(merged.statement)} statements")
283
+ """
284
+ if not policies:
285
+ raise ValueError("At least one policy must be provided")
286
+
287
+ all_statements: list[Statement] = []
288
+ for policy in policies:
289
+ all_statements.extend(policy.statement)
290
+
291
+ # Use capitalized field names (aliases) for Pydantic model construction
292
+ return IAMPolicy(
293
+ Version=policies[0].version,
294
+ Statement=all_statements,
295
+ Id=None, # Clear ID when merging
296
+ )
297
+
298
+
299
+ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
300
+ """
301
+ Get a summary of policy contents.
302
+
303
+ Args:
304
+ policy: IAMPolicy to summarize
305
+
306
+ Returns:
307
+ Dictionary with summary statistics
308
+
309
+ Example:
310
+ >>> policy = parse_policy(policy_json)
311
+ >>> summary = get_policy_summary(policy)
312
+ >>> print(f"Statements: {summary['statement_count']}")
313
+ >>> print(f"Actions: {summary['action_count']}")
314
+ >>> print(f"Resources: {summary['resource_count']}")
315
+ """
316
+ actions = extract_actions(policy)
317
+ resources = extract_resources(policy)
318
+ condition_keys = extract_condition_keys(policy)
319
+
320
+ # Count allow vs deny statements
321
+ allow_count = sum(1 for s in policy.statement if s.effect.lower() == "allow")
322
+ deny_count = sum(1 for s in policy.statement if s.effect.lower() == "deny")
323
+
324
+ # Check for wildcards
325
+ has_wildcard_actions = any("*" in action for action in actions)
326
+ has_wildcard_resources = any("*" in resource for resource in resources)
327
+
328
+ return {
329
+ "version": policy.version,
330
+ "statement_count": len(policy.statement),
331
+ "allow_statements": allow_count,
332
+ "deny_statements": deny_count,
333
+ "action_count": len(actions),
334
+ "resource_count": len(resources),
335
+ "condition_key_count": len(condition_keys),
336
+ "has_wildcard_actions": has_wildcard_actions,
337
+ "has_wildcard_resources": has_wildcard_resources,
338
+ "actions": actions,
339
+ "resources": resources,
340
+ "condition_keys": condition_keys,
341
+ }
342
+
343
+
344
+ def policy_to_json(policy: IAMPolicy, indent: int = 2) -> str:
345
+ """
346
+ Convert IAMPolicy to formatted JSON string.
347
+
348
+ Args:
349
+ policy: IAMPolicy to convert
350
+ indent: Number of spaces for indentation (default: 2)
351
+
352
+ Returns:
353
+ Formatted JSON string
354
+
355
+ Example:
356
+ >>> policy = parse_policy(policy_dict)
357
+ >>> json_str = policy_to_json(policy)
358
+ >>> print(json_str)
359
+ """
360
+ policy_dict = policy.model_dump(by_alias=True, exclude_none=True)
361
+ return json.dumps(policy_dict, indent=indent)
362
+
363
+
364
+ def policy_to_dict(policy: IAMPolicy) -> dict[str, Any]:
365
+ """
366
+ Convert IAMPolicy to Python dictionary.
367
+
368
+ Args:
369
+ policy: IAMPolicy to convert
370
+
371
+ Returns:
372
+ Policy as Python dict with AWS field names (Version, Statement, etc.)
373
+
374
+ Example:
375
+ >>> policy = parse_policy(policy_json)
376
+ >>> policy_dict = policy_to_dict(policy)
377
+ >>> print(policy_dict["Version"])
378
+ """
379
+ return policy.model_dump(by_alias=True, exclude_none=True)
380
+
381
+
382
+ def is_resource_policy(policy: IAMPolicy) -> bool:
383
+ """
384
+ Check if policy appears to be a resource policy (vs identity policy).
385
+
386
+ Resource policies have a Principal field, identity policies don't.
387
+
388
+ Args:
389
+ policy: IAMPolicy to check
390
+
391
+ Returns:
392
+ True if policy appears to be a resource policy
393
+
394
+ Example:
395
+ >>> policy = parse_policy(bucket_policy_json)
396
+ >>> if is_resource_policy(policy):
397
+ ... print("This is an S3 bucket policy or similar")
398
+ """
399
+ return any(stmt.principal is not None for stmt in policy.statement)
400
+
401
+
402
+ def has_public_access(policy: IAMPolicy) -> bool:
403
+ """
404
+ Check if policy grants public access (Principal: "*").
405
+
406
+ Args:
407
+ policy: IAMPolicy to check
408
+
409
+ Returns:
410
+ True if any statement has Principal set to "*"
411
+
412
+ Example:
413
+ >>> policy = parse_policy(policy_json)
414
+ >>> if has_public_access(policy):
415
+ ... print("WARNING: This policy allows public access!")
416
+ """
417
+ for stmt in policy.statement:
418
+ if stmt.principal == "*":
419
+ return True
420
+ if isinstance(stmt.principal, dict):
421
+ # Check for {"AWS": "*"} or {"Service": "*"}
422
+ for value in stmt.principal.values():
423
+ if value == "*" or (isinstance(value, list) and "*" in value):
424
+ return True
425
+ return False