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,556 @@
1
+ """Policy structure validation check.
2
+
3
+ This check validates the fundamental structure of IAM policy statements,
4
+ ensuring they meet AWS IAM requirements. It checks for:
5
+
6
+ 1. Required fields (Effect, Action/NotAction, Resource/NotResource)
7
+ 2. Mutual exclusivity (Action vs NotAction, Resource vs NotResource, Principal vs NotPrincipal)
8
+ 3. Valid field values (Effect must be "Allow" or "Deny")
9
+ 4. Unknown/unexpected fields in statements
10
+ 5. Valid Version field in policy document
11
+
12
+ This check is inspired by Parliament's analyze_statement function and should run
13
+ BEFORE all other checks to catch fundamental structural issues early.
14
+
15
+ By detecting these issues early, we can:
16
+ - Provide detailed error messages about what's wrong
17
+ - Post specific findings to GitHub for user feedback
18
+ - Allow other checks to run and find additional issues
19
+ - Help users fix policies that would otherwise be rejected by Pydantic validation
20
+ """
21
+
22
+ import re
23
+ from typing import Any, ClassVar
24
+
25
+ from iam_validator.core.aws_service import AWSServiceFetcher
26
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
27
+ from iam_validator.core.models import IAMPolicy, PolicyType, ValidationIssue
28
+
29
+ # Valid statement fields according to AWS IAM policy grammar
30
+ # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
31
+ VALID_STATEMENT_FIELDS = {
32
+ "Effect",
33
+ "Sid",
34
+ "Principal",
35
+ "NotPrincipal",
36
+ "Action",
37
+ "NotAction",
38
+ "Resource",
39
+ "NotResource",
40
+ "Condition",
41
+ }
42
+
43
+ # Valid policy document fields
44
+ VALID_POLICY_FIELDS = {
45
+ "Version",
46
+ "Statement",
47
+ "Id",
48
+ }
49
+
50
+ # Valid AWS IAM policy versions
51
+ VALID_POLICY_VERSIONS = {"2012-10-17", "2008-10-17"}
52
+
53
+ # Valid Effect values
54
+ VALID_EFFECTS = {"Allow", "Deny"}
55
+
56
+ # SID format: alphanumeric characters only (no spaces, hyphens, or underscores in AWS strict grammar)
57
+ # However, AWS console and APIs often accept hyphens and underscores, so we allow them
58
+ SID_PATTERN = re.compile(r"^[a-zA-Z0-9]+$")
59
+
60
+ # Assume role actions used in trust policies (frozen set for O(1) lookup performance)
61
+ ASSUME_ROLE_ACTIONS = frozenset(
62
+ {
63
+ "sts:AssumeRole",
64
+ "sts:AssumeRoleWithSAML",
65
+ "sts:AssumeRoleWithWebIdentity",
66
+ "sts:TagSession",
67
+ "sts:SetSourceIdentity",
68
+ "sts:SetContext",
69
+ }
70
+ )
71
+
72
+
73
+ def is_trust_policy(policy: IAMPolicy) -> bool:
74
+ """Detect if policy is a trust policy (role assumption policy).
75
+
76
+ Trust policies are a special type of resource policy attached to IAM roles.
77
+ They control who can assume the role.
78
+
79
+ Battle-hardened detection logic (minimizes false positives):
80
+ 1. Has Principal element (resource policy characteristic)
81
+ 2. Contains assume role actions (sts:AssumeRole*, sts:TagSession)
82
+ 3. Effect is "Allow" (trust policies grant assumption, never Deny)
83
+ 4. No specific resource ARNs (role itself is the resource; only * or *:* allowed)
84
+
85
+ Conservative approach prevents false positives:
86
+ - Exact action matching (not just startswith)
87
+ - Rejects policies with specific resource ARNs
88
+ - Validates Effect is Allow
89
+
90
+ Args:
91
+ policy: IAM policy to analyze
92
+
93
+ Returns:
94
+ True if policy appears to be a trust policy
95
+
96
+ Examples:
97
+ Trust policy (returns True):
98
+ {"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"},
99
+ "Action": "sts:AssumeRole"}
100
+
101
+ S3 bucket policy (returns False - has specific resources):
102
+ {"Effect": "Allow", "Principal": "*",
103
+ "Action": "s3:GetObject", "Resource": "arn:aws:s3:::bucket/*"}
104
+ """
105
+ # First pass: Check if ANY statement has specific resource ARNs
106
+ # Trust policies don't have specific resource ARNs (role itself is the resource)
107
+ for statement in policy.statement or []:
108
+ if statement.resource:
109
+ resources = (
110
+ [statement.resource] if isinstance(statement.resource, str) else statement.resource
111
+ )
112
+ for resource in resources:
113
+ if isinstance(resource, str) and resource != "*" and not resource.endswith(":*"):
114
+ # Specific resource ARN found in ANY statement - NOT a trust policy
115
+ # Trust policies should only have *, *:*, or no Resource field
116
+ return False
117
+
118
+ # Second pass: Check for valid assume statements
119
+ has_valid_assume_statement = False
120
+
121
+ for statement in policy.statement or []:
122
+ # Skip if no principal (trust policies must have principals)
123
+ if statement.principal is None and statement.not_principal is None:
124
+ continue
125
+
126
+ # Skip if Effect is Deny (trust policies use Allow)
127
+ if statement.effect and statement.effect.lower() != "allow":
128
+ continue
129
+
130
+ # Get actions (handle both string and list)
131
+ actions = []
132
+ if statement.action:
133
+ actions = [statement.action] if isinstance(statement.action, str) else statement.action
134
+
135
+ # Check if any action is an assume action (O(1) set lookup)
136
+ has_assume_action = any(
137
+ action in ASSUME_ROLE_ACTIONS or action in ("sts:*", "*")
138
+ for action in actions
139
+ if isinstance(action, str)
140
+ )
141
+
142
+ if has_assume_action:
143
+ # Found a valid trust policy statement
144
+ has_valid_assume_statement = True
145
+ # Continue checking - we already validated no specific resources in first pass
146
+
147
+ return has_valid_assume_statement
148
+
149
+
150
+ def detect_policy_type(policy: IAMPolicy) -> PolicyType:
151
+ """Auto-detect policy type based on statement structure.
152
+
153
+ Detection logic (simple and safe - avoids false positives):
154
+ 1. If any statement has Principal/NotPrincipal → RESOURCE_POLICY
155
+ 2. Otherwise → IDENTITY_POLICY (default, also covers SCPs)
156
+
157
+ Note: The following policy types require EXPLICIT specification via --policy-type flag
158
+ and are NOT auto-detected to avoid false positives and confusing errors:
159
+
160
+ - TRUST_POLICY: Requires explicit flag to enable trust-specific validation
161
+ and suppress irrelevant warnings (missing Resource field, etc.)
162
+ Use: --policy-type TRUST_POLICY
163
+
164
+ - SERVICE_CONTROL_POLICY: SCPs have the same structure as identity policies
165
+ and cannot be reliably distinguished without context
166
+ Use: --policy-type SERVICE_CONTROL_POLICY
167
+
168
+ - RESOURCE_CONTROL_POLICY: RCPs have strict requirements that require explicit
169
+ validation mode to avoid false positives
170
+ Use: --policy-type RESOURCE_CONTROL_POLICY
171
+
172
+ Auto-detection only distinguishes between:
173
+ - IDENTITY_POLICY (no Principal element) - most common
174
+ - RESOURCE_POLICY (has Principal element) - S3, SNS, SQS, etc.
175
+
176
+ Args:
177
+ policy: IAM policy to analyze
178
+
179
+ Returns:
180
+ Detected PolicyType (IDENTITY_POLICY or RESOURCE_POLICY only)
181
+ """
182
+ # Check if any statement has Principal/NotPrincipal (indicates resource policy)
183
+ for statement in policy.statement or []:
184
+ if statement.principal is not None or statement.not_principal is not None:
185
+ return "RESOURCE_POLICY"
186
+
187
+ # Default to identity policy (most common case)
188
+ # SCPs have the same structure and will be detected as IDENTITY_POLICY
189
+ return "IDENTITY_POLICY"
190
+
191
+
192
+ def validate_policy_document(policy_dict: dict[str, Any]) -> list[ValidationIssue]:
193
+ """Validate the top-level policy document structure.
194
+
195
+ Args:
196
+ policy_dict: Raw policy dictionary
197
+
198
+ Returns:
199
+ List of validation issues
200
+ """
201
+ issues: list[ValidationIssue] = []
202
+
203
+ # Check for unknown fields in policy document
204
+ unknown_fields = set(policy_dict.keys()) - VALID_POLICY_FIELDS
205
+ if unknown_fields:
206
+ issues.append(
207
+ ValidationIssue(
208
+ severity="error",
209
+ statement_index=-1, # Policy-level issue
210
+ issue_type="unknown_policy_field",
211
+ message=f"Policy document contains unknown field(s): {', '.join(sorted(f'`{f}`' for f in unknown_fields))}",
212
+ suggestion=f"Remove the unknown field(s). Valid policy fields are: {', '.join(f'`{f}`' for f in sorted(VALID_POLICY_FIELDS))}",
213
+ example=('{\n "Version": "2012-10-17",\n "Statement": [...]\n}'),
214
+ )
215
+ )
216
+
217
+ # Validate Version field
218
+ if "Version" not in policy_dict:
219
+ issues.append(
220
+ ValidationIssue(
221
+ severity="error",
222
+ statement_index=-1,
223
+ issue_type="missing_version",
224
+ message="Policy document is missing the `Version` field",
225
+ suggestion="Add a `Version` field with value `2012-10-17` (recommended) or `2008-10-17`",
226
+ example=('{\n "Version": "2012-10-17",\n "Statement": [...]\n}'),
227
+ )
228
+ )
229
+ elif policy_dict["Version"] not in VALID_POLICY_VERSIONS:
230
+ issues.append(
231
+ ValidationIssue(
232
+ severity="error",
233
+ statement_index=-1,
234
+ issue_type="invalid_version",
235
+ message=f"Invalid policy `Version`: `{policy_dict['Version']}`. Must be `2012-10-17` or `2008-10-17`",
236
+ suggestion="Use `Version` `2012-10-17` (recommended) for the latest IAM policy grammar",
237
+ example=('{\n "Version": "2012-10-17",\n "Statement": [...]\n}'),
238
+ )
239
+ )
240
+
241
+ # Validate Statement field
242
+ if "Statement" not in policy_dict:
243
+ issues.append(
244
+ ValidationIssue(
245
+ severity="error",
246
+ statement_index=-1,
247
+ issue_type="missing_statement",
248
+ message="Policy document is missing the `Statement` field",
249
+ suggestion="Add a `Statement` field containing an array of policy statements",
250
+ example=(
251
+ "{\n"
252
+ ' "Version": "2012-10-17",\n'
253
+ ' "Statement": [\n'
254
+ " {\n"
255
+ ' "Effect": "Allow",\n'
256
+ ' "Action": "s3:GetObject",\n'
257
+ ' "Resource": "*"\n'
258
+ " }\n"
259
+ " ]\n"
260
+ "}"
261
+ ),
262
+ )
263
+ )
264
+ elif not isinstance(policy_dict["Statement"], list):
265
+ issues.append(
266
+ ValidationIssue(
267
+ severity="error",
268
+ statement_index=-1,
269
+ issue_type="invalid_statement_type",
270
+ message=f"`Statement` field must be an array, not `{type(policy_dict['Statement']).__name__}`",
271
+ suggestion="Wrap your statement in an array: [ {...} ]",
272
+ example=(
273
+ "{\n"
274
+ ' "Version": "2012-10-17",\n'
275
+ ' "Statement": [\n'
276
+ " {\n"
277
+ ' "Effect": "Allow",\n'
278
+ ' "Action": "s3:GetObject",\n'
279
+ ' "Resource": "*"\n'
280
+ " }\n"
281
+ " ]\n"
282
+ "}"
283
+ ),
284
+ )
285
+ )
286
+ elif len(policy_dict["Statement"]) == 0:
287
+ issues.append(
288
+ ValidationIssue(
289
+ severity="error",
290
+ statement_index=-1,
291
+ issue_type="empty_statement",
292
+ message="Policy document has an empty `Statement` array",
293
+ suggestion="Add at least one policy `Statement`",
294
+ example=(
295
+ "{\n"
296
+ ' "Version": "2012-10-17",\n'
297
+ ' "Statement": [\n'
298
+ " {\n"
299
+ ' "Effect": "Allow",\n'
300
+ ' "Action": "s3:GetObject",\n'
301
+ ' "Resource": "*"\n'
302
+ " }\n"
303
+ " ]\n"
304
+ "}"
305
+ ),
306
+ )
307
+ )
308
+
309
+ return issues
310
+
311
+
312
+ def validate_statement_structure(
313
+ statement_dict: dict[str, Any], statement_idx: int, policy_type: str = "IDENTITY_POLICY"
314
+ ) -> list[ValidationIssue]:
315
+ """Validate the structure of a single statement.
316
+
317
+ This implements validation similar to Parliament's analyze_statement function.
318
+
319
+ Args:
320
+ statement_dict: Raw statement dictionary
321
+ statement_idx: Index of the statement in the policy
322
+ policy_type: Type of policy being validated (affects missing Resource validation)
323
+
324
+ Returns:
325
+ List of validation issues
326
+ """
327
+ issues: list[ValidationIssue] = []
328
+ sid = statement_dict.get("Sid")
329
+
330
+ # Check for unknown fields
331
+ unknown_fields = set(statement_dict.keys()) - VALID_STATEMENT_FIELDS
332
+ if unknown_fields:
333
+ issues.append(
334
+ ValidationIssue(
335
+ severity="error",
336
+ statement_sid=sid,
337
+ statement_index=statement_idx,
338
+ issue_type="unknown_field",
339
+ message=f"`Statement` contains unknown field(s): {', '.join(sorted(f'`{f}`' for f in unknown_fields))}",
340
+ suggestion=f"Remove the unknown field(s). Valid statement fields are: {', '.join(sorted(f'`{f}`' for f in VALID_STATEMENT_FIELDS))}",
341
+ )
342
+ )
343
+
344
+ # Validate Effect field (required)
345
+ if "Effect" not in statement_dict:
346
+ issues.append(
347
+ ValidationIssue(
348
+ severity="error",
349
+ statement_sid=sid,
350
+ statement_index=statement_idx,
351
+ issue_type="missing_effect",
352
+ message="`Statement` is missing the required `Effect` field",
353
+ suggestion="Add an `Effect` field with value `Allow` or `Deny`",
354
+ example='"Effect": "Allow"',
355
+ field_name="effect",
356
+ )
357
+ )
358
+ elif statement_dict["Effect"] not in VALID_EFFECTS:
359
+ issues.append(
360
+ ValidationIssue(
361
+ severity="error",
362
+ statement_sid=sid,
363
+ statement_index=statement_idx,
364
+ issue_type="invalid_effect",
365
+ message=f"Invalid `Effect` value: `{statement_dict['Effect']}`. Must be `Allow` or `Deny`",
366
+ suggestion="Change `Effect` to either `Allow` or `Deny`",
367
+ example='"Effect": "Allow"',
368
+ field_name="effect",
369
+ )
370
+ )
371
+
372
+ # Validate SID format (if present)
373
+ if sid is not None:
374
+ if not isinstance(sid, str):
375
+ issues.append(
376
+ ValidationIssue(
377
+ severity="error",
378
+ statement_sid=str(sid),
379
+ statement_index=statement_idx,
380
+ issue_type="invalid_sid_type",
381
+ message=f"`Sid` must be a `string`, not `{type(sid).__name__}`",
382
+ suggestion='Wrap the `Sid` value in quotes to make it a string: `"Sid": "AllowS3Access"`',
383
+ example='"Sid": "AllowS3Access"',
384
+ field_name="sid",
385
+ )
386
+ )
387
+ elif not SID_PATTERN.match(sid):
388
+ # According to AWS grammar, SID should be alphanumeric only
389
+ # However, we issue a warning instead of error since some AWS services accept more
390
+ invalid_chars = "".join(set(c for c in sid if not c.isalnum()))
391
+ issues.append(
392
+ ValidationIssue(
393
+ severity="warning",
394
+ statement_sid=sid,
395
+ statement_index=statement_idx,
396
+ issue_type="invalid_sid_format",
397
+ message=f"`Sid` `{sid}` contains non-alphanumeric characters: `{invalid_chars}`",
398
+ suggestion="According to AWS IAM policy grammar, `Sid` should contain only alphanumeric characters `(A-Z, a-z, 0-9)`.",
399
+ field_name="sid",
400
+ )
401
+ )
402
+
403
+ # Validate Principal/NotPrincipal mutual exclusivity
404
+ if "Principal" in statement_dict and "NotPrincipal" in statement_dict:
405
+ issues.append(
406
+ ValidationIssue(
407
+ severity="error",
408
+ statement_sid=sid,
409
+ statement_index=statement_idx,
410
+ issue_type="principal_conflict",
411
+ message="`Statement` contains both `Principal` and `NotPrincipal` fields",
412
+ suggestion="Use either `Principal` or `NotPrincipal`, not both",
413
+ field_name="principal",
414
+ )
415
+ )
416
+
417
+ # Validate Action/NotAction mutual exclusivity and presence
418
+ has_action = "Action" in statement_dict
419
+ has_not_action = "NotAction" in statement_dict
420
+
421
+ if has_action and has_not_action:
422
+ issues.append(
423
+ ValidationIssue(
424
+ severity="error",
425
+ statement_sid=sid,
426
+ statement_index=statement_idx,
427
+ issue_type="action_conflict",
428
+ message="`Statement` contains both `Action` and `NotAction` fields",
429
+ suggestion="Use either `Action` or `NotAction`, not both",
430
+ field_name="action",
431
+ )
432
+ )
433
+ elif not has_action and not has_not_action:
434
+ issues.append(
435
+ ValidationIssue(
436
+ severity="error",
437
+ statement_sid=sid,
438
+ statement_index=statement_idx,
439
+ issue_type="missing_action",
440
+ message="`Statement` is missing both `Action` and `NotAction` fields",
441
+ suggestion="Add either an `Action` or `NotAction` field to specify which AWS actions this statement applies to",
442
+ example=('"Action": [\n "s3:GetObject",\n "s3:PutObject"\n]'),
443
+ field_name="action",
444
+ )
445
+ )
446
+
447
+ # Validate Resource/NotResource mutual exclusivity and presence
448
+ has_resource = "Resource" in statement_dict
449
+ has_not_resource = "NotResource" in statement_dict
450
+
451
+ if has_resource and has_not_resource:
452
+ issues.append(
453
+ ValidationIssue(
454
+ severity="error",
455
+ statement_sid=sid,
456
+ statement_index=statement_idx,
457
+ issue_type="resource_conflict",
458
+ message="`Statement` contains both `Resource` and `NotResource` fields",
459
+ suggestion="Use either `Resource` or `NotResource`, not both",
460
+ field_name="resource",
461
+ )
462
+ )
463
+ elif not has_resource and not has_not_resource:
464
+ # Trust policies don't need Resource field (role itself is the resource)
465
+ if policy_type == "TRUST_POLICY":
466
+ # Skip this check for trust policies - it's expected and correct
467
+ pass
468
+ else:
469
+ # Resource/NotResource are optional in some contexts (e.g., resource policies with Principal)
470
+ # Issue an info-level message
471
+ issues.append(
472
+ ValidationIssue(
473
+ severity="info",
474
+ statement_sid=sid,
475
+ statement_index=statement_idx,
476
+ issue_type="missing_resource",
477
+ message="`Statement` is missing both `Resource` and `NotResource` fields",
478
+ suggestion="Most policies require a `Resource` field. Add a `Resource` or `NotResource` field to specify which AWS resources this statement applies to.",
479
+ example=('"Resource": "*" OR "Resource": "arn:aws:s3:::my-bucket/*"'),
480
+ field_name="resource",
481
+ )
482
+ )
483
+
484
+ return issues
485
+
486
+
487
+ class PolicyStructureCheck(PolicyCheck):
488
+ """Validates fundamental IAM policy structure.
489
+
490
+ This check ensures policies meet AWS IAM requirements for basic structure:
491
+ - Required fields are present
492
+ - Mutually exclusive fields aren't used together
493
+ - Field values are valid
494
+ - No unknown fields are present
495
+
496
+ This check should run FIRST before all other checks to catch fundamental
497
+ issues early.
498
+ """
499
+
500
+ check_id: ClassVar[str] = "policy_structure"
501
+ description: ClassVar[str] = (
502
+ "Validates fundamental IAM policy structure (required fields, field conflicts, valid values)"
503
+ )
504
+ default_severity: ClassVar[str] = "error"
505
+
506
+ async def execute_policy(
507
+ self,
508
+ policy: IAMPolicy,
509
+ policy_file: str,
510
+ fetcher: AWSServiceFetcher,
511
+ config: CheckConfig,
512
+ **kwargs,
513
+ ) -> list[ValidationIssue]:
514
+ """Execute the policy structure check on the entire policy.
515
+
516
+ This validates:
517
+ 1. Policy document structure (Version, Statement fields)
518
+ 2. Each statement's structure (required fields, conflicts, valid values)
519
+ 3. Auto-detects policy type for better validation
520
+
521
+ Args:
522
+ policy: The complete IAM policy to validate
523
+ policy_file: Path to the policy file (unused)
524
+ fetcher: AWS service fetcher (unused)
525
+ config: Check configuration
526
+ **kwargs: Additional arguments (may contain raw_policy_dict)
527
+
528
+ Returns:
529
+ List of ValidationIssue objects for structural problems
530
+ """
531
+ del policy_file, fetcher, config # Unused
532
+ issues: list[ValidationIssue] = []
533
+
534
+ # Get policy_type from kwargs (passed by validation flow)
535
+ policy_type = kwargs.get("policy_type", "IDENTITY_POLICY")
536
+
537
+ # Validate policy document structure if raw dict is available
538
+ raw_policy_dict = kwargs.get("raw_policy_dict")
539
+ if raw_policy_dict:
540
+ issues.extend(validate_policy_document(raw_policy_dict))
541
+
542
+ # Validate each statement's structure
543
+ if isinstance(raw_policy_dict.get("Statement"), list):
544
+ for idx, stmt_dict in enumerate(raw_policy_dict["Statement"]):
545
+ if isinstance(stmt_dict, dict):
546
+ issues.extend(validate_statement_structure(stmt_dict, idx, policy_type))
547
+
548
+ # Auto-detect policy type
549
+ detected_type = detect_policy_type(policy)
550
+
551
+ # Store detected type in kwargs for other checks to use
552
+ # This allows checks like principal_validation to automatically apply
553
+ if "detected_policy_type" not in kwargs:
554
+ kwargs["detected_policy_type"] = detected_type
555
+
556
+ return issues