iam-policy-validator 1.8.0__py3-none-any.whl → 1.10.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 (49) hide show
  1. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/METADATA +106 -1
  2. iam_policy_validator-1.10.0.dist-info/RECORD +96 -0
  3. iam_validator/__init__.py +1 -1
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/action_condition_enforcement.py +504 -190
  6. iam_validator/checks/action_resource_matching.py +8 -15
  7. iam_validator/checks/action_validation.py +6 -12
  8. iam_validator/checks/condition_key_validation.py +6 -12
  9. iam_validator/checks/condition_type_mismatch.py +9 -16
  10. iam_validator/checks/full_wildcard.py +9 -13
  11. iam_validator/checks/mfa_condition_check.py +8 -17
  12. iam_validator/checks/policy_size.py +6 -39
  13. iam_validator/checks/policy_structure.py +10 -40
  14. iam_validator/checks/policy_type_validation.py +18 -19
  15. iam_validator/checks/principal_validation.py +11 -20
  16. iam_validator/checks/resource_validation.py +5 -12
  17. iam_validator/checks/sensitive_action.py +8 -15
  18. iam_validator/checks/service_wildcard.py +6 -12
  19. iam_validator/checks/set_operator_validation.py +11 -18
  20. iam_validator/checks/sid_uniqueness.py +8 -38
  21. iam_validator/checks/trust_policy_validation.py +8 -14
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +6 -12
  24. iam_validator/checks/wildcard_resource.py +6 -12
  25. iam_validator/commands/cache.py +4 -3
  26. iam_validator/commands/validate.py +26 -4
  27. iam_validator/core/__init__.py +1 -1
  28. iam_validator/core/aws_fetcher.py +24 -1030
  29. iam_validator/core/aws_service/__init__.py +21 -0
  30. iam_validator/core/aws_service/cache.py +108 -0
  31. iam_validator/core/aws_service/client.py +205 -0
  32. iam_validator/core/aws_service/fetcher.py +612 -0
  33. iam_validator/core/aws_service/parsers.py +149 -0
  34. iam_validator/core/aws_service/patterns.py +51 -0
  35. iam_validator/core/aws_service/storage.py +291 -0
  36. iam_validator/core/aws_service/validators.py +379 -0
  37. iam_validator/core/check_registry.py +82 -14
  38. iam_validator/core/config/defaults.py +10 -0
  39. iam_validator/core/constants.py +17 -0
  40. iam_validator/core/label_manager.py +197 -0
  41. iam_validator/core/policy_checks.py +7 -3
  42. iam_validator/core/pr_commenter.py +34 -7
  43. iam_validator/sdk/__init__.py +1 -1
  44. iam_validator/sdk/context.py +1 -1
  45. iam_validator/sdk/helpers.py +1 -1
  46. iam_policy_validator-1.8.0.dist-info/RECORD +0 -87
  47. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/WHEEL +0 -0
  48. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/entry_points.txt +0 -0
  49. {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.10.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,11 +1,23 @@
1
1
  """
2
- Action-Specific Condition Enforcement Check (Unified)
2
+ Action-Specific Condition Enforcement Check
3
3
 
4
- This built-in check ensures that specific actions have required conditions.
4
+ This check ensures that specific actions have required conditions.
5
5
  Supports ALL types of conditions: MFA, IP, VPC, time, tags, encryption, etc.
6
6
 
7
- Supports advanced "all_of" and "any_of" logic for both actions and conditions.
8
- Supports both STATEMENT-LEVEL and POLICY-LEVEL enforcement.
7
+ The entire policy is scanned once, checking all statements for matching actions.
8
+
9
+ ACTION MATCHING MODES:
10
+ - Simple list: Checks each statement for any of the specified actions
11
+ Example: actions: ["iam:PassRole", "iam:CreateUser"]
12
+
13
+ - any_of: Finds statements that contain ANY of the specified actions
14
+ Example: actions: {any_of: ["iam:CreateUser", "iam:AttachUserPolicy"]}
15
+
16
+ - all_of: Finds statements that contain ALL specified actions (overly permissive detection)
17
+ Example: actions: {all_of: ["iam:CreateAccessKey", "iam:UpdateAccessKey"]}
18
+
19
+ - none_of: Flags statements that contain forbidden actions
20
+ Example: actions: {none_of: ["iam:DeleteUser", "s3:DeleteBucket"]}
9
21
 
10
22
  Common use cases:
11
23
  - iam:PassRole must have iam:PassedToService condition
@@ -13,17 +25,17 @@ Common use cases:
13
25
  - Actions must have source IP restrictions
14
26
  - Resources must have required tags
15
27
  - Combine multiple conditions (MFA + IP + Tags)
16
- - Policy-level: Ensure ALL statements granting certain actions have MFA
28
+ - Detect overly permissive statements (all_of)
29
+ - Ensure privilege escalation combinations are protected
17
30
 
18
31
  Configuration in iam-validator.yaml:
19
32
 
20
33
  checks:
21
34
  action_condition_enforcement:
22
35
  enabled: true
23
- severity: error
36
+ severity: high
24
37
  description: "Enforce specific conditions for specific actions"
25
38
 
26
- # STATEMENT-LEVEL: Check individual statements (default)
27
39
  action_condition_requirements:
28
40
  # BASIC: Simple action with required condition
29
41
  - actions:
@@ -87,56 +99,38 @@ Configuration in iam-validator.yaml:
87
99
  expected_value: false
88
100
  description: "Ensure insecure transport is never allowed"
89
101
 
90
- # none_of for actions: Flag if forbidden actions are present
91
- - actions:
92
- none_of:
93
- - "iam:*"
94
- - "s3:DeleteBucket"
95
- description: "These dangerous actions should never be used"
96
-
97
- # POLICY-LEVEL: Scan entire policy and enforce conditions across all matching statements
98
- policy_level_requirements:
99
- # Example: If ANY statement grants privilege escalation actions,
100
- # then ALL such statements must have MFA
102
+ # any_of for actions: If ANY statement grants privilege escalation actions, require MFA
101
103
  - actions:
102
104
  any_of:
103
105
  - "iam:CreateUser"
104
106
  - "iam:AttachUserPolicy"
105
107
  - "iam:PutUserPolicy"
106
- scope: "policy"
107
108
  required_conditions:
108
109
  - condition_key: "aws:MultiFactorAuthPresent"
109
110
  expected_value: true
110
- description: "Privilege escalation actions require MFA across entire policy"
111
+ description: "Privilege escalation actions require MFA"
111
112
  severity: "critical"
112
113
 
113
- # Example: All admin actions across the policy must have MFA
114
- - actions:
115
- any_of:
116
- - "iam:*"
117
- - "s3:*"
118
- scope: "policy"
119
- required_conditions:
120
- all_of:
121
- - condition_key: "aws:MultiFactorAuthPresent"
122
- expected_value: true
123
- - condition_key: "aws:SourceIp"
124
- apply_to: "all_matching_statements"
125
-
126
- # Example: Ensure no statement in the policy allows dangerous combinations
114
+ # all_of for actions: Flag statements that contain BOTH dangerous actions (overly permissive)
127
115
  - actions:
128
116
  all_of:
129
117
  - "iam:CreateAccessKey"
130
118
  - "iam:UpdateAccessKey"
131
- scope: "policy"
132
119
  severity: "critical"
133
- description: "Dangerous combination of actions detected in policy"
120
+ description: "Statement grants both CreateAccessKey and UpdateAccessKey - too permissive"
121
+
122
+ # none_of for actions: Flag if forbidden actions are present
123
+ - actions:
124
+ none_of:
125
+ - "iam:DeleteUser"
126
+ - "s3:DeleteBucket"
127
+ description: "These dangerous actions should never be used"
134
128
  """
135
129
 
136
130
  import re
137
- from typing import TYPE_CHECKING, Any
131
+ from typing import TYPE_CHECKING, Any, ClassVar
138
132
 
139
- from iam_validator.core.aws_fetcher import AWSServiceFetcher
133
+ from iam_validator.core.aws_service import AWSServiceFetcher
140
134
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
141
135
  from iam_validator.core.models import Statement, ValidationIssue
142
136
 
@@ -147,206 +141,526 @@ if TYPE_CHECKING:
147
141
  class ActionConditionEnforcementCheck(PolicyCheck):
148
142
  """Enforces specific condition requirements for specific actions with all_of/any_of support."""
149
143
 
150
- @property
151
- def check_id(self) -> str:
152
- return "action_condition_enforcement"
144
+ check_id: ClassVar[str] = "action_condition_enforcement"
145
+ description: ClassVar[str] = (
146
+ "Enforces conditions (MFA, IP, tags, etc.) for specific actions (supports all_of/any_of)"
147
+ )
148
+ default_severity: ClassVar[str] = "error"
149
+
150
+ async def execute_policy(
151
+ self,
152
+ policy: "IAMPolicy",
153
+ policy_file: str,
154
+ fetcher: AWSServiceFetcher,
155
+ config: CheckConfig,
156
+ **kwargs,
157
+ ) -> list[ValidationIssue]:
158
+ """
159
+ Execute policy-wide condition enforcement check.
160
+
161
+ This method scans the entire policy once and enforces conditions based on action matching:
162
+ - Simple list: Checks each statement for matching actions
163
+ - all_of: Finds statements that contain ALL specified actions (overly permissive detection)
164
+ - any_of: Finds statements that contain ANY of the specified actions
165
+ - none_of: Flags statements that contain forbidden actions
166
+
167
+ Example use cases:
168
+ - any_of: "If ANY statement grants iam:CreateUser, iam:AttachUserPolicy,
169
+ or iam:PutUserPolicy, then ALL such statements must have MFA condition."
170
+ - all_of: "Flag statements that grant BOTH iam:CreateAccessKey AND
171
+ iam:UpdateAccessKey (overly permissive)"
172
+
173
+ Args:
174
+ policy: The complete IAM policy to check
175
+ policy_file: Path to the policy file (for context/reporting)
176
+ fetcher: AWS service fetcher for validation against AWS APIs
177
+ config: CheckConfig: Configuration for this check instance
178
+ **kwargs: Additional context (policy_type, etc.)
179
+
180
+ Returns:
181
+ List of ValidationIssue objects found by this check
182
+ """
183
+ del policy_file, kwargs # Not used in current implementation
184
+ issues = []
185
+
186
+ # Get action condition requirements from config
187
+ # Support both old (policy_level_requirements) and new (action_condition_requirements) keys
188
+ requirements = config.config.get(
189
+ "action_condition_requirements",
190
+ config.config.get("policy_level_requirements", []),
191
+ )
192
+
193
+ if not requirements:
194
+ return issues
195
+
196
+ # Process each requirement
197
+ for requirement in requirements:
198
+ # Check if actions use all_of/any_of/none_of (policy-wide) or simple list (per-statement)
199
+ actions_config = requirement.get("actions", [])
200
+ uses_logical_operators = isinstance(actions_config, dict) and any(
201
+ key in actions_config for key in ("all_of", "any_of", "none_of")
202
+ )
153
203
 
154
- @property
155
- def description(self) -> str:
156
- return "Enforces conditions (MFA, IP, tags, etc.) for specific actions (supports all_of/any_of)"
204
+ if uses_logical_operators:
205
+ # Policy-wide detection (all_of/any_of/none_of)
206
+ policy_issues = await self._check_policy_wide(policy, requirement, fetcher, config)
207
+ issues.extend(policy_issues)
208
+ else:
209
+ # Per-statement check (simple list)
210
+ statement_issues = await self._check_per_statement(
211
+ policy, requirement, fetcher, config
212
+ )
213
+ issues.extend(statement_issues)
157
214
 
158
- @property
159
- def default_severity(self) -> str:
160
- return "error"
215
+ return issues
161
216
 
162
- async def execute(
217
+ async def _check_policy_wide(
163
218
  self,
164
- statement: Statement,
165
- statement_idx: int,
219
+ policy: "IAMPolicy",
220
+ requirement: dict[str, Any],
221
+ fetcher: AWSServiceFetcher,
222
+ config: CheckConfig,
223
+ ) -> list[ValidationIssue]:
224
+ """
225
+ Check actions across the entire policy using all_of/any_of/none_of logic.
226
+
227
+ This enables policy-wide detection patterns:
228
+ - all_of: ALL required actions must exist somewhere in the policy
229
+ - any_of: At least ONE required action must exist somewhere in the policy
230
+ - none_of: NONE of the forbidden actions should exist in the policy
231
+ """
232
+ issues = []
233
+ actions_config = requirement.get("actions", {})
234
+ all_of = actions_config.get("all_of", [])
235
+ any_of = actions_config.get("any_of", [])
236
+ none_of = actions_config.get("none_of", [])
237
+
238
+ # Collect all actions across the entire policy
239
+ policy_wide_actions: set[str] = set()
240
+ statements_by_action: dict[str, list[tuple[int, Statement]]] = {}
241
+
242
+ for idx, statement in enumerate(policy.statement or []):
243
+ if statement.effect != "Allow":
244
+ continue
245
+
246
+ statement_actions = statement.get_actions()
247
+ policy_wide_actions.update(statement_actions)
248
+
249
+ # Track which statements grant which actions
250
+ for action in statement_actions:
251
+ if action not in statements_by_action:
252
+ statements_by_action[action] = []
253
+ statements_by_action[action].append((idx, statement))
254
+
255
+ # Check all_of: ALL required actions must exist in policy
256
+ if all_of:
257
+ all_of_result = await self._check_all_of_policy_wide(
258
+ all_of,
259
+ policy_wide_actions,
260
+ statements_by_action,
261
+ requirement,
262
+ fetcher,
263
+ config,
264
+ )
265
+ issues.extend(all_of_result)
266
+
267
+ # Check any_of: At least ONE required action must exist in policy
268
+ if any_of:
269
+ any_of_result = await self._check_any_of_policy_wide(
270
+ any_of,
271
+ policy_wide_actions,
272
+ statements_by_action,
273
+ requirement,
274
+ fetcher,
275
+ config,
276
+ )
277
+ issues.extend(any_of_result)
278
+
279
+ # Check none_of: NONE of the forbidden actions should exist in policy
280
+ if none_of:
281
+ none_of_result = await self._check_none_of_policy_wide(
282
+ none_of,
283
+ policy_wide_actions,
284
+ statements_by_action,
285
+ requirement,
286
+ config,
287
+ fetcher,
288
+ )
289
+ issues.extend(none_of_result)
290
+
291
+ return issues
292
+
293
+ async def _check_all_of_policy_wide(
294
+ self,
295
+ all_of_actions: list[str],
296
+ policy_wide_actions: set[str],
297
+ statements_by_action: dict[str, list[tuple[int, Statement]]],
298
+ requirement: dict[str, Any],
166
299
  fetcher: AWSServiceFetcher,
167
300
  config: CheckConfig,
168
301
  ) -> list[ValidationIssue]:
169
- """Execute statement-level condition enforcement check."""
302
+ """
303
+ Check if ALL required actions exist anywhere in the policy.
304
+
305
+ For all_of, we report ONLY statements that contain ALL the required actions,
306
+ not statements that contain just some of them. This is useful for detecting
307
+ overly permissive individual statements.
308
+ """
170
309
  issues = []
171
310
 
172
- # Only check Allow statements
173
- if statement.effect != "Allow":
311
+ # First, check if ALL required actions exist somewhere in the policy
312
+ found_actions_mapping: dict[str, str] = {} # req_action -> matched_policy_action
313
+ missing_actions: list[str] = []
314
+
315
+ for req_action in all_of_actions:
316
+ action_found = False
317
+ for policy_action in policy_wide_actions:
318
+ if await self._action_matches(
319
+ policy_action, req_action, requirement.get("action_patterns", []), fetcher
320
+ ):
321
+ action_found = True
322
+ found_actions_mapping[req_action] = policy_action
323
+ break
324
+
325
+ if not action_found:
326
+ missing_actions.append(req_action)
327
+
328
+ # If not all actions exist in the policy, no issue
329
+ if missing_actions:
174
330
  return issues
175
331
 
176
- # Get action condition requirements from config
177
- action_condition_requirements = config.config.get("action_condition_requirements", [])
178
- if not action_condition_requirements:
332
+ # ALL required actions exist in the policy
333
+ # Now find statements that have ALL of them (not just some)
334
+ statements_with_all_actions: list[tuple[int, Statement, list[str]]] = []
335
+
336
+ # Check each statement to see if it contains ALL required actions
337
+ for statement in statements_by_action.get(list(found_actions_mapping.values())[0], []):
338
+ stmt_idx, stmt = statement
339
+ stmt_actions = stmt.get_actions()
340
+
341
+ # Check if this statement has ALL required actions
342
+ has_all_actions = True
343
+ matched_actions = []
344
+
345
+ for req_action in all_of_actions:
346
+ req_action_found = False
347
+ for stmt_action in stmt_actions:
348
+ if await self._action_matches(
349
+ stmt_action, req_action, requirement.get("action_patterns", []), fetcher
350
+ ):
351
+ req_action_found = True
352
+ if stmt_action not in matched_actions:
353
+ matched_actions.append(stmt_action)
354
+ break
355
+
356
+ if not req_action_found:
357
+ has_all_actions = False
358
+ break
359
+
360
+ if has_all_actions:
361
+ statements_with_all_actions.append((stmt_idx, stmt, matched_actions))
362
+
363
+ # Also check other statements not in the first action's list
364
+ checked_indices = {s[0] for s in statements_with_all_actions}
365
+ for policy_action, stmt_list in statements_by_action.items():
366
+ for stmt_idx, stmt in stmt_list:
367
+ if stmt_idx in checked_indices:
368
+ continue
369
+
370
+ stmt_actions = stmt.get_actions()
371
+
372
+ # Check if this statement has ALL required actions
373
+ has_all_actions = True
374
+ matched_actions = []
375
+
376
+ for req_action in all_of_actions:
377
+ req_action_found = False
378
+ for stmt_action in stmt_actions:
379
+ if await self._action_matches(
380
+ stmt_action, req_action, requirement.get("action_patterns", []), fetcher
381
+ ):
382
+ req_action_found = True
383
+ if stmt_action not in matched_actions:
384
+ matched_actions.append(stmt_action)
385
+ break
386
+
387
+ if not req_action_found:
388
+ has_all_actions = False
389
+ break
390
+
391
+ if has_all_actions:
392
+ statements_with_all_actions.append((stmt_idx, stmt, matched_actions))
393
+ checked_indices.add(stmt_idx)
394
+
395
+ # If no statements have ALL actions, no issue to report
396
+ if not statements_with_all_actions:
179
397
  return issues
180
398
 
181
- statement_actions = statement.get_actions()
399
+ # Report statements that have ALL the dangerous actions
400
+ return self._generate_policy_wide_issues(
401
+ statements_with_all_actions,
402
+ list(found_actions_mapping.values()),
403
+ requirement,
404
+ config,
405
+ "all_of",
406
+ )
182
407
 
183
- # Check each requirement rule
184
- for requirement in action_condition_requirements:
185
- # Check if this requirement applies to the statement's actions
186
- actions_match, matching_actions = await self._check_action_match(
187
- statement_actions, requirement, fetcher
188
- )
408
+ async def _check_any_of_policy_wide(
409
+ self,
410
+ any_of_actions: list[str],
411
+ policy_wide_actions: set[str],
412
+ statements_by_action: dict[str, list[tuple[int, Statement]]],
413
+ requirement: dict[str, Any],
414
+ fetcher: AWSServiceFetcher,
415
+ config: CheckConfig,
416
+ ) -> list[ValidationIssue]:
417
+ """Check if at least ONE required action exists anywhere in the policy."""
418
+ issues = []
419
+ found_actions: list[str] = []
420
+ statements_with_required_actions: list[tuple[int, Statement, list[str]]] = []
421
+
422
+ for req_action in any_of_actions:
423
+ for policy_action in policy_wide_actions:
424
+ if await self._action_matches(
425
+ policy_action, req_action, requirement.get("action_patterns", []), fetcher
426
+ ):
427
+ found_actions.append(policy_action)
428
+
429
+ # Track statements that have this action
430
+ if policy_action in statements_by_action:
431
+ for stmt_idx, stmt in statements_by_action[policy_action]:
432
+ existing = next(
433
+ (s for s in statements_with_required_actions if s[0] == stmt_idx),
434
+ None,
435
+ )
436
+ if existing:
437
+ if policy_action not in existing[2]:
438
+ existing[2].append(policy_action)
439
+ else:
440
+ statements_with_required_actions.append(
441
+ (stmt_idx, stmt, [policy_action])
442
+ )
443
+
444
+ # If no actions found, no issue
445
+ if not found_actions:
446
+ return issues
189
447
 
190
- if not actions_match:
191
- continue
448
+ # At least one action found - validate conditions
449
+ return self._generate_policy_wide_issues(
450
+ statements_with_required_actions,
451
+ found_actions,
452
+ requirement,
453
+ config,
454
+ "any_of",
455
+ )
192
456
 
193
- # Check if this is a none_of action rule (forbidden actions)
194
- actions_config = requirement.get("actions", [])
195
- if isinstance(actions_config, dict) and "none_of" in actions_config:
196
- # This is a forbidden action rule - flag it
197
- description = requirement.get("description", "These actions should not be used")
198
- # Use per-requirement severity if specified, else use global
199
- severity = requirement.get("severity", self.get_severity(config))
200
- matching_actions_formatted = ", ".join(f"`{a}`" for a in matching_actions)
201
- issues.append(
202
- ValidationIssue(
203
- severity=severity,
204
- statement_sid=statement.sid,
205
- statement_index=statement_idx,
206
- issue_type="forbidden_action_present",
207
- message=f"FORBIDDEN: Actions {matching_actions_formatted} should not be used. {description}",
208
- action=", ".join(matching_actions),
209
- suggestion=f"Remove these forbidden actions from the statement: {matching_actions_formatted}. {description}",
210
- line_number=statement.line_number,
211
- )
457
+ async def _check_none_of_policy_wide(
458
+ self,
459
+ none_of_actions: list[str],
460
+ policy_wide_actions: set[str],
461
+ statements_by_action: dict[str, list[tuple[int, Statement]]],
462
+ requirement: dict[str, Any],
463
+ config: CheckConfig,
464
+ fetcher: AWSServiceFetcher,
465
+ ) -> list[ValidationIssue]:
466
+ """Check if any forbidden actions exist in the policy."""
467
+ issues = []
468
+ forbidden_found: list[str] = []
469
+ statements_with_forbidden: list[tuple[int, Statement, list[str]]] = []
470
+
471
+ for forbidden_action in none_of_actions:
472
+ for policy_action in policy_wide_actions:
473
+ if await self._action_matches(
474
+ policy_action, forbidden_action, requirement.get("action_patterns", []), fetcher
475
+ ):
476
+ forbidden_found.append(policy_action)
477
+
478
+ # Track statements with forbidden actions
479
+ if policy_action in statements_by_action:
480
+ for stmt_idx, stmt in statements_by_action[policy_action]:
481
+ existing = next(
482
+ (s for s in statements_with_forbidden if s[0] == stmt_idx), None
483
+ )
484
+ if existing:
485
+ if policy_action not in existing[2]:
486
+ existing[2].append(policy_action)
487
+ else:
488
+ statements_with_forbidden.append((stmt_idx, stmt, [policy_action]))
489
+
490
+ # If forbidden actions found, create issues
491
+ if not forbidden_found:
492
+ return issues
493
+
494
+ description = requirement.get("description", "These actions should not be used")
495
+ severity = requirement.get("severity", self.get_severity(config))
496
+
497
+ for stmt_idx, stmt, actions in statements_with_forbidden:
498
+ actions_formatted = ", ".join(f"`{a}`" for a in actions)
499
+ statement_refs = [
500
+ f"Statement #{idx + 1}{' (SID: ' + s.sid + ')' if s.sid else ''}"
501
+ for idx, s, _ in statements_with_forbidden
502
+ ]
503
+
504
+ issues.append(
505
+ ValidationIssue(
506
+ severity=severity,
507
+ statement_sid=stmt.sid,
508
+ statement_index=stmt_idx,
509
+ issue_type="forbidden_action",
510
+ message=f"Forbidden actions {actions_formatted} found. {description}",
511
+ action=", ".join(actions),
512
+ suggestion=f"Remove these forbidden actions. Found in: {', '.join(statement_refs)}. {description}",
513
+ line_number=stmt.line_number,
212
514
  )
213
- continue
515
+ )
214
516
 
215
- # Actions match - now validate required conditions
216
- required_conditions_config = requirement.get("required_conditions", [])
217
- if not required_conditions_config:
218
- continue
517
+ return issues
219
518
 
220
- # Check if conditions are in all_of/any_of/none_of format or simple list
519
+ def _generate_policy_wide_issues(
520
+ self,
521
+ statements_with_actions: list[tuple[int, Statement, list[str]]],
522
+ found_actions: list[str],
523
+ requirement: dict[str, Any],
524
+ config: CheckConfig,
525
+ operator_type: str,
526
+ ) -> list[ValidationIssue]:
527
+ """Generate validation issues for policy-wide checks."""
528
+ issues = []
529
+ required_conditions_config = requirement.get("required_conditions", [])
530
+ description = requirement.get("description", "")
531
+ severity = requirement.get("severity", self.get_severity(config))
532
+
533
+ if not required_conditions_config:
534
+ # No conditions specified, just report that actions were found
535
+ all_actions_formatted = ", ".join(f"`{a}`" for a in sorted(set(found_actions)))
536
+ statement_refs = [
537
+ f"Statement #{idx + 1}{' (SID: ' + stmt.sid + ')' if stmt.sid else ''}"
538
+ for idx, stmt, _ in statements_with_actions
539
+ ]
540
+
541
+ first_idx, first_stmt, _ = statements_with_actions[0]
542
+ issues.append(
543
+ ValidationIssue(
544
+ severity=severity,
545
+ statement_sid=first_stmt.sid,
546
+ statement_index=first_idx,
547
+ issue_type="action_detected",
548
+ message=f"Actions {all_actions_formatted} found across {len(statements_with_actions)} statement(s) ({operator_type}). {description}",
549
+ action=", ".join(sorted(set(found_actions))),
550
+ suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
551
+ line_number=first_stmt.line_number,
552
+ )
553
+ )
554
+ return issues
555
+
556
+ # Validate conditions for each statement
557
+ for idx, statement, matching_actions in statements_with_actions:
221
558
  condition_issues = self._validate_conditions(
222
559
  statement,
223
- statement_idx,
560
+ idx,
224
561
  required_conditions_config,
225
562
  matching_actions,
226
563
  config,
227
- requirement, # Pass the full requirement for severity override
564
+ requirement,
228
565
  )
229
566
 
567
+ # Add context
568
+ for issue in condition_issues:
569
+ issue.suggestion = (
570
+ f"{issue.suggestion}\n\n"
571
+ f"Note: Found {len(statements_with_actions)} statement(s) with these actions in the policy ({operator_type})."
572
+ )
573
+
230
574
  issues.extend(condition_issues)
231
575
 
232
576
  return issues
233
577
 
234
- async def execute_policy(
578
+ async def _check_per_statement(
235
579
  self,
236
580
  policy: "IAMPolicy",
237
- policy_file: str,
581
+ requirement: dict[str, Any],
238
582
  fetcher: AWSServiceFetcher,
239
583
  config: CheckConfig,
240
- **kwargs,
241
584
  ) -> list[ValidationIssue]:
242
585
  """
243
- Execute policy-level condition enforcement check.
244
-
245
- This method scans the entire policy and enforces that ALL statements granting
246
- certain actions must have specific conditions. This is useful for ensuring
247
- consistent security controls across the entire policy.
248
-
249
- Example use case:
250
- - "If ANY statement in the policy grants iam:CreateUser, iam:AttachUserPolicy,
251
- or iam:PutUserPolicy, then ALL such statements must have MFA condition."
252
-
253
- Args:
254
- policy: The complete IAM policy to check
255
- policy_file: Path to the policy file (for context/reporting)
256
- fetcher: AWS service fetcher for validation against AWS APIs
257
- config: Configuration for this check instance
258
- **kwargs: Additional context (policy_type, etc.)
586
+ Check each statement individually for matching actions (simple list format).
259
587
 
260
- Returns:
261
- List of ValidationIssue objects found by this check
588
+ Used when actions are specified as a simple list (not using all_of/any_of/none_of).
262
589
  """
263
- del policy_file, kwargs # Not used in current implementation
264
590
  issues = []
591
+ matching_statements: list[tuple[int, Statement, list[str]]] = []
265
592
 
266
- # Get policy-level requirements from config
267
- policy_level_requirements = config.config.get("policy_level_requirements", [])
268
- if not policy_level_requirements:
269
- return issues
270
-
271
- # Process each policy-level requirement
272
- for requirement in policy_level_requirements:
273
- # Collect all statements that match the action criteria
274
- matching_statements: list[tuple[int, Statement, list[str]]] = []
275
-
276
- for idx, statement in enumerate(policy.statement or []):
277
- # Only check Allow statements
278
- if statement.effect != "Allow":
279
- continue
593
+ for idx, statement in enumerate(policy.statement or []):
594
+ # Only check Allow statements
595
+ if statement.effect != "Allow":
596
+ continue
280
597
 
281
- statement_actions = statement.get_actions()
598
+ statement_actions = statement.get_actions()
282
599
 
283
- # Check if this statement matches the action requirement
284
- actions_match, matching_actions = await self._check_action_match(
285
- statement_actions, requirement, fetcher
286
- )
600
+ # Check if this statement matches the action requirement
601
+ actions_match, matching_actions = await self._check_action_match(
602
+ statement_actions, requirement, fetcher
603
+ )
287
604
 
288
- if actions_match and matching_actions:
289
- matching_statements.append((idx, statement, matching_actions))
605
+ if actions_match and matching_actions:
606
+ matching_statements.append((idx, statement, matching_actions))
290
607
 
291
- # If no statements match, skip this requirement
292
- if not matching_statements:
293
- continue
608
+ # If no statements match, skip this requirement
609
+ if not matching_statements:
610
+ return issues
294
611
 
295
- # Now validate that ALL matching statements have the required conditions
296
- required_conditions_config = requirement.get("required_conditions", [])
297
- if not required_conditions_config:
298
- # No conditions specified, just report that actions were found
299
- description = requirement.get("description", "")
300
- severity = requirement.get("severity", self.get_severity(config))
301
-
302
- # Create a summary issue for all matching statements
303
- all_actions = set()
304
- statement_refs = []
305
- for idx, stmt, actions in matching_statements:
306
- all_actions.update(actions)
307
- sid_info = f" (SID: {stmt.sid})" if stmt.sid else ""
308
- statement_refs.append(f"Statement #{idx + 1}{sid_info}")
309
-
310
- # Use the first matching statement's index for the issue
311
- first_idx, first_stmt, _ = matching_statements[0]
312
- all_actions_formatted = ", ".join(f"`{a}`" for a in sorted(all_actions))
313
-
314
- issues.append(
315
- ValidationIssue(
316
- severity=severity,
317
- statement_sid=first_stmt.sid,
318
- statement_index=first_idx,
319
- issue_type="policy_level_action_detected",
320
- message=f"POLICY-LEVEL: Actions {all_actions_formatted} found in {len(matching_statements)} statement(s). {description}",
321
- action=", ".join(sorted(all_actions)),
322
- suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
323
- line_number=first_stmt.line_number,
324
- )
612
+ # Now validate that ALL matching statements have the required conditions
613
+ required_conditions_config = requirement.get("required_conditions", [])
614
+ if not required_conditions_config:
615
+ # No conditions specified, just report that actions were found
616
+ description = requirement.get("description", "")
617
+ severity = requirement.get("severity", self.get_severity(config))
618
+
619
+ # Create a summary issue for all matching statements
620
+ all_actions = set()
621
+ statement_refs = []
622
+ for idx, stmt, actions in matching_statements:
623
+ all_actions.update(actions)
624
+ sid_info = f" (SID: {stmt.sid})" if stmt.sid else ""
625
+ statement_refs.append(f"Statement #{idx + 1}{sid_info}")
626
+
627
+ # Use the first matching statement's index for the issue
628
+ first_idx, first_stmt, _ = matching_statements[0]
629
+ all_actions_formatted = ", ".join(f"`{a}`" for a in sorted(all_actions))
630
+
631
+ issues.append(
632
+ ValidationIssue(
633
+ severity=severity,
634
+ statement_sid=first_stmt.sid,
635
+ statement_index=first_idx,
636
+ issue_type="action_detected",
637
+ message=f"Actions {all_actions_formatted} found in {len(matching_statements)} statement(s). {description}",
638
+ action=", ".join(sorted(all_actions)),
639
+ suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
640
+ line_number=first_stmt.line_number,
325
641
  )
326
- continue
642
+ )
643
+ return issues
327
644
 
328
- # Validate conditions for each matching statement
329
- for idx, statement, matching_actions in matching_statements:
330
- condition_issues = self._validate_conditions(
331
- statement,
332
- idx,
333
- required_conditions_config,
334
- matching_actions,
335
- config,
336
- requirement,
337
- )
645
+ # Validate conditions for each matching statement
646
+ for idx, statement, matching_actions in matching_statements:
647
+ condition_issues = self._validate_conditions(
648
+ statement,
649
+ idx,
650
+ required_conditions_config,
651
+ matching_actions,
652
+ config,
653
+ requirement,
654
+ )
338
655
 
339
- # Add policy-level context to each issue
340
- for issue in condition_issues:
341
- # Modify the message to indicate this is part of policy-level enforcement
342
- issue.message = f"POLICY-LEVEL: {issue.message}"
343
- issue.suggestion = (
344
- f"{issue.suggestion}\n\n"
345
- f"Note: This is enforced at the policy level. "
346
- f"Found {len(matching_statements)} statement(s) with these actions in the policy."
347
- )
656
+ # Add context to each issue
657
+ for issue in condition_issues:
658
+ issue.suggestion = (
659
+ f"{issue.suggestion}\n\n"
660
+ f"Note: Found {len(matching_statements)} statement(s) with these actions in the policy."
661
+ )
348
662
 
349
- issues.extend(condition_issues)
663
+ issues.extend(condition_issues)
350
664
 
351
665
  return issues
352
666
 
@@ -815,7 +1129,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
815
1129
  Returns:
816
1130
  Tuple of (suggestion_text, example_code)
817
1131
  """
818
- suggestion = description if description else f"Add condition: {condition_key}"
1132
+ suggestion = description if description else f"Add condition: `{condition_key}`"
819
1133
 
820
1134
  # Build example based on condition key type
821
1135
  if example: