iam-policy-validator 1.7.1__py3-none-any.whl → 1.8.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 (51) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/METADATA +22 -7
  2. iam_policy_validator-1.8.0.dist-info/RECORD +87 -0
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/__init__.py +5 -3
  5. iam_validator/checks/action_condition_enforcement.py +81 -36
  6. iam_validator/checks/action_resource_matching.py +75 -37
  7. iam_validator/checks/action_validation.py +1 -1
  8. iam_validator/checks/condition_key_validation.py +7 -7
  9. iam_validator/checks/condition_type_mismatch.py +10 -8
  10. iam_validator/checks/full_wildcard.py +2 -8
  11. iam_validator/checks/mfa_condition_check.py +8 -8
  12. iam_validator/checks/policy_structure.py +577 -0
  13. iam_validator/checks/policy_type_validation.py +48 -32
  14. iam_validator/checks/principal_validation.py +86 -150
  15. iam_validator/checks/resource_validation.py +8 -8
  16. iam_validator/checks/sensitive_action.py +9 -11
  17. iam_validator/checks/service_wildcard.py +4 -10
  18. iam_validator/checks/set_operator_validation.py +11 -11
  19. iam_validator/checks/sid_uniqueness.py +8 -4
  20. iam_validator/checks/trust_policy_validation.py +512 -0
  21. iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
  22. iam_validator/checks/utils/wildcard_expansion.py +1 -1
  23. iam_validator/checks/wildcard_action.py +5 -9
  24. iam_validator/checks/wildcard_resource.py +5 -9
  25. iam_validator/commands/validate.py +8 -14
  26. iam_validator/core/__init__.py +1 -2
  27. iam_validator/core/access_analyzer.py +1 -1
  28. iam_validator/core/access_analyzer_report.py +2 -2
  29. iam_validator/core/aws_fetcher.py +159 -64
  30. iam_validator/core/check_registry.py +83 -79
  31. iam_validator/core/config/condition_requirements.py +69 -17
  32. iam_validator/core/config/config_loader.py +1 -2
  33. iam_validator/core/config/defaults.py +74 -59
  34. iam_validator/core/config/service_principals.py +40 -3
  35. iam_validator/core/constants.py +57 -0
  36. iam_validator/core/formatters/console.py +10 -1
  37. iam_validator/core/formatters/csv.py +2 -1
  38. iam_validator/core/formatters/enhanced.py +42 -8
  39. iam_validator/core/formatters/markdown.py +2 -1
  40. iam_validator/core/ignore_patterns.py +297 -0
  41. iam_validator/core/models.py +35 -10
  42. iam_validator/core/policy_checks.py +34 -474
  43. iam_validator/core/policy_loader.py +98 -18
  44. iam_validator/core/report.py +65 -24
  45. iam_validator/integrations/github_integration.py +4 -5
  46. iam_validator/utils/__init__.py +4 -0
  47. iam_validator/utils/terminal.py +22 -0
  48. iam_policy_validator-1.7.1.dist-info/RECORD +0 -83
  49. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
  50. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
  51. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -21,6 +21,8 @@ Example:
