iam-policy-validator 1.4.0__py3-none-any.whl → 1.5.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 (33) hide show
  1. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +18 -19
  2. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/RECORD +31 -20
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +13 -3
  5. iam_validator/checks/action_condition_enforcement.py +1 -6
  6. iam_validator/checks/condition_key_validation.py +21 -1
  7. iam_validator/checks/full_wildcard.py +67 -0
  8. iam_validator/checks/principal_validation.py +497 -3
  9. iam_validator/checks/sensitive_action.py +178 -0
  10. iam_validator/checks/service_wildcard.py +105 -0
  11. iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
  12. iam_validator/checks/wildcard_action.py +62 -0
  13. iam_validator/checks/wildcard_resource.py +131 -0
  14. iam_validator/commands/download_services.py +3 -8
  15. iam_validator/commands/validate.py +28 -2
  16. iam_validator/core/aws_fetcher.py +25 -12
  17. iam_validator/core/check_registry.py +15 -21
  18. iam_validator/core/config/__init__.py +83 -0
  19. iam_validator/core/config/aws_api.py +35 -0
  20. iam_validator/core/config/condition_requirements.py +535 -0
  21. iam_validator/core/config/defaults.py +390 -0
  22. iam_validator/core/config/principal_requirements.py +421 -0
  23. iam_validator/core/config/sensitive_actions.py +133 -0
  24. iam_validator/core/config/service_principals.py +95 -0
  25. iam_validator/core/config/wildcards.py +124 -0
  26. iam_validator/core/config_loader.py +29 -9
  27. iam_validator/core/formatters/enhanced.py +11 -5
  28. iam_validator/core/formatters/sarif.py +78 -14
  29. iam_validator/checks/security_best_practices.py +0 -536
  30. iam_validator/core/defaults.py +0 -393
  31. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
  32. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
  33. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,124 @@
