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.
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +18 -19
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/RECORD +31 -20
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +13 -3
- iam_validator/checks/action_condition_enforcement.py +1 -6
- iam_validator/checks/condition_key_validation.py +21 -1
- iam_validator/checks/full_wildcard.py +67 -0
- iam_validator/checks/principal_validation.py +497 -3
- iam_validator/checks/sensitive_action.py +178 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
- iam_validator/checks/wildcard_action.py +62 -0
- iam_validator/checks/wildcard_resource.py +131 -0
- iam_validator/commands/download_services.py +3 -8
- iam_validator/commands/validate.py +28 -2
- iam_validator/core/aws_fetcher.py +25 -12
- iam_validator/core/check_registry.py +15 -21
- iam_validator/core/config/__init__.py +83 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/condition_requirements.py +535 -0
- iam_validator/core/config/defaults.py +390 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +133 -0
- iam_validator/core/config/service_principals.py +95 -0
- iam_validator/core/config/wildcards.py +124 -0
- iam_validator/core/config_loader.py +29 -9
- iam_validator/core/formatters/enhanced.py +11 -5
- iam_validator/core/formatters/sarif.py +78 -14
- iam_validator/checks/security_best_practices.py +0 -536
- iam_validator/core/defaults.py +0 -393
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
- {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
|
-
#
|
|
72
|
-
#
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
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": "
|
|
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": "
|
|
114
|
-
"shortDescription": {"text": "
|
|
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": "
|
|
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": "
|
|
123
|
-
"shortDescription": {"text": "
|
|
124
|
-
"fullDescription": {
|
|
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 "
|
|
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"
|