iam-policy-validator 1.14.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.
Files changed (106) hide show
  1. iam_policy_validator-1.14.0.dist-info/METADATA +782 -0
  2. iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
  3. iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.14.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 +9 -0
  9. iam_validator/checks/__init__.py +45 -0
  10. iam_validator/checks/action_condition_enforcement.py +1442 -0
  11. iam_validator/checks/action_resource_matching.py +472 -0
  12. iam_validator/checks/action_validation.py +67 -0
  13. iam_validator/checks/condition_key_validation.py +88 -0
  14. iam_validator/checks/condition_type_mismatch.py +257 -0
  15. iam_validator/checks/full_wildcard.py +62 -0
  16. iam_validator/checks/mfa_condition_check.py +105 -0
  17. iam_validator/checks/policy_size.py +114 -0
  18. iam_validator/checks/policy_structure.py +556 -0
  19. iam_validator/checks/policy_type_validation.py +331 -0
  20. iam_validator/checks/principal_validation.py +708 -0
  21. iam_validator/checks/resource_validation.py +135 -0
  22. iam_validator/checks/sensitive_action.py +438 -0
  23. iam_validator/checks/service_wildcard.py +98 -0
  24. iam_validator/checks/set_operator_validation.py +153 -0
  25. iam_validator/checks/sid_uniqueness.py +146 -0
  26. iam_validator/checks/trust_policy_validation.py +509 -0
  27. iam_validator/checks/utils/__init__.py +17 -0
  28. iam_validator/checks/utils/action_parser.py +149 -0
  29. iam_validator/checks/utils/policy_level_checks.py +190 -0
  30. iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
  31. iam_validator/checks/utils/wildcard_expansion.py +86 -0
  32. iam_validator/checks/wildcard_action.py +58 -0
  33. iam_validator/checks/wildcard_resource.py +374 -0
  34. iam_validator/commands/__init__.py +31 -0
  35. iam_validator/commands/analyze.py +549 -0
  36. iam_validator/commands/base.py +48 -0
  37. iam_validator/commands/cache.py +393 -0
  38. iam_validator/commands/completion.py +471 -0
  39. iam_validator/commands/download_services.py +255 -0
  40. iam_validator/commands/post_to_pr.py +86 -0
  41. iam_validator/commands/query.py +485 -0
  42. iam_validator/commands/validate.py +830 -0
  43. iam_validator/core/__init__.py +13 -0
  44. iam_validator/core/access_analyzer.py +671 -0
  45. iam_validator/core/access_analyzer_report.py +640 -0
  46. iam_validator/core/aws_fetcher.py +29 -0
  47. iam_validator/core/aws_service/__init__.py +21 -0
  48. iam_validator/core/aws_service/cache.py +108 -0
  49. iam_validator/core/aws_service/client.py +205 -0
  50. iam_validator/core/aws_service/fetcher.py +641 -0
  51. iam_validator/core/aws_service/parsers.py +149 -0
  52. iam_validator/core/aws_service/patterns.py +51 -0
  53. iam_validator/core/aws_service/storage.py +291 -0
  54. iam_validator/core/aws_service/validators.py +380 -0
  55. iam_validator/core/check_registry.py +679 -0
  56. iam_validator/core/cli.py +134 -0
  57. iam_validator/core/codeowners.py +245 -0
  58. iam_validator/core/condition_validators.py +626 -0
  59. iam_validator/core/config/__init__.py +81 -0
  60. iam_validator/core/config/aws_api.py +35 -0
  61. iam_validator/core/config/aws_global_conditions.py +160 -0
  62. iam_validator/core/config/category_suggestions.py +181 -0
  63. iam_validator/core/config/check_documentation.py +390 -0
  64. iam_validator/core/config/condition_requirements.py +258 -0
  65. iam_validator/core/config/config_loader.py +670 -0
  66. iam_validator/core/config/defaults.py +739 -0
  67. iam_validator/core/config/principal_requirements.py +421 -0
  68. iam_validator/core/config/sensitive_actions.py +672 -0
  69. iam_validator/core/config/service_principals.py +132 -0
  70. iam_validator/core/config/wildcards.py +127 -0
  71. iam_validator/core/constants.py +149 -0
  72. iam_validator/core/diff_parser.py +325 -0
  73. iam_validator/core/finding_fingerprint.py +131 -0
  74. iam_validator/core/formatters/__init__.py +27 -0
  75. iam_validator/core/formatters/base.py +147 -0
  76. iam_validator/core/formatters/console.py +68 -0
  77. iam_validator/core/formatters/csv.py +171 -0
  78. iam_validator/core/formatters/enhanced.py +481 -0
  79. iam_validator/core/formatters/html.py +672 -0
  80. iam_validator/core/formatters/json.py +33 -0
  81. iam_validator/core/formatters/markdown.py +64 -0
  82. iam_validator/core/formatters/sarif.py +251 -0
  83. iam_validator/core/ignore_patterns.py +297 -0
  84. iam_validator/core/ignore_processor.py +309 -0
  85. iam_validator/core/ignored_findings.py +400 -0
  86. iam_validator/core/label_manager.py +197 -0
  87. iam_validator/core/models.py +404 -0
  88. iam_validator/core/policy_checks.py +220 -0
  89. iam_validator/core/policy_loader.py +785 -0
  90. iam_validator/core/pr_commenter.py +780 -0
  91. iam_validator/core/report.py +942 -0
  92. iam_validator/integrations/__init__.py +28 -0
  93. iam_validator/integrations/github_integration.py +1821 -0
  94. iam_validator/integrations/ms_teams.py +442 -0
  95. iam_validator/sdk/__init__.py +220 -0
  96. iam_validator/sdk/arn_matching.py +382 -0
  97. iam_validator/sdk/context.py +222 -0
  98. iam_validator/sdk/exceptions.py +48 -0
  99. iam_validator/sdk/helpers.py +177 -0
  100. iam_validator/sdk/policy_utils.py +451 -0
  101. iam_validator/sdk/query_utils.py +454 -0
  102. iam_validator/sdk/shortcuts.py +283 -0
  103. iam_validator/utils/__init__.py +35 -0
  104. iam_validator/utils/cache.py +105 -0
  105. iam_validator/utils/regex.py +205 -0
  106. iam_validator/utils/terminal.py +22 -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_service 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,451 @@
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
+ if policy.statement is None:
67
+ statements: list[Statement] = []
68
+ elif isinstance(policy.statement, list):
69
+ statements = policy.statement
70
+ else:
71
+ # Single statement - wrap in list
72
+ statements = [policy.statement]
73
+
74
+ # Normalize actions and resources in each statement
75
+ normalized_statements: list[Statement] = []
76
+ for stmt in statements:
77
+ action = [stmt.action] if isinstance(stmt.action, str) else stmt.action
78
+ resource = [stmt.resource] if isinstance(stmt.resource, str) else stmt.resource
79
+ not_action = [stmt.not_action] if isinstance(stmt.not_action, str) else stmt.not_action
80
+ not_resource = (
81
+ [stmt.not_resource] if isinstance(stmt.not_resource, str) else stmt.not_resource
82
+ )
83
+
84
+ # Create a new statement with normalized fields
85
+ # Use capitalized field names (aliases) for Pydantic model construction
86
+ normalized_stmt = Statement(
87
+ Sid=stmt.sid,
88
+ Effect=stmt.effect,
89
+ Action=action,
90
+ NotAction=not_action,
91
+ Resource=resource,
92
+ NotResource=not_resource,
93
+ Condition=stmt.condition,
94
+ Principal=stmt.principal,
95
+ NotPrincipal=stmt.not_principal,
96
+ )
97
+ normalized_statements.append(normalized_stmt)
98
+
99
+ # Return a new policy with normalized statements
100
+ # Use capitalized field names (aliases) for Pydantic model construction
101
+ return IAMPolicy(
102
+ Version=policy.version,
103
+ Statement=normalized_statements,
104
+ Id=policy.id,
105
+ )
106
+
107
+
108
+ def extract_actions(policy: IAMPolicy) -> list[str]:
109
+ """
110
+ Extract all actions from a policy.
111
+
112
+ Args:
113
+ policy: IAMPolicy to extract actions from
114
+
115
+ Returns:
116
+ List of all unique actions in the policy
117
+
118
+ Example:
119
+ >>> policy = parse_policy(policy_json)
120
+ >>> actions = extract_actions(policy)
121
+ >>> print(f"Policy uses {len(actions)} actions")
122
+ """
123
+ actions = set()
124
+
125
+ if policy.statement is None:
126
+ return []
127
+
128
+ for stmt in policy.statement:
129
+ # Handle Action field
130
+ if stmt.action:
131
+ stmt_actions = [stmt.action] if isinstance(stmt.action, str) else stmt.action
132
+ actions.update(stmt_actions)
133
+
134
+ # Handle NotAction field
135
+ if stmt.not_action:
136
+ not_actions = [stmt.not_action] if isinstance(stmt.not_action, str) else stmt.not_action
137
+ actions.update(not_actions)
138
+
139
+ return sorted(actions)
140
+
141
+
142
+ def extract_resources(policy: IAMPolicy) -> list[str]:
143
+ """
144
+ Extract all resources from a policy.
145
+
146
+ Args:
147
+ policy: IAMPolicy to extract resources from
148
+
149
+ Returns:
150
+ List of all unique resources in the policy
151
+
152
+ Example:
153
+ >>> policy = parse_policy(policy_json)
154
+ >>> resources = extract_resources(policy)
155
+ >>> for arn in resources:
156
+ ... print(f"Resource: {arn}")
157
+ """
158
+ resources = set()
159
+
160
+ if policy.statement is None:
161
+ return []
162
+
163
+ for stmt in policy.statement:
164
+ # Handle Resource field
165
+ if stmt.resource:
166
+ stmt_resources = [stmt.resource] if isinstance(stmt.resource, str) else stmt.resource
167
+ resources.update(stmt_resources)
168
+
169
+ # Handle NotResource field
170
+ if stmt.not_resource:
171
+ not_resources = (
172
+ [stmt.not_resource] if isinstance(stmt.not_resource, str) else stmt.not_resource
173
+ )
174
+ resources.update(not_resources)
175
+
176
+ return sorted(resources)
177
+
178
+
179
+ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
180
+ """
181
+ Extract all condition keys used in a policy.
182
+
183
+ Args:
184
+ policy: IAMPolicy to extract condition keys from
185
+
186
+ Returns:
187
+ List of all unique condition keys in the policy
188
+
189
+ Example:
190
+ >>> policy = parse_policy(policy_json)
191
+ >>> keys = extract_condition_keys(policy)
192
+ >>> print(f"Policy uses condition keys: {', '.join(keys)}")
193
+ """
194
+ condition_keys: set[str] = set()
195
+
196
+ if policy.statement is None:
197
+ return []
198
+
199
+ for stmt in policy.statement:
200
+ if stmt.condition:
201
+ # Condition format: {"StringEquals": {"aws:username": "johndoe"}}
202
+ for _, key_values in stmt.condition.items():
203
+ if isinstance(key_values, dict):
204
+ condition_keys.update(key_values.keys())
205
+
206
+ return sorted(condition_keys)
207
+
208
+
209
+ def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statement]:
210
+ """
211
+ Find all statements containing a specific action.
212
+
213
+ Supports exact match and wildcard patterns.
214
+
215
+ Args:
216
+ policy: IAMPolicy to search
217
+ action: Action to search for (e.g., "s3:GetObject" or "s3:*")
218
+
219
+ Returns:
220
+ List of Statement objects containing the action
221
+
222
+ Example:
223
+ >>> policy = parse_policy(policy_json)
224
+ >>> stmts = find_statements_with_action(policy, "s3:GetObject")
225
+ >>> for stmt in stmts:
226
+ ... print(f"Statement {stmt.sid} allows s3:GetObject")
227
+ """
228
+ import fnmatch # pylint: disable=import-outside-toplevel
229
+
230
+ matching_statements = []
231
+
232
+ if policy.statement is None:
233
+ return []
234
+
235
+ for stmt in policy.statement:
236
+ stmt_actions = stmt.get_actions()
237
+
238
+ # Check if action matches any statement action (with wildcard support)
239
+ for stmt_action in stmt_actions:
240
+ if fnmatch.fnmatch(action, stmt_action) or fnmatch.fnmatch(stmt_action, action):
241
+ matching_statements.append(stmt)
242
+ break
243
+
244
+ return matching_statements
245
+
246
+
247
+ def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Statement]:
248
+ """
249
+ Find all statements containing a specific resource.
250
+
251
+ Supports exact match and wildcard patterns.
252
+
253
+ Args:
254
+ policy: IAMPolicy to search
255
+ resource: Resource ARN to search for
256
+
257
+ Returns:
258
+ List of Statement objects containing the resource
259
+
260
+ Example:
261
+ >>> policy = parse_policy(policy_json)
262
+ >>> stmts = find_statements_with_resource(policy, "arn:aws:s3:::my-bucket/*")
263
+ >>> print(f"Found {len(stmts)} statements with this resource")
264
+ """
265
+ import fnmatch # pylint: disable=import-outside-toplevel
266
+
267
+ matching_statements = []
268
+
269
+ if policy.statement is None:
270
+ return []
271
+
272
+ for stmt in policy.statement:
273
+ stmt_resources = stmt.get_resources()
274
+
275
+ # Check if resource matches any statement resource (with wildcard support)
276
+ for stmt_resource in stmt_resources:
277
+ if fnmatch.fnmatch(resource, stmt_resource) or fnmatch.fnmatch(stmt_resource, resource):
278
+ matching_statements.append(stmt)
279
+ break
280
+
281
+ return matching_statements
282
+
283
+
284
+ def merge_policies(*policies: IAMPolicy) -> IAMPolicy:
285
+ """
286
+ Merge multiple policies into one.
287
+
288
+ Combines all statements from multiple policies into a single policy document.
289
+ Uses the version from the first policy.
290
+
291
+ Args:
292
+ *policies: IAMPolicy objects to merge
293
+
294
+ Returns:
295
+ New IAMPolicy with all statements combined
296
+
297
+ Example:
298
+ >>> policy1 = parse_policy(json1)
299
+ >>> policy2 = parse_policy(json2)
300
+ >>> merged = merge_policies(policy1, policy2)
301
+ >>> print(f"Merged policy has {len(merged.statement)} statements")
302
+ """
303
+ if not policies:
304
+ raise ValueError("At least one policy must be provided")
305
+
306
+ all_statements: list[Statement] = []
307
+ for policy in policies:
308
+ if policy.statement is not None:
309
+ all_statements.extend(policy.statement)
310
+
311
+ # Use capitalized field names (aliases) for Pydantic model construction
312
+ return IAMPolicy(
313
+ Version=policies[0].version,
314
+ Statement=all_statements,
315
+ Id=None, # Clear ID when merging
316
+ )
317
+
318
+
319
+ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
320
+ """
321
+ Get a summary of policy contents.
322
+
323
+ Args:
324
+ policy: IAMPolicy to summarize
325
+
326
+ Returns:
327
+ Dictionary with summary statistics
328
+
329
+ Example:
330
+ >>> policy = parse_policy(policy_json)
331
+ >>> summary = get_policy_summary(policy)
332
+ >>> print(f"Statements: {summary['statement_count']}")
333
+ >>> print(f"Actions: {summary['action_count']}")
334
+ >>> print(f"Resources: {summary['resource_count']}")
335
+ """
336
+ actions = extract_actions(policy)
337
+ resources = extract_resources(policy)
338
+ condition_keys = extract_condition_keys(policy)
339
+
340
+ # Count allow vs deny statements
341
+ statements = policy.statement or []
342
+ allow_count = sum(1 for s in statements if s.effect and s.effect.lower() == "allow")
343
+ deny_count = sum(1 for s in statements if s.effect and s.effect.lower() == "deny")
344
+
345
+ # Check for wildcards
346
+ has_wildcard_actions = any("*" in action for action in actions)
347
+ has_wildcard_resources = any("*" in resource for resource in resources)
348
+
349
+ return {
350
+ "version": policy.version,
351
+ "statement_count": len(statements),
352
+ "allow_statements": allow_count,
353
+ "deny_statements": deny_count,
354
+ "action_count": len(actions),
355
+ "resource_count": len(resources),
356
+ "condition_key_count": len(condition_keys),
357
+ "has_wildcard_actions": has_wildcard_actions,
358
+ "has_wildcard_resources": has_wildcard_resources,
359
+ "actions": actions,
360
+ "resources": resources,
361
+ "condition_keys": condition_keys,
362
+ }
363
+
364
+
365
+ def policy_to_json(policy: IAMPolicy, indent: int = 2) -> str:
366
+ """
367
+ Convert IAMPolicy to formatted JSON string.
368
+
369
+ Args:
370
+ policy: IAMPolicy to convert
371
+ indent: Number of spaces for indentation (default: 2)
372
+
373
+ Returns:
374
+ Formatted JSON string
375
+
376
+ Example:
377
+ >>> policy = parse_policy(policy_dict)
378
+ >>> json_str = policy_to_json(policy)
379
+ >>> print(json_str)
380
+ """
381
+ policy_dict = policy.model_dump(by_alias=True, exclude_none=True)
382
+ return json.dumps(policy_dict, indent=indent)
383
+
384
+
385
+ def policy_to_dict(policy: IAMPolicy) -> dict[str, Any]:
386
+ """
387
+ Convert IAMPolicy to Python dictionary.
388
+
389
+ Args:
390
+ policy: IAMPolicy to convert
391
+
392
+ Returns:
393
+ Policy as Python dict with AWS field names (Version, Statement, etc.)
394
+
395
+ Example:
396
+ >>> policy = parse_policy(policy_json)
397
+ >>> policy_dict = policy_to_dict(policy)
398
+ >>> print(policy_dict["Version"])
399
+ """
400
+ return policy.model_dump(by_alias=True, exclude_none=True)
401
+
402
+
403
+ def is_resource_policy(policy: IAMPolicy) -> bool:
404
+ """
405
+ Check if policy appears to be a resource policy (vs identity policy).
406
+
407
+ Resource policies have a Principal field, identity policies don't.
408
+
409
+ Args:
410
+ policy: IAMPolicy to check
411
+
412
+ Returns:
413
+ True if policy appears to be a resource policy
414
+
415
+ Example:
416
+ >>> policy = parse_policy(bucket_policy_json)
417
+ >>> if is_resource_policy(policy):
418
+ ... print("This is an S3 bucket policy or similar")
419
+ """
420
+ if policy.statement is None:
421
+ return False
422
+ return any(stmt.principal is not None for stmt in policy.statement)
423
+
424
+
425
+ def has_public_access(policy: IAMPolicy) -> bool:
426
+ """
427
+ Check if policy grants public access (Principal: "*").
428
+
429
+ Args:
430
+ policy: IAMPolicy to check
431
+
432
+ Returns:
433
+ True if any statement has Principal set to "*"
434
+
435
+ Example:
436
+ >>> policy = parse_policy(policy_json)
437
+ >>> if has_public_access(policy):
438
+ ... print("WARNING: This policy allows public access!")
439
+ """
440
+ if policy.statement is None:
441
+ return False
442
+
443
+ for stmt in policy.statement:
444
+ if stmt.principal == "*":
445
+ return True
446
+ if isinstance(stmt.principal, dict):
447
+ # Check for {"AWS": "*"} or {"Service": "*"}
448
+ for value in stmt.principal.values():
449
+ if value == "*" or (isinstance(value, list) and "*" in value):
450
+ return True
451
+ return False