1
+ """
2
+ Default wildcard configurations for security best practices checks.
3
+
4
+ These wildcards define which actions are considered "safe" to use with
5
+ Resource: "*" (e.g., read-only describe operations).
6
+
7
+ Using Python tuples instead of YAML lists provides:
8
+ - Zero parsing overhead
9
+ - Immutable by default (tuples)
10
+ - Better performance
11
+ - Easy PyPI packaging
12
+ """
13
+
14
+ from typing import Final
15
+
16
+ # ============================================================================
17
+ # Allowed Wildcards for Resource: "*"
18
+ # ============================================================================
19
+ # These action patterns are considered safe to use with wildcard resources
20
+ # They are typically read-only operations that need broad resource access
21
+
22
+ DEFAULT_ALLOWED_WILDCARDS: Final[tuple[str, ...]] = (
23
+ # Auto Scaling
24
+ "autoscaling:Describe*",
25
+ # CloudWatch
26
+ "cloudwatch:Describe*",
27
+ "cloudwatch:Get*",
28
+ "cloudwatch:List*",
29
+ # DynamoDB
30
+ "dynamodb:Describe*",
31
+ # EC2
32
+ "ec2:Describe*",
33
+ # Elastic Load Balancing
34
+ "elasticloadbalancing:Describe*",
35
+ # IAM (non-sensitive read operations)
36
+ "iam:Get*",
37
+ "iam:List*",
38
+ # KMS
39
+ "kms:Describe*",
40
+ # Lambda
41
+ "lambda:Get*",
42
+ "lambda:List*",
43
+ # CloudWatch Logs
44
+ "logs:Describe*",
45
+ "logs:Filter*",
46
+ "logs:Get*",
47
+ # RDS
48
+ "rds:Describe*",
49
+ # Route53
50
+ "route53:Get*",
51
+ "route53:List*",
52
+ # S3 (safe read operations only)
53
+ "s3:Describe*",
54
+ "s3:GetBucket*",
55
+ "s3:GetM*",
56
+ "s3:List*",
57
+ # SQS
58
+ "sqs:Get*",
59
+ "sqs:List*",
60
+ # API Gateway
61
+ "apigateway:GET",
62
+ )
63
+
64
+ # ============================================================================
65
+ # Service-Level Wildcards (Allowed Services)
66
+ # ============================================================================
67
+ # Services that are allowed to use service-level wildcards like "logs:*"
68
+ # These are typically low-risk monitoring/logging services
69
+
70
+ DEFAULT_SERVICE_WILDCARDS: Final[tuple[str, ...]] = (
71
+ "logs",
72
+ "cloudwatch",
73
+ "xray",
74
+ )
75
+
76
+
77
+ def get_allowed_wildcards() -> tuple[str, ...]:
78
+ """
79
+ Get tuple of allowed wildcard action patterns.
80
+
81
+ Returns:
82
+ Tuple of action patterns that are safe to use with Resource: "*"
83
+ """
84
+ return DEFAULT_ALLOWED_WILDCARDS
85
+
86
+
87
+ def get_allowed_service_wildcards() -> tuple[str, ...]:
88
+ """
89
+ Get tuple of services allowed to use service-level wildcards.
90
+
91
+ Returns:
92
+ Tuple of service names (e.g., "logs", "cloudwatch")
93
+ """
94
+ return DEFAULT_SERVICE_WILDCARDS
95
+
96
+
97
+ def is_allowed_wildcard(pattern: str) -> bool:
98
+ """
99
+ Check if a wildcard pattern is in the allowed list.
100
+
101
+ Args:
102
+ pattern: Action pattern to check (e.g., "s3:List*")
103
+
104
+ Returns:
105
+ True if pattern is in allowed wildcards
106
+
107
+ Performance: O(n) but typically small list (~25 items)
108
+ """
109
+ return pattern in DEFAULT_ALLOWED_WILDCARDS
110
+
111
+
112
+ def is_allowed_service_wildcard(service: str) -> bool:
113
+ """
114
+ Check if a service is allowed to use service-level wildcards.
115
+
116
+ Args:
117
+ service: Service name (e.g., "logs", "s3")
118
+
119
+ Returns:
120
+ True if service is in allowed list
121
+
122
+ Performance: O(n) but very small list (~3 items)
123
+ """
124
+ return service in DEFAULT_SERVICE_WILDCARDS
@@ -15,7 +15,7 @@ from typing import Any
15
15
  import yaml
16
16
 
17
17
  from iam_validator.core.check_registry import CheckConfig, CheckRegistry, PolicyCheck
18
- from iam_validator.core.defaults import get_default_config
18
+ from iam_validator.core.config.defaults import get_default_config
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -68,18 +68,38 @@ class ValidatorConfig:
68
68
  self.config_dict = config_dict or {}
69
69
 
70
70
  # Support both nested and flat structure
71
- # New flat structure: each check is a top-level key ending with "_check"
72
- # Old nested structure: all checks under "checks" key
71
+ # 1. Old nested structure: all checks under "checks" key
72
+ # 2. New flat structure: each check is a top-level key ending with "_check"
73
+ # 3. Default config structure: check IDs directly at top level (without "_check" suffix)
73
74
  if "checks" in self.config_dict:
74
75
  # Old nested structure
75
76
  self.checks_config = self.config_dict.get("checks", {})
76
77
  else:
77
- # New flat structure - extract all keys ending with "_check"
78
- self.checks_config = {
79
- key.replace("_check", ""): value
80
- for key, value in self.config_dict.items()
81
- if key.endswith("_check") and isinstance(value, dict)
82
- }
78
+ # New flat structure and default config structure
79
+ # Extract all keys ending with "_check" OR that look like check configurations
80
+ self.checks_config = {}
81
+
82
+ # First, add keys ending with "_check"
83
+ for key, value in self.config_dict.items():
84
+ if key.endswith("_check") and isinstance(value, dict):
85
+ self.checks_config[key.replace("_check", "")] = value
86
+
87
+ # Then, add top-level keys that look like check configurations
88
+ # (they have dict values and contain typical check config keys like enabled, severity, etc.)
89
+ for key, value in self.config_dict.items():
90
+ if (
91
+ key
92
+ not in [
93
+ "settings",
94
+ "custom_checks",
95
+ "custom_checks_dir",
96
+ ] # Skip special config keys
97
+ and not key.endswith("_check") # Skip if already processed above
98
+ and isinstance(value, dict) # Must be a dict
99
+ and key not in self.checks_config # Not already added
100
+ ):
101
+ # This looks like a check configuration
102
+ self.checks_config[key] = value
83
103
 
84
104
  self.custom_checks = self.config_dict.get("custom_checks", [])
85
105
  self.custom_checks_dir = self.config_dict.get("custom_checks_dir")
@@ -36,13 +36,18 @@ class EnhancedFormatter(OutputFormatter):
36
36
 
37
37
  Args:
38
38
  report: Validation report to format
39
- **kwargs: Additional options (color: bool = True)
39
+ **kwargs: Additional options:
40
+ - color (bool): Enable color output (default: True)
41
+ - show_summary (bool): Show Executive Summary panel (default: True)
42
+ - show_severity_breakdown (bool): Show Issue Severity Breakdown panel (default: True)
40
43
 
41
44
  Returns:
42
45
  Formatted string with ANSI codes for console display