21
21
  This check will report: s3:GetObject requires arn:aws:s3:::mybucket/*
22
22
  """
23
23
 
24
+ import re
25
+
24
26
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
25
27
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
26
28
  from iam_validator.core.models import Statement, ValidationIssue
@@ -86,6 +88,10 @@ class ActionResourceMatchingCheck(PolicyCheck):
86
88
  statement_sid = statement.sid
87
89
  line_number = statement.line_number
88
90
 
91
+ # Skip if no resources to validate (e.g., trust policies don't have Resource field)
92
+ if not resources:
93
+ return issues
94
+
89
95
  # Skip if we have a wildcard resource (handled by other checks)
90
96
  if "*" in resources:
91
97
  return issues
@@ -231,18 +237,23 @@ class ActionResourceMatchingCheck(PolicyCheck):
231
237
  if reason:
232
238
  message = reason
233
239
  elif all_required_formats and len(all_required_formats) > 1:
234
- types = ", ".join(f["type"] for f in all_required_formats)
240
+ types = ", ".join(f"`{f['type']}`" for f in all_required_formats)
235
241
  message = (
236
- f"No resources match for action '{action}'. This action requires one of: {types}"
242
+ f"No resources match for action `{action}`. This action requires one of: {types}"
237
243
  )
238
244
  else:
239
245
  message = (
240
- f"No resources match for action '{action}'. "
241
- f"This action requires resource type: {required_type}"
246
+ f"No resources match for action `{action}`. "
247
+ f"This action requires resource type: `{required_type}`"
242
248
  )
243
249
 
244
250
  # Build suggestion with examples
245
- suggestion = self._get_suggestion(action, required_format, provided_resources)
251
+ suggestion = self._get_suggestion(
252
+ action=action,
253
+ required_format=required_format,
254
+ provided_resources=provided_resources,
255
+ all_required_formats=all_required_formats,
256
+ )
246
257
 
247
258
  return ValidationIssue(
248
259
  severity=self.get_severity(config),
@@ -265,6 +276,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
265
276
  action: str,
266
277
  required_format: str,
267
278
  provided_resources: list[str],
279
+ all_required_formats: list[dict] | None = None,
268
280
  ) -> str:
269
281
  """
270
282
  Generate helpful suggestion for fixing the mismatch.
@@ -281,44 +293,75 @@ class ActionResourceMatchingCheck(PolicyCheck):
281
293
  # Special case: Wildcard resource
282
294
  if required_format == "*":
283
295
  return (
284
- f'Action {action} can only use Resource: "*" (wildcard).\n'
296
+ f'Action `{action}` can only use Resource: `"*"` (wildcard).\n'
285
297
  f" This action does not support resource-level permissions.\n"
286
- f' Example: "Resource": "*"'
298
+ f' Example: "Resource": `"*"`'
287
299
  )
288
300
 
289
- # Extract resource type from ARN pattern
290
- # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
291
- # Examples:
292
- # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
293
- # arn:${Partition}:iam::${Account}:user/${UserName} -> user
294
- resource_type = self._extract_resource_type_from_pattern(required_format)
295
-
296
- # Build service-specific suggestion
301
+ # Build service-specific suggestion with proper markdown formatting
297
302
  suggestion_parts = []
298
303
 
299
- # Add action description
300
- suggestion_parts.append(f"Action {action} requires {resource_type} resource type.")
304
+ # If multiple resource types are valid, show all of them
305
+ if all_required_formats and len(all_required_formats) > 1:
306
+ resource_types = [fmt["type"] for fmt in all_required_formats]
307
+ suggestion_parts.append(
308
+ f"Action `{action}` requires one of these resource types: {', '.join(f'`{t}`' for t in resource_types)}"
309
+ )
310
+ suggestion_parts.append("")
301
311
 
302
- # Add expected format
303
- suggestion_parts.append(f" Expected format: {required_format}")
312
+ # Show format and example for each resource type
313
+ for fmt in all_required_formats:
314
+ resource_type = fmt["type"]
315
+ arn_format = fmt["format"]
304
316
 
305
- # Add practical example based on the pattern
306
- example = self._generate_example_arn(required_format)
307
- if example:
308
- suggestion_parts.append(f" Example: {example}")
317
+ suggestion_parts.append(
318
+ f"**Option {all_required_formats.index(fmt) + 1}: `{resource_type}` resource**"
319
+ )
320
+ suggestion_parts.append("```")
321
+ suggestion_parts.append(arn_format)
322
+ suggestion_parts.append("```")
309
323
 
310
- # Add helpful context for common patterns
311
- context = self._get_resource_context(action_name, resource_type, required_format)
312
- if context:
313
- suggestion_parts.append(f" {context}")
324
+ # Add practical example
325
+ example = self._generate_example_arn(arn_format)
326
+ if example:
327
+ suggestion_parts.append(f"Example: `{example}`")
314
328
 
315
- suggestion = "\n".join(suggestion_parts)
329
+ suggestion_parts.append("")
330
+ else:
331
+ # Single resource type - show detailed info
332
+ # Extract resource type from ARN pattern
333
+ # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
334
+ # Examples:
335
+ # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
336
+ # arn:${Partition}:iam::${Account}:user/${UserName} -> user
337
+ resource_type = self._extract_resource_type_from_pattern(required_format)
338
+
339
+ # Add action description
340
+ suggestion_parts.append(f"Action `{action}` requires `{resource_type}` resource type.")
341
+ suggestion_parts.append("")
342
+
343
+ # Add expected format in code block
344
+ suggestion_parts.append("**Expected format:**")
345
+ suggestion_parts.append(f"```\n{required_format}\n```")
346
+
347
+ # Add practical example based on the pattern
348
+ example = self._generate_example_arn(required_format)
349
+ if example:
350
+ suggestion_parts.append("**Example:**")
351
+ suggestion_parts.append(f"```\n{example}\n```")
352
+
353
+ # Add helpful context for common patterns
354
+ context = self._get_resource_context(action_name, resource_type, required_format)
355
+ if context:
356
+ suggestion_parts.append(f"**Note:** {context}")
316
357
 
317
358
  # Add current resources to help user understand the mismatch
318
359
  if provided_resources and len(provided_resources) <= 3:
319
- current = ", ".join(provided_resources)
320
- suggestion += f"\n Current resources: {current}"
360
+ suggestion_parts.append("**Current resources:**")
361
+ for resource in provided_resources:
362
+ suggestion_parts.append(f"- `{resource}`")
321
363
 
364
+ suggestion = "\n".join(suggestion_parts)
322
365
  return suggestion
323
366
 
324
367
  def _extract_resource_type_from_pattern(self, pattern: str) -> str:
@@ -340,17 +383,14 @@ class ActionResourceMatchingCheck(PolicyCheck):
340
383
 
341
384
  # Extract resource type (part before / or entire string)
342
385
  if "/" in resource_part:
343
- resource_type = resource_part.split("/")[0]
386
+ resource_type = resource_part.split("/", maxsplit=1)[0]
344
387
  elif ":" in resource_part:
345
- resource_type = resource_part.split(":")[0]
388
+ resource_type = resource_part.split(":", maxsplit=1)[0]
346
389
  else:
347
390
  resource_type = resource_part
348
391
 
349
392
  # Remove template variables like ${...}
350
- import re
351
-
352
393
  resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
353
-
354
394
  return resource_type.strip() or "resource"
355
395
 
356
396
  def _generate_example_arn(self, pattern: str) -> str:
@@ -359,8 +399,6 @@ class ActionResourceMatchingCheck(PolicyCheck):
359
399
 
360
400
  Converts AWS template variables to realistic examples.
361
401
  """
362
- import re
363
-
364
402
  example = pattern
365
403
 
366
404
  # Common substitutions
@@ -63,7 +63,7 @@ class ActionValidationCheck(PolicyCheck):
63
63
  statement_sid=statement_sid,
64
64
  statement_index=statement_idx,
65
65
  issue_type="invalid_action",
66
- message=error_msg or f"Invalid action: {action}",
66
+ message=error_msg or f"Invalid action: `{action}`",
67
67
  action=action,
68
68
  line_number=line_number,
69
69
  )
@@ -52,26 +52,26 @@ class ConditionKeyValidationCheck(PolicyCheck):
52
52
  continue
53
53
 
54
54
  # Validate against action and resource types
55
- is_valid, error_msg, warning_msg = await fetcher.validate_condition_key(
56
- action, condition_key, resources
57
- )
55
+ result = await fetcher.validate_condition_key(action, condition_key, resources)
58
56
 
59
- if not is_valid:
57
+ if not result.is_valid:
60
58
  issues.append(
61
59
  ValidationIssue(
62
60
  severity=self.get_severity(config),
63
61
  statement_sid=statement_sid,
64
62
  statement_index=statement_idx,
65
63
  issue_type="invalid_condition_key",
66
- message=error_msg or f"Invalid condition key: {condition_key}",
64
+ message=result.error_message
65
+ or f"Invalid condition key: `{condition_key}`",
67
66
  action=action,
68
67
  condition_key=condition_key,
69
68
  line_number=line_number,
69
+ suggestion=result.suggestion,
70
70
  )
71
71
  )
72
72
  # Only report once per condition key (not per action)
73
73
  break
74
- elif warning_msg and warn_on_global_keys:
74
+ elif result.warning_message and warn_on_global_keys:
75
75
  # Add warning for global condition keys with action-specific keys
76
76
  # Only if warn_on_global_condition_keys is enabled
77
77
  issues.append(
@@ -80,7 +80,7 @@ class ConditionKeyValidationCheck(PolicyCheck):
80
80
  statement_sid=statement_sid,
81
81
  statement_index=statement_idx,
82
82
  issue_type="global_condition_key_with_action_specific",
83
- message=warning_msg,
83
+ message=result.warning_message,
84
84
  action=action,
85
85
  condition_key=condition_key,
86
86
  line_number=line_number,
@@ -72,7 +72,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
72
72
  # Check each condition operator and its keys/values
73
73
  for operator, conditions in statement.condition.items():
74
74
  # Normalize the operator and get its expected type
75
- base_operator, operator_type, set_prefix = normalize_operator(operator)
75
+ base_operator, operator_type, _set_prefix = normalize_operator(operator)
76
76
 
77
77
  if operator_type is None:
78
78
  # Unknown operator - this will be caught by another check
@@ -107,8 +107,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
107
107
  ValidationIssue(
108
108
  severity="warning",
109
109
  message=(
110
- f"Type mismatch (usable but not recommended): Operator '{operator}' expects "
111
- f"{operator_type} values, but condition key '{condition_key}' is type {key_type}. "
110
+ f"Type mismatch (usable but not recommended): Operator `{operator}` expects "
111
+ f"`{operator_type}` values, but condition key `{condition_key}` is type `{key_type}`. "
112
112
  f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
113
113
  ),
114
114
  statement_sid=statement_sid,
@@ -123,8 +123,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
123
123
  ValidationIssue(
124
124
  severity=self.get_severity(config),
125
125
  message=(
126
- f"Type mismatch: Operator '{operator}' expects {operator_type} values, "
127
- f"but condition key '{condition_key}' is type {key_type}."
126
+ f"Type mismatch: Operator `{operator}` expects `{operator_type}` values, "
127
+ f"but condition key `{condition_key}` is type `{key_type}`."
128
128
  ),
129
129
  statement_sid=statement_sid,
130
130
  statement_index=statement_idx,
@@ -141,7 +141,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
141
141
  ValidationIssue(
142
142
  severity=self.get_severity(config),
143
143
  message=(
144
- f"Invalid value format for condition key '{condition_key}': {error_msg}"
144
+ f"Invalid value format for condition key `{condition_key}`: {error_msg}"
145
145
  ),
146
146
  statement_sid=statement_sid,
147
147
  statement_index=statement_idx,
@@ -172,7 +172,9 @@ class ConditionTypeMismatchCheck(PolicyCheck):
172
172
  Returns:
173
173
  Type string or None if not found
174
174
  """
175
- from iam_validator.core.config.aws_global_conditions import get_global_conditions
175
+ from iam_validator.core.config.aws_global_conditions import ( # pylint: disable=import-outside-toplevel
176
+ get_global_conditions,
177
+ )
176
178
 
177
179
  # Check if it's a global condition key
178
180
  global_conditions = get_global_conditions()
@@ -227,7 +229,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
227
229
  if condition_key_obj.types:
228
230
  return condition_key_obj.types[0]
229
231
 
230
- except Exception:
232
+ except Exception: # pylint: disable=broad-exception-caught
231
233
  # If we can't look up the action, skip it
232
234
  continue
233
235
 
@@ -43,19 +43,12 @@ class FullWildcardCheck(PolicyCheck):
43
43
  "message",
44
44
  "Statement allows all actions on all resources - CRITICAL SECURITY RISK",
45
45
  )
46
- suggestion_text = config.config.get(
46
+ suggestion = config.config.get(
47
47
  "suggestion",
48
48
  "This grants full administrative access. Replace both wildcards with specific actions and resources to follow least-privilege principle",
49
49
  )
50
50
  example = config.config.get("example", "")
51
51
 
52
- # Combine suggestion + example
53
- suggestion = (
54
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
55
- if example
56
- else suggestion_text
57
- )
58
-
59
52
  issues.append(
60
53
  ValidationIssue(
61
54
  severity=self.get_severity(config),
@@ -64,6 +57,7 @@ class FullWildcardCheck(PolicyCheck):
64
57
  issue_type="security_risk",
65
58
  message=message,
66
59
  suggestion=suggestion,
60
+ example=example if example else None,
67
61
  line_number=statement.line_number,
68
62
  )
69
63
  )
@@ -70,11 +70,11 @@ class MFAConditionCheck(PolicyCheck):
70
70
  ValidationIssue(
71
71
  severity=self.get_severity(config),
72
72
  message=(
73
- "Dangerous MFA condition pattern detected. "
74
- 'Using {"Bool": {"aws:MultiFactorAuthPresent": "false"}} does not enforce MFA '
75
- "because aws:MultiFactorAuthPresent may not exist in the request context. "
76
- 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement, '
77
- "or use BoolIfExists in a Deny statement."
73
+ "**Dangerous MFA condition pattern detected.** "
74
+ 'Using `{"Bool": {"aws:MultiFactorAuthPresent": "false"}}` does not enforce MFA '
75
+ "because `aws:MultiFactorAuthPresent` may not exist in the request context. "
76
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement, '
77
+ "or use `BoolIfExists` in a Deny statement."
78
78
  ),
79
79
  statement_sid=statement_sid,
80
80
  statement_index=statement_idx,
@@ -97,10 +97,10 @@ class MFAConditionCheck(PolicyCheck):
97
97
  ValidationIssue(
98
98
  severity=self.get_severity(config),
99
99
  message=(
100
- "Dangerous MFA condition pattern detected. "
101
- 'Using {"Null": {"aws:MultiFactorAuthPresent": "false"}} only checks if the key exists, '
100
+ "**Dangerous MFA condition pattern detected.** "
101
+ 'Using `{"Null": {"aws:MultiFactorAuthPresent": "false"}}` only checks if the key exists, '
102
102
  "not whether MFA was actually used. This does not enforce MFA. "
103
- 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement instead.'
103
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement instead.'
104
104
  ),
105
105
  statement_sid=statement_sid,
106
106
  statement_index=statement_idx,