iam-policy-validator 1.3.1__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.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +164 -19
- iam_policy_validator-1.5.0.dist-info/RECORD +67 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +15 -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/policy_size.py +1 -0
- iam_validator/checks/policy_type_validation.py +299 -0
- iam_validator/checks/principal_validation.py +776 -0
- iam_validator/checks/sensitive_action.py +178 -0
- iam_validator/checks/service_wildcard.py +105 -0
- iam_validator/checks/sid_uniqueness.py +45 -7
- 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/post_to_pr.py +7 -0
- iam_validator/commands/validate.py +204 -16
- iam_validator/core/aws_fetcher.py +25 -12
- iam_validator/core/check_registry.py +25 -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/core/models.py +13 -3
- iam_validator/core/policy_checks.py +39 -6
- iam_validator/core/pr_commenter.py +30 -9
- iam_policy_validator-1.3.1.dist-info/RECORD +0 -54
- iam_validator/checks/security_best_practices.py +0 -535
- iam_validator/core/defaults.py +0 -366
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.3.1.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"
|
iam_validator/core/models.py
CHANGED
|
@@ -4,10 +4,18 @@ This module defines Pydantic models for AWS service information,
|
|
|
4
4
|
IAM policies, and validation results.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Any, ClassVar
|
|
7
|
+
from typing import Any, ClassVar, Literal
|
|
8
8
|
|
|
9
9
|
from pydantic import BaseModel, ConfigDict, Field
|
|
10
10
|
|
|
11
|
+
# Policy Type Constants
|
|
12
|
+
PolicyType = Literal[
|
|
13
|
+
"IDENTITY_POLICY",
|
|
14
|
+
"RESOURCE_POLICY",
|
|
15
|
+
"SERVICE_CONTROL_POLICY",
|
|
16
|
+
"RESOURCE_CONTROL_POLICY",
|
|
17
|
+
]
|
|
18
|
+
|
|
11
19
|
|
|
12
20
|
# AWS Service Reference Models
|
|
13
21
|
class ServiceInfo(BaseModel):
|
|
@@ -202,9 +210,10 @@ class ValidationIssue(BaseModel):
|
|
|
202
210
|
|
|
203
211
|
parts = []
|
|
204
212
|
|
|
205
|
-
# Add identifier for bot comment cleanup
|
|
213
|
+
# Add identifier for bot comment cleanup (HTML comment - not visible to users)
|
|
206
214
|
if include_identifier:
|
|
207
|
-
parts.append("
|
|
215
|
+
parts.append("<!-- iam-policy-validator-review -->\n")
|
|
216
|
+
parts.append("🤖 **IAM Policy Validator**\n")
|
|
208
217
|
|
|
209
218
|
# Build statement context for better navigation
|
|
210
219
|
statement_context = f"Statement[{self.statement_index}]"
|
|
@@ -240,6 +249,7 @@ class PolicyValidationResult(BaseModel):
|
|
|
240
249
|
|
|
241
250
|
policy_file: str
|
|
242
251
|
is_valid: bool
|
|
252
|
+
policy_type: PolicyType = "IDENTITY_POLICY"
|
|
243
253
|
issues: list[ValidationIssue] = Field(default_factory=list)
|
|
244
254
|
actions_checked: int = 0
|
|
245
255
|
condition_keys_checked: int = 0
|
|
@@ -16,6 +16,7 @@ from iam_validator.core.aws_fetcher import AWSServiceFetcher
|
|
|
16
16
|
from iam_validator.core.check_registry import CheckRegistry
|
|
17
17
|
from iam_validator.core.models import (
|
|
18
18
|
IAMPolicy,
|
|
19
|
+
PolicyType,
|
|
19
20
|
PolicyValidationResult,
|
|
20
21
|
Statement,
|
|
21
22
|
ValidationIssue,
|
|
@@ -112,17 +113,30 @@ class PolicyValidator:
|
|
|
112
113
|
logger.debug(f"Could not find field line in {policy_file}: {e}")
|
|
113
114
|
return None
|
|
114
115
|
|
|
115
|
-
async def validate_policy(
|
|
116
|
+
async def validate_policy(
|
|
117
|
+
self, policy: IAMPolicy, policy_file: str, policy_type: PolicyType = "IDENTITY_POLICY"
|
|
118
|
+
) -> PolicyValidationResult:
|
|
116
119
|
"""Validate a complete IAM policy.
|
|
117
120
|
|
|
118
121
|
Args:
|
|
119
122
|
policy: IAM policy to validate
|
|
120
123
|
policy_file: Path to the policy file
|
|
124
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
121
125
|
|
|
122
126
|
Returns:
|
|
123
127
|
PolicyValidationResult with all findings
|
|
124
128
|
"""
|
|
125
|
-
result = PolicyValidationResult(
|
|
129
|
+
result = PolicyValidationResult(
|
|
130
|
+
policy_file=policy_file, is_valid=True, policy_type=policy_type
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Apply automatic policy-type validation (not configurable - always runs)
|
|
134
|
+
from iam_validator.checks import policy_type_validation
|
|
135
|
+
|
|
136
|
+
policy_type_issues = await policy_type_validation.execute_policy(
|
|
137
|
+
policy, policy_file, policy_type=policy_type
|
|
138
|
+
)
|
|
139
|
+
result.issues.extend(policy_type_issues)
|
|
126
140
|
|
|
127
141
|
for idx, statement in enumerate(policy.statement):
|
|
128
142
|
# Get line number for this statement
|
|
@@ -460,6 +474,7 @@ async def validate_policies(
|
|
|
460
474
|
config_path: str | None = None,
|
|
461
475
|
use_registry: bool = True,
|
|
462
476
|
custom_checks_dir: str | None = None,
|
|
477
|
+
policy_type: PolicyType = "IDENTITY_POLICY",
|
|
463
478
|
) -> list[PolicyValidationResult]:
|
|
464
479
|
"""Validate multiple policies concurrently.
|
|
465
480
|
|
|
@@ -468,6 +483,7 @@ async def validate_policies(
|
|
|
468
483
|
config_path: Optional path to configuration file
|
|
469
484
|
use_registry: If True, use CheckRegistry system; if False, use legacy validator
|
|
470
485
|
custom_checks_dir: Optional path to directory containing custom checks for auto-discovery
|
|
486
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
471
487
|
|
|
472
488
|
Returns:
|
|
473
489
|
List of validation results
|
|
@@ -492,7 +508,10 @@ async def validate_policies(
|
|
|
492
508
|
) as fetcher:
|
|
493
509
|
validator = PolicyValidator(fetcher)
|
|
494
510
|
|
|
495
|
-
tasks = [
|
|
511
|
+
tasks = [
|
|
512
|
+
validator.validate_policy(policy, file_path, policy_type)
|
|
513
|
+
for file_path, policy in policies
|
|
514
|
+
]
|
|
496
515
|
|
|
497
516
|
results = await asyncio.gather(*tasks)
|
|
498
517
|
|
|
@@ -560,7 +579,9 @@ async def validate_policies(
|
|
|
560
579
|
aws_services_dir=aws_services_dir,
|
|
561
580
|
) as fetcher:
|
|
562
581
|
tasks = [
|
|
563
|
-
_validate_policy_with_registry(
|
|
582
|
+
_validate_policy_with_registry(
|
|
583
|
+
policy, file_path, registry, fetcher, fail_on_severities, policy_type
|
|
584
|
+
)
|
|
564
585
|
for file_path, policy in policies
|
|
565
586
|
]
|
|
566
587
|
|
|
@@ -575,6 +596,7 @@ async def _validate_policy_with_registry(
|
|
|
575
596
|
registry: CheckRegistry,
|
|
576
597
|
fetcher: AWSServiceFetcher,
|
|
577
598
|
fail_on_severities: list[str] | None = None,
|
|
599
|
+
policy_type: PolicyType = "IDENTITY_POLICY",
|
|
578
600
|
) -> PolicyValidationResult:
|
|
579
601
|
"""Validate a single policy using the CheckRegistry system.
|
|
580
602
|
|
|
@@ -584,15 +606,26 @@ async def _validate_policy_with_registry(
|
|
|
584
606
|
registry: CheckRegistry instance with configured checks
|
|
585
607
|
fetcher: AWS service fetcher instance
|
|
586
608
|
fail_on_severities: List of severity levels that should cause validation to fail
|
|
609
|
+
policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
|
|
587
610
|
|
|
588
611
|
Returns:
|
|
589
612
|
PolicyValidationResult with all findings
|
|
590
613
|
"""
|
|
591
|
-
result = PolicyValidationResult(policy_file=policy_file, is_valid=True)
|
|
614
|
+
result = PolicyValidationResult(policy_file=policy_file, is_valid=True, policy_type=policy_type)
|
|
615
|
+
|
|
616
|
+
# Apply automatic policy-type validation (not configurable - always runs)
|
|
617
|
+
from iam_validator.checks import policy_type_validation
|
|
618
|
+
|
|
619
|
+
policy_type_issues = await policy_type_validation.execute_policy(
|
|
620
|
+
policy, policy_file, policy_type=policy_type
|
|
621
|
+
)
|
|
622
|
+
result.issues.extend(policy_type_issues)
|
|
592
623
|
|
|
593
624
|
# Run policy-level checks first (checks that need to see the entire policy)
|
|
594
625
|
# These checks examine relationships between statements, not individual statements
|
|
595
|
-
policy_level_issues = await registry.execute_policy_checks(
|
|
626
|
+
policy_level_issues = await registry.execute_policy_checks(
|
|
627
|
+
policy, policy_file, fetcher, policy_type
|
|
628
|
+
)
|
|
596
629
|
result.issues.extend(policy_level_issues)
|
|
597
630
|
|
|
598
631
|
# Execute all statement-level checks for each statement
|
|
@@ -20,17 +20,25 @@ class PRCommenter:
|
|
|
20
20
|
# Identifier for bot comments (used for cleanup/updates)
|
|
21
21
|
BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
22
22
|
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
23
|
-
REVIEW_IDENTIFIER = "
|
|
23
|
+
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
24
24
|
|
|
25
|
-
def __init__(
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
github: GitHubIntegration | None = None,
|
|
28
|
+
cleanup_old_comments: bool = True,
|
|
29
|
+
fail_on_severities: list[str] | None = None,
|
|
30
|
+
):
|
|
26
31
|
"""Initialize PR commenter.
|
|
27
32
|
|
|
28
33
|
Args:
|
|
29
34
|
github: GitHubIntegration instance (will create one if None)
|
|
30
35
|
cleanup_old_comments: Whether to clean up old bot comments before posting new ones
|
|
36
|
+
fail_on_severities: List of severity levels that should trigger REQUEST_CHANGES
|
|
37
|
+
(e.g., ["error", "critical", "high"])
|
|
31
38
|
"""
|
|
32
39
|
self.github = github
|
|
33
40
|
self.cleanup_old_comments = cleanup_old_comments
|
|
41
|
+
self.fail_on_severities = fail_on_severities or ["error", "critical"]
|
|
34
42
|
|
|
35
43
|
async def post_findings_to_pr(
|
|
36
44
|
self,
|
|
@@ -136,17 +144,22 @@ class PRCommenter:
|
|
|
136
144
|
for file_comments in comments_by_file.values():
|
|
137
145
|
all_comments.extend(file_comments)
|
|
138
146
|
|
|
139
|
-
# Determine review event based on
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
# Determine review event based on fail_on_severities config
|
|
148
|
+
# Check if any issue has a severity that should trigger REQUEST_CHANGES
|
|
149
|
+
has_blocking_issues = any(
|
|
150
|
+
issue.severity in self.fail_on_severities
|
|
151
|
+
for result in report.results
|
|
152
|
+
for issue in result.issues
|
|
142
153
|
)
|
|
143
154
|
|
|
144
|
-
event
|
|
155
|
+
# Set review event: request changes if any blocking issues, else comment
|
|
156
|
+
event = ReviewEvent.REQUEST_CHANGES if has_blocking_issues else ReviewEvent.COMMENT
|
|
145
157
|
|
|
146
|
-
# Post review with comments (include identifier in review body for
|
|
158
|
+
# Post review with comments (include identifier in review body for cleanup)
|
|
147
159
|
review_body = (
|
|
148
160
|
f"{self.REVIEW_IDENTIFIER}\n\n"
|
|
149
|
-
f"
|
|
161
|
+
f"🤖 **IAM Policy Validator**\n\n"
|
|
162
|
+
f"## Validation Results\n\n"
|
|
150
163
|
f"Found {report.total_issues} issues across {report.total_policies} policies.\n"
|
|
151
164
|
f"See inline comments for details."
|
|
152
165
|
)
|
|
@@ -279,6 +292,7 @@ async def post_report_to_pr(
|
|
|
279
292
|
report_file: str,
|
|
280
293
|
create_review: bool = True,
|
|
281
294
|
add_summary: bool = True,
|
|
295
|
+
config_path: str | None = None,
|
|
282
296
|
) -> bool:
|
|
283
297
|
"""Post a JSON report to a PR.
|
|
284
298
|
|
|
@@ -286,6 +300,7 @@ async def post_report_to_pr(
|
|
|
286
300
|
report_file: Path to JSON report file
|
|
287
301
|
create_review: Whether to create line-specific review
|
|
288
302
|
add_summary: Whether to add summary comment
|
|
303
|
+
config_path: Optional path to config file (to get fail_on_severity)
|
|
289
304
|
|
|
290
305
|
Returns:
|
|
291
306
|
True if successful, False otherwise
|
|
@@ -297,9 +312,15 @@ async def post_report_to_pr(
|
|
|
297
312
|
|
|
298
313
|
report = ValidationReport.model_validate(report_data)
|
|
299
314
|
|
|
315
|
+
# Load config to get fail_on_severity setting
|
|
316
|
+
from iam_validator.core.config_loader import ConfigLoader
|
|
317
|
+
|
|
318
|
+
config = ConfigLoader.load_config(config_path)
|
|
319
|
+
fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
|
|
320
|
+
|
|
300
321
|
# Post to PR
|
|
301
322
|
async with GitHubIntegration() as github:
|
|
302
|
-
commenter = PRCommenter(github)
|
|
323
|
+
commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
|
|
303
324
|
return await commenter.post_findings_to_pr(
|
|
304
325
|
report,
|
|
305
326
|
create_review=create_review,
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
|
|
2
|
-
iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
|
|
3
|
-
iam_validator/__version__.py,sha256=hbgDe5p_vG5JrspHS61bAQLyKxbRMqbUDzeKUVq_gmo,206
|
|
4
|
-
iam_validator/checks/__init__.py,sha256=eKTPgiZ1i3zvyP6OdKgLx9s3u69onITMYifmJPJwZgM,968
|
|
5
|
-
iam_validator/checks/action_condition_enforcement.py,sha256=3M1Wj89Af6H-ywBTruZbJPzhCBBQVanVb5hwv-fkiDE,29721
|
|
6
|
-
iam_validator/checks/action_resource_constraint.py,sha256=p-gP7S9QYR6M7vffrnJY6LOlMUTn0kpEbrxQ8pTY5rs,6031
|
|
7
|
-
iam_validator/checks/action_validation.py,sha256=IpxtTsk58f2zEZ-xzAoyHw4QK8BCRV43OffP-8ydf9E,2578
|
|
8
|
-
iam_validator/checks/condition_key_validation.py,sha256=bc4LQ8IRKyt0RquaQvQvVjmeJnuOUAFRL8xdduLPa_U,2661
|
|
9
|
-
iam_validator/checks/policy_size.py,sha256=4cvZiWRJXGuvYo8PRcdD1Py_ZL8Xw0lOJfXTs6EX-_I,5753
|
|
10
|
-
iam_validator/checks/resource_validation.py,sha256=AEIoiR6AKYLuVaA8ne3QE5qy6NCMDe98_2JAiwE9-JU,4261
|
|
11
|
-
iam_validator/checks/security_best_practices.py,sha256=uf3ZAhBkyN8ka9bZHWi2kkAGIibhqWMIF06DBXsgu9U,23093
|
|
12
|
-
iam_validator/checks/sid_uniqueness.py,sha256=U2Kk5lYi9mHhhTpCWAD0ZQfxcLnIJJa7KGC5nOzTEbY,5145
|
|
13
|
-
iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
|
|
14
|
-
iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
|
|
15
|
-
iam_validator/checks/utils/sensitive_action_matcher.py,sha256=VlTpgjMnympYa28kOdm6xRIUL2P87rOvm1O2NdnjtVI,8900
|
|
16
|
-
iam_validator/checks/utils/wildcard_expansion.py,sha256=V3V_KRpapOzPBhpUObJjGHoMhvCH90QvDxppeEHIG_U,3152
|
|
17
|
-
iam_validator/commands/__init__.py,sha256=M-5bo8w0TCWydK0cXgJyPD2fmk8bpQs-3b26YbgLzlc,565
|
|
18
|
-
iam_validator/commands/analyze.py,sha256=TWlDaZ8gVOdNv6__KQQfzeLVW36qLiL5IzlhGYfvq_g,16501
|
|
19
|
-
iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
|
|
20
|
-
iam_validator/commands/cache.py,sha256=NHfbIDWI8tj-3o-4fIZJQS-Vvd9bxIH3Lk6kBtNuiUU,14212
|
|
21
|
-
iam_validator/commands/download_services.py,sha256=anRcobOuhkiEmHpwW_AJb1e2ifgkgYAO2-b9-JBrBcg,9152
|
|
22
|
-
iam_validator/commands/post_to_pr.py,sha256=hl_K-XlELYN-ArjMdgQqysvIE-26yf9XdrMl4ToDwG0,2148
|
|
23
|
-
iam_validator/commands/validate.py,sha256=R295cOTly8n7zL1jfvbh9RuCgiM5edBqbf6YMn_4G9A,14013
|
|
24
|
-
iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
|
|
25
|
-
iam_validator/core/access_analyzer.py,sha256=poeT1i74jXpKr1B3UmvqiTvCTbq82zffWgZHwiFUwoo,24337
|
|
26
|
-
iam_validator/core/access_analyzer_report.py,sha256=IrQVszlhFfQ6WykYLpig7TU3hf8dnQTegPDsOvHjR5Q,24873
|
|
27
|
-
iam_validator/core/aws_fetcher.py,sha256=0rG7qi3Lz6ulU6pDL0nZ6sklgSAS5pwo0ViykDspRt8,33382
|
|
28
|
-
iam_validator/core/aws_global_conditions.py,sha256=ADVcMEWhgvDZWdBmRUQN3HB7a9OycbTLecXFAy3LPbo,5837
|
|
29
|
-
iam_validator/core/check_registry.py,sha256=wxqaF2t_3lWgT6x7_PnnZ8XGjHKUxUk72UlmdYBLFyo,15679
|
|
30
|
-
iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
|
|
31
|
-
iam_validator/core/config_loader.py,sha256=Pq2rd6LJtEZET0ZeW4hEZS2ZRLC5gNRsKbtLyIsT21I,16516
|
|
32
|
-
iam_validator/core/defaults.py,sha256=brGPx0_8zmsMNddYryMKbcoIh8VJq2mdXZdGDItAsQs,13251
|
|
33
|
-
iam_validator/core/models.py,sha256=rWIZnD-I81Sg4asgOhnB10FWJC5mxQ2JO9bdS0sHb4Q,10772
|
|
34
|
-
iam_validator/core/policy_checks.py,sha256=pMlZ2XkuqppVOUZq__e8w_yGoy7lIHjAB5RiTXwJo4Q,25114
|
|
35
|
-
iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
|
|
36
|
-
iam_validator/core/pr_commenter.py,sha256=TOhVXKTFcRHQ9EVuShXQcKXn9aNjB1mU6FnR2jvltmw,10581
|
|
37
|
-
iam_validator/core/report.py,sha256=Yeh_u9jQvTyDV3ignyPcWEQVfFcxNZNrxf4T0fjeWb4,33283
|
|
38
|
-
iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
|
|
39
|
-
iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
|
|
40
|
-
iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
|
|
41
|
-
iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
|
|
42
|
-
iam_validator/core/formatters/enhanced.py,sha256=-W9JACV4FNVWoWtfVxXLla4d__Gg96SASbNAijpJnT0,16638
|
|
43
|
-
iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
|
|
44
|
-
iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
|
|
45
|
-
iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
|
|
46
|
-
iam_validator/core/formatters/sarif.py,sha256=tqp8g7RmUh0HRk-kKDaucx4sa-5I9ikgkSpy1MM8Vi4,7200
|
|
47
|
-
iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
|
|
48
|
-
iam_validator/integrations/github_integration.py,sha256=bKs94vNT4PmcmUPUeuY2WJFhCYpUY2SWiBP1vj-andA,25673
|
|
49
|
-
iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
|
|
50
|
-
iam_policy_validator-1.3.1.dist-info/METADATA,sha256=NNF1fvnG9g8pGMopQ71yn5rHtWnRIVMBUGPEeNLX9jI,29465
|
|
51
|
-
iam_policy_validator-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
52
|
-
iam_policy_validator-1.3.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
|
|
53
|
-
iam_policy_validator-1.3.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
54
|
-
iam_policy_validator-1.3.1.dist-info/RECORD,,
|