43
46
  """
44
47
  # Allow disabling color for plain text output
45
48
  color = kwargs.get("color", True)
49
+ show_summary = kwargs.get("show_summary", True)
50
+ show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
46
51
 
47
52
  # Use StringIO to capture Rich console output
48
53
  string_buffer = StringIO()
@@ -58,11 +63,12 @@ class EnhancedFormatter(OutputFormatter):
58
63
  console.print(Panel(title, border_style="bright_blue", padding=(1, 0)))
59
64
  console.print()
60
65
 
61
- # Executive Summary with progress bars
62
- self._print_summary_panel(console, report)
66
+ # Executive Summary with progress bars (optional)
67
+ if show_summary:
68
+ self._print_summary_panel(console, report)
63
69
 
64
- # Severity breakdown if there are issues
65
- if report.total_issues > 0:
70
+ # Severity breakdown if there are issues (optional)
71
+ if show_severity_breakdown and report.total_issues > 0:
66
72
  console.print()
67
73
  self._print_severity_breakdown(console, report)
68
74
 
@@ -100,7 +100,7 @@ class SARIFFormatter(OutputFormatter):
100
100
  "text": "The specified condition key is not valid for this action"
101
101
  },
102
102
  "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
103
- "defaultConfiguration": {"level": "warning"},
103
+ "defaultConfiguration": {"level": "error"},
104
104
  },
105
105
  {
106
106
  "id": "invalid-resource",
@@ -110,18 +110,55 @@ class SARIFFormatter(OutputFormatter):
110
110
  "defaultConfiguration": {"level": "error"},
111
111
  },
112
112
  {
113
- "id": "security-wildcard",
114
- "shortDescription": {"text": "Overly Permissive Wildcard"},
113
+ "id": "duplicate-sid",
114
+ "shortDescription": {"text": "Duplicate Statement ID"},
115
+ "fullDescription": {
116
+ "text": "Multiple statements use the same Statement ID (Sid), which can cause confusion"
117
+ },
118
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html",
119
+ "defaultConfiguration": {"level": "error"},
120
+ },
121
+ {
122
+ "id": "overly-permissive",
123
+ "shortDescription": {"text": "Overly Permissive Policy"},
115
124
  "fullDescription": {
116
- "text": "Using wildcards in actions or resources can be a security risk"
125
+ "text": "Policy grants overly broad permissions using wildcards in actions or resources"
117
126
  },
118
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html",
127
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege",
119
128
  "defaultConfiguration": {"level": "warning"},
120
129
  },
121
130
  {
122
- "id": "security-sensitive-action",
123
- "shortDescription": {"text": "Sensitive Action Without Conditions"},
124
- "fullDescription": {"text": "Sensitive actions should have condition restrictions"},
131
+ "id": "missing-condition",
132
+ "shortDescription": {"text": "Missing Condition Restrictions"},
133
+ "fullDescription": {
134
+ "text": "Sensitive actions should include condition restrictions to limit when they can be used"
135
+ },
136
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-policy-conditions",
137
+ "defaultConfiguration": {"level": "warning"},
138
+ },
139
+ {
140
+ "id": "missing-required-condition",
141
+ "shortDescription": {"text": "Missing Required Condition"},
142
+ "fullDescription": {
143
+ "text": "Specific actions require certain conditions to prevent privilege escalation or security issues"
144
+ },
145
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html",
146
+ "defaultConfiguration": {"level": "error"},
147
+ },
148
+ {
149
+ "id": "invalid-principal",
150
+ "shortDescription": {"text": "Invalid Principal"},
151
+ "fullDescription": {
152
+ "text": "The specified principal is invalid or improperly formatted"
153
+ },
154
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html",
155
+ "defaultConfiguration": {"level": "error"},
156
+ },
157
+ {
158
+ "id": "general-issue",
159
+ "shortDescription": {"text": "IAM Policy Issue"},
160
+ "fullDescription": {"text": "General IAM policy validation issue"},
161
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html",
125
162
  "defaultConfiguration": {"level": "warning"},
126
163
  },
127
164
  ]
@@ -170,18 +207,45 @@ class SARIFFormatter(OutputFormatter):
170
207
  return results
171
208
 
172
209
  def _get_rule_id(self, issue: ValidationIssue) -> str:
173
- """Map issue to SARIF rule ID."""
210
+ """Map issue to SARIF rule ID.
211
+
212
+ Uses the issue_type field directly, converting underscores to hyphens
213
+ for SARIF rule ID format. Falls back to heuristic matching for unknown types.
214
+ """
215
+ # Map common issue types directly
216
+ issue_type_map = {
217
+ "invalid_action": "invalid-action",
218
+ "invalid_condition_key": "invalid-condition-key",
219
+ "invalid_resource": "invalid-resource",
220
+ "duplicate_sid": "duplicate-sid",
221
+ "overly_permissive": "overly-permissive",
222
+ "missing_condition": "missing-condition",
223
+ "missing_required_condition": "missing-required-condition",
224
+ "invalid_principal": "invalid-principal",
225
+ }
226
+
227
+ # Try direct mapping from issue_type
228
+ if issue.issue_type in issue_type_map:
229
+ return issue_type_map[issue.issue_type]
230
+
231
+ # Fallback: heuristic matching based on message
174
232
  message_lower = issue.message.lower()
175
233
 
176
234
  if "action" in message_lower and "not found" in message_lower:
177
235
  return "invalid-action"
178
236
  elif "condition key" in message_lower:
179
237
  return "invalid-condition-key"
180
- elif "arn" in message_lower or "resource" in message_lower:
238
+ elif "duplicate" in message_lower and "sid" in message_lower:
239
+ return "duplicate-sid"
240
+ elif "wildcard" in message_lower or "overly permissive" in message_lower:
241
+ return "overly-permissive"
242
+ elif "missing" in message_lower and "condition" in message_lower:
243
+ if "required" in message_lower:
244
+ return "missing-required-condition"
245
+ return "missing-condition"
246
+ elif "principal" in message_lower:
247
+ return "invalid-principal"
248
+ elif "resource" in message_lower or "arn" in message_lower:
181
249
  return "invalid-resource"
182
- elif "wildcard" in message_lower or "*" in issue.message:
183
- return "security-wildcard"
184
- elif "sensitive" in message_lower:
185
- return "security-sensitive-action"
186
250
  else:
187
251
  return "general-issue"