iam-policy-validator 1.14.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.14.0.dist-info/METADATA +782 -0
- iam_policy_validator-1.14.0.dist-info/RECORD +106 -0
- iam_policy_validator-1.14.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.14.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.14.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +9 -0
- iam_validator/checks/__init__.py +45 -0
- iam_validator/checks/action_condition_enforcement.py +1442 -0
- iam_validator/checks/action_resource_matching.py +472 -0
- iam_validator/checks/action_validation.py +67 -0
- iam_validator/checks/condition_key_validation.py +88 -0
- iam_validator/checks/condition_type_mismatch.py +257 -0
- iam_validator/checks/full_wildcard.py +62 -0
- iam_validator/checks/mfa_condition_check.py +105 -0
- iam_validator/checks/policy_size.py +114 -0
- iam_validator/checks/policy_structure.py +556 -0
- iam_validator/checks/policy_type_validation.py +331 -0
- iam_validator/checks/principal_validation.py +708 -0
- iam_validator/checks/resource_validation.py +135 -0
- iam_validator/checks/sensitive_action.py +438 -0
- iam_validator/checks/service_wildcard.py +98 -0
- iam_validator/checks/set_operator_validation.py +153 -0
- iam_validator/checks/sid_uniqueness.py +146 -0
- iam_validator/checks/trust_policy_validation.py +509 -0
- iam_validator/checks/utils/__init__.py +17 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/utils/policy_level_checks.py +190 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +293 -0
- iam_validator/checks/utils/wildcard_expansion.py +86 -0
- iam_validator/checks/wildcard_action.py +58 -0
- iam_validator/checks/wildcard_resource.py +374 -0
- iam_validator/commands/__init__.py +31 -0
- iam_validator/commands/analyze.py +549 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +393 -0
- iam_validator/commands/completion.py +471 -0
- iam_validator/commands/download_services.py +255 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +830 -0
- iam_validator/core/__init__.py +13 -0
- iam_validator/core/access_analyzer.py +671 -0
- iam_validator/core/access_analyzer_report.py +640 -0
- iam_validator/core/aws_fetcher.py +29 -0
- iam_validator/core/aws_service/__init__.py +21 -0
- iam_validator/core/aws_service/cache.py +108 -0
- iam_validator/core/aws_service/client.py +205 -0
- iam_validator/core/aws_service/fetcher.py +641 -0
- iam_validator/core/aws_service/parsers.py +149 -0
- iam_validator/core/aws_service/patterns.py +51 -0
- iam_validator/core/aws_service/storage.py +291 -0
- iam_validator/core/aws_service/validators.py +380 -0
- iam_validator/core/check_registry.py +679 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/condition_validators.py +626 -0
- iam_validator/core/config/__init__.py +81 -0
- iam_validator/core/config/aws_api.py +35 -0
- iam_validator/core/config/aws_global_conditions.py +160 -0
- iam_validator/core/config/category_suggestions.py +181 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/condition_requirements.py +258 -0
- iam_validator/core/config/config_loader.py +670 -0
- iam_validator/core/config/defaults.py +739 -0
- iam_validator/core/config/principal_requirements.py +421 -0
- iam_validator/core/config/sensitive_actions.py +672 -0
- iam_validator/core/config/service_principals.py +132 -0
- iam_validator/core/config/wildcards.py +127 -0
- iam_validator/core/constants.py +149 -0
- iam_validator/core/diff_parser.py +325 -0
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +68 -0
- iam_validator/core/formatters/csv.py +171 -0
- iam_validator/core/formatters/enhanced.py +481 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +64 -0
- iam_validator/core/formatters/sarif.py +251 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/label_manager.py +197 -0
- iam_validator/core/models.py +404 -0
- iam_validator/core/policy_checks.py +220 -0
- iam_validator/core/policy_loader.py +785 -0
- iam_validator/core/pr_commenter.py +780 -0
- iam_validator/core/report.py +942 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +1821 -0
- iam_validator/integrations/ms_teams.py +442 -0
- iam_validator/sdk/__init__.py +220 -0
- iam_validator/sdk/arn_matching.py +382 -0
- iam_validator/sdk/context.py +222 -0
- iam_validator/sdk/exceptions.py +48 -0
- iam_validator/sdk/helpers.py +177 -0
- iam_validator/sdk/policy_utils.py +451 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_validator/sdk/shortcuts.py +283 -0
- iam_validator/utils/__init__.py +35 -0
- iam_validator/utils/cache.py +105 -0
- iam_validator/utils/regex.py +205 -0
- iam_validator/utils/terminal.py +22 -0
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"""Policy structure validation check.
|
|
2
|
+
|
|
3
|
+
This check validates the fundamental structure of IAM policy statements,
|
|
4
|
+
ensuring they meet AWS IAM requirements. It checks for:
|
|
5
|
+
|
|
6
|
+
1. Required fields (Effect, Action/NotAction, Resource/NotResource)
|
|
7
|
+
2. Mutual exclusivity (Action vs NotAction, Resource vs NotResource, Principal vs NotPrincipal)
|
|
8
|
+
3. Valid field values (Effect must be "Allow" or "Deny")
|
|
9
|
+
4. Unknown/unexpected fields in statements
|
|
10
|
+
5. Valid Version field in policy document
|
|
11
|
+
|
|
12
|
+
This check is inspired by Parliament's analyze_statement function and should run
|
|
13
|
+
BEFORE all other checks to catch fundamental structural issues early.
|
|
14
|
+
|
|
15
|
+
By detecting these issues early, we can:
|
|
16
|
+
- Provide detailed error messages about what's wrong
|
|
17
|
+
- Post specific findings to GitHub for user feedback
|
|
18
|
+
- Allow other checks to run and find additional issues
|
|
19
|
+
- Help users fix policies that would otherwise be rejected by Pydantic validation
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import re
|
|
23
|
+
from typing import Any, ClassVar
|
|
24
|
+
|
|
25
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
26
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
27
|
+
from iam_validator.core.models import IAMPolicy, PolicyType, ValidationIssue
|
|
28
|
+
|
|
29
|
+
# Valid statement fields according to AWS IAM policy grammar
|
|
30
|
+
# https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_grammar.html
|
|
31
|
+
VALID_STATEMENT_FIELDS = {
|
|
32
|
+
"Effect",
|
|
33
|
+
"Sid",
|
|
34
|
+
"Principal",
|
|
35
|
+
"NotPrincipal",
|
|
36
|
+
"Action",
|
|
37
|
+
"NotAction",
|
|
38
|
+
"Resource",
|
|
39
|
+
"NotResource",
|
|
40
|
+
"Condition",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Valid policy document fields
|
|
44
|
+
VALID_POLICY_FIELDS = {
|
|
45
|
+
"Version",
|
|
46
|
+
"Statement",
|
|
47
|
+
"Id",
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# Valid AWS IAM policy versions
|
|
51
|
+
VALID_POLICY_VERSIONS = {"2012-10-17", "2008-10-17"}
|
|
52
|
+
|
|
53
|
+
# Valid Effect values
|
|
54
|
+
VALID_EFFECTS = {"Allow", "Deny"}
|
|
55
|
+
|
|
56
|
+
# SID format: alphanumeric characters only (no spaces, hyphens, or underscores in AWS strict grammar)
|
|
57
|
+
# However, AWS console and APIs often accept hyphens and underscores, so we allow them
|
|
58
|
+
SID_PATTERN = re.compile(r"^[a-zA-Z0-9]+$")
|
|
59
|
+
|
|
60
|
+
# Assume role actions used in trust policies (frozen set for O(1) lookup performance)
|
|
61
|
+
ASSUME_ROLE_ACTIONS = frozenset(
|
|
62
|
+
{
|
|
63
|
+
"sts:AssumeRole",
|
|
64
|
+
"sts:AssumeRoleWithSAML",
|
|
65
|
+
"sts:AssumeRoleWithWebIdentity",
|
|
66
|
+
"sts:TagSession",
|
|
67
|
+
"sts:SetSourceIdentity",
|
|
68
|
+
"sts:SetContext",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def is_trust_policy(policy: IAMPolicy) -> bool:
|
|
74
|
+
"""Detect if policy is a trust policy (role assumption policy).
|
|
75
|
+
|
|
76
|
+
Trust policies are a special type of resource policy attached to IAM roles.
|
|
77
|
+
They control who can assume the role.
|
|
78
|
+
|
|
79
|
+
Battle-hardened detection logic (minimizes false positives):
|
|
80
|
+
1. Has Principal element (resource policy characteristic)
|
|
81
|
+
2. Contains assume role actions (sts:AssumeRole*, sts:TagSession)
|
|
82
|
+
3. Effect is "Allow" (trust policies grant assumption, never Deny)
|
|
83
|
+
4. No specific resource ARNs (role itself is the resource; only * or *:* allowed)
|
|
84
|
+
|
|
85
|
+
Conservative approach prevents false positives:
|
|
86
|
+
- Exact action matching (not just startswith)
|
|
87
|
+
- Rejects policies with specific resource ARNs
|
|
88
|
+
- Validates Effect is Allow
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
policy: IAM policy to analyze
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True if policy appears to be a trust policy
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
Trust policy (returns True):
|
|
98
|
+
{"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"},
|
|
99
|
+
"Action": "sts:AssumeRole"}
|
|
100
|
+
|
|
101
|
+
S3 bucket policy (returns False - has specific resources):
|
|
102
|
+
{"Effect": "Allow", "Principal": "*",
|
|
103
|
+
"Action": "s3:GetObject", "Resource": "arn:aws:s3:::bucket/*"}
|
|
104
|
+
"""
|
|
105
|
+
# First pass: Check if ANY statement has specific resource ARNs
|
|
106
|
+
# Trust policies don't have specific resource ARNs (role itself is the resource)
|
|
107
|
+
for statement in policy.statement or []:
|
|
108
|
+
if statement.resource:
|
|
109
|
+
resources = (
|
|
110
|
+
[statement.resource] if isinstance(statement.resource, str) else statement.resource
|
|
111
|
+
)
|
|
112
|
+
for resource in resources:
|
|
113
|
+
if isinstance(resource, str) and resource != "*" and not resource.endswith(":*"):
|
|
114
|
+
# Specific resource ARN found in ANY statement - NOT a trust policy
|
|
115
|
+
# Trust policies should only have *, *:*, or no Resource field
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
# Second pass: Check for valid assume statements
|
|
119
|
+
has_valid_assume_statement = False
|
|
120
|
+
|
|
121
|
+
for statement in policy.statement or []:
|
|
122
|
+
# Skip if no principal (trust policies must have principals)
|
|
123
|
+
if statement.principal is None and statement.not_principal is None:
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Skip if Effect is Deny (trust policies use Allow)
|
|
127
|
+
if statement.effect and statement.effect.lower() != "allow":
|
|
128
|
+
continue
|
|
129
|
+
|
|
130
|
+
# Get actions (handle both string and list)
|
|
131
|
+
actions = []
|
|
132
|
+
if statement.action:
|
|
133
|
+
actions = [statement.action] if isinstance(statement.action, str) else statement.action
|
|
134
|
+
|
|
135
|
+
# Check if any action is an assume action (O(1) set lookup)
|
|
136
|
+
has_assume_action = any(
|
|
137
|
+
action in ASSUME_ROLE_ACTIONS or action in ("sts:*", "*")
|
|
138
|
+
for action in actions
|
|
139
|
+
if isinstance(action, str)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if has_assume_action:
|
|
143
|
+
# Found a valid trust policy statement
|
|
144
|
+
has_valid_assume_statement = True
|
|
145
|
+
# Continue checking - we already validated no specific resources in first pass
|
|
146
|
+
|
|
147
|
+
return has_valid_assume_statement
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def detect_policy_type(policy: IAMPolicy) -> PolicyType:
|
|
151
|
+
"""Auto-detect policy type based on statement structure.
|
|
152
|
+
|
|
153
|
+
Detection logic (simple and safe - avoids false positives):
|
|
154
|
+
1. If any statement has Principal/NotPrincipal → RESOURCE_POLICY
|
|
155
|
+
2. Otherwise → IDENTITY_POLICY (default, also covers SCPs)
|
|
156
|
+
|
|
157
|
+
Note: The following policy types require EXPLICIT specification via --policy-type flag
|
|
158
|
+
and are NOT auto-detected to avoid false positives and confusing errors:
|
|
159
|
+
|
|
160
|
+
- TRUST_POLICY: Requires explicit flag to enable trust-specific validation
|
|
161
|
+
and suppress irrelevant warnings (missing Resource field, etc.)
|
|
162
|
+
Use: --policy-type TRUST_POLICY
|
|
163
|
+
|
|
164
|
+
- SERVICE_CONTROL_POLICY: SCPs have the same structure as identity policies
|
|
165
|
+
and cannot be reliably distinguished without context
|
|
166
|
+
Use: --policy-type SERVICE_CONTROL_POLICY
|
|
167
|
+
|
|
168
|
+
- RESOURCE_CONTROL_POLICY: RCPs have strict requirements that require explicit
|
|
169
|
+
validation mode to avoid false positives
|
|
170
|
+
Use: --policy-type RESOURCE_CONTROL_POLICY
|
|
171
|
+
|
|
172
|
+
Auto-detection only distinguishes between:
|
|
173
|
+
- IDENTITY_POLICY (no Principal element) - most common
|
|
174
|
+
- RESOURCE_POLICY (has Principal element) - S3, SNS, SQS, etc.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
policy: IAM policy to analyze
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Detected PolicyType (IDENTITY_POLICY or RESOURCE_POLICY only)
|
|
181
|
+
"""
|
|
182
|
+
# Check if any statement has Principal/NotPrincipal (indicates resource policy)
|
|
183
|
+
for statement in policy.statement or []:
|
|
184
|
+
if statement.principal is not None or statement.not_principal is not None:
|
|
185
|
+
return "RESOURCE_POLICY"
|
|
186
|
+
|
|
187
|
+
# Default to identity policy (most common case)
|
|
188
|
+
# SCPs have the same structure and will be detected as IDENTITY_POLICY
|
|
189
|
+
return "IDENTITY_POLICY"
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def validate_policy_document(policy_dict: dict[str, Any]) -> list[ValidationIssue]:
|
|
193
|
+
"""Validate the top-level policy document structure.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
policy_dict: Raw policy dictionary
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
List of validation issues
|
|
200
|
+
"""
|
|
201
|
+
issues: list[ValidationIssue] = []
|
|
202
|
+
|
|
203
|
+
# Check for unknown fields in policy document
|
|
204
|
+
unknown_fields = set(policy_dict.keys()) - VALID_POLICY_FIELDS
|
|
205
|
+
if unknown_fields:
|
|
206
|
+
issues.append(
|
|
207
|
+
ValidationIssue(
|
|
208
|
+
severity="error",
|
|
209
|
+
statement_index=-1, # Policy-level issue
|
|
210
|
+
issue_type="unknown_policy_field",
|
|
211
|
+
message=f"Policy document contains unknown field(s): {', '.join(sorted(f'`{f}`' for f in unknown_fields))}",
|
|
212
|
+
suggestion=f"Remove the unknown field(s). Valid policy fields are: {', '.join(f'`{f}`' for f in sorted(VALID_POLICY_FIELDS))}",
|
|
213
|
+
example=('{\n "Version": "2012-10-17",\n "Statement": [...]\n}'),
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Validate Version field
|
|
218
|
+
if "Version" not in policy_dict:
|
|
219
|
+
issues.append(
|
|
220
|
+
ValidationIssue(
|
|
221
|
+
severity="error",
|
|
222
|
+
statement_index=-1,
|
|
223
|
+
issue_type="missing_version",
|
|
224
|
+
message="Policy document is missing the `Version` field",
|
|
225
|
+
suggestion="Add a `Version` field with value `2012-10-17` (recommended) or `2008-10-17`",
|
|
226
|
+
example=('{\n "Version": "2012-10-17",\n "Statement": [...]\n}'),
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
elif policy_dict["Version"] not in VALID_POLICY_VERSIONS:
|
|
230
|
+
issues.append(
|
|
231
|
+
ValidationIssue(
|
|
232
|
+
severity="error",
|
|
233
|
+
statement_index=-1,
|
|
234
|
+
issue_type="invalid_version",
|
|
235
|
+
message=f"Invalid policy `Version`: `{policy_dict['Version']}`. Must be `2012-10-17` or `2008-10-17`",
|
|
236
|
+
suggestion="Use `Version` `2012-10-17` (recommended) for the latest IAM policy grammar",
|
|
237
|
+
example=('{\n "Version": "2012-10-17",\n "Statement": [...]\n}'),
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Validate Statement field
|
|
242
|
+
if "Statement" not in policy_dict:
|
|
243
|
+
issues.append(
|
|
244
|
+
ValidationIssue(
|
|
245
|
+
severity="error",
|
|
246
|
+
statement_index=-1,
|
|
247
|
+
issue_type="missing_statement",
|
|
248
|
+
message="Policy document is missing the `Statement` field",
|
|
249
|
+
suggestion="Add a `Statement` field containing an array of policy statements",
|
|
250
|
+
example=(
|
|
251
|
+
"{\n"
|
|
252
|
+
' "Version": "2012-10-17",\n'
|
|
253
|
+
' "Statement": [\n'
|
|
254
|
+
" {\n"
|
|
255
|
+
' "Effect": "Allow",\n'
|
|
256
|
+
' "Action": "s3:GetObject",\n'
|
|
257
|
+
' "Resource": "*"\n'
|
|
258
|
+
" }\n"
|
|
259
|
+
" ]\n"
|
|
260
|
+
"}"
|
|
261
|
+
),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
elif not isinstance(policy_dict["Statement"], list):
|
|
265
|
+
issues.append(
|
|
266
|
+
ValidationIssue(
|
|
267
|
+
severity="error",
|
|
268
|
+
statement_index=-1,
|
|
269
|
+
issue_type="invalid_statement_type",
|
|
270
|
+
message=f"`Statement` field must be an array, not `{type(policy_dict['Statement']).__name__}`",
|
|
271
|
+
suggestion="Wrap your statement in an array: [ {...} ]",
|
|
272
|
+
example=(
|
|
273
|
+
"{\n"
|
|
274
|
+
' "Version": "2012-10-17",\n'
|
|
275
|
+
' "Statement": [\n'
|
|
276
|
+
" {\n"
|
|
277
|
+
' "Effect": "Allow",\n'
|
|
278
|
+
' "Action": "s3:GetObject",\n'
|
|
279
|
+
' "Resource": "*"\n'
|
|
280
|
+
" }\n"
|
|
281
|
+
" ]\n"
|
|
282
|
+
"}"
|
|
283
|
+
),
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
elif len(policy_dict["Statement"]) == 0:
|
|
287
|
+
issues.append(
|
|
288
|
+
ValidationIssue(
|
|
289
|
+
severity="error",
|
|
290
|
+
statement_index=-1,
|
|
291
|
+
issue_type="empty_statement",
|
|
292
|
+
message="Policy document has an empty `Statement` array",
|
|
293
|
+
suggestion="Add at least one policy `Statement`",
|
|
294
|
+
example=(
|
|
295
|
+
"{\n"
|
|
296
|
+
' "Version": "2012-10-17",\n'
|
|
297
|
+
' "Statement": [\n'
|
|
298
|
+
" {\n"
|
|
299
|
+
' "Effect": "Allow",\n'
|
|
300
|
+
' "Action": "s3:GetObject",\n'
|
|
301
|
+
' "Resource": "*"\n'
|
|
302
|
+
" }\n"
|
|
303
|
+
" ]\n"
|
|
304
|
+
"}"
|
|
305
|
+
),
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return issues
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def validate_statement_structure(
|
|
313
|
+
statement_dict: dict[str, Any], statement_idx: int, policy_type: str = "IDENTITY_POLICY"
|
|
314
|
+
) -> list[ValidationIssue]:
|
|
315
|
+
"""Validate the structure of a single statement.
|
|
316
|
+
|
|
317
|
+
This implements validation similar to Parliament's analyze_statement function.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
statement_dict: Raw statement dictionary
|
|
321
|
+
statement_idx: Index of the statement in the policy
|
|
322
|
+
policy_type: Type of policy being validated (affects missing Resource validation)
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
List of validation issues
|
|
326
|
+
"""
|
|
327
|
+
issues: list[ValidationIssue] = []
|
|
328
|
+
sid = statement_dict.get("Sid")
|
|
329
|
+
|
|
330
|
+
# Check for unknown fields
|
|
331
|
+
unknown_fields = set(statement_dict.keys()) - VALID_STATEMENT_FIELDS
|
|
332
|
+
if unknown_fields:
|
|
333
|
+
issues.append(
|
|
334
|
+
ValidationIssue(
|
|
335
|
+
severity="error",
|
|
336
|
+
statement_sid=sid,
|
|
337
|
+
statement_index=statement_idx,
|
|
338
|
+
issue_type="unknown_field",
|
|
339
|
+
message=f"`Statement` contains unknown field(s): {', '.join(sorted(f'`{f}`' for f in unknown_fields))}",
|
|
340
|
+
suggestion=f"Remove the unknown field(s). Valid statement fields are: {', '.join(sorted(f'`{f}`' for f in VALID_STATEMENT_FIELDS))}",
|
|
341
|
+
)
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
# Validate Effect field (required)
|
|
345
|
+
if "Effect" not in statement_dict:
|
|
346
|
+
issues.append(
|
|
347
|
+
ValidationIssue(
|
|
348
|
+
severity="error",
|
|
349
|
+
statement_sid=sid,
|
|
350
|
+
statement_index=statement_idx,
|
|
351
|
+
issue_type="missing_effect",
|
|
352
|
+
message="`Statement` is missing the required `Effect` field",
|
|
353
|
+
suggestion="Add an `Effect` field with value `Allow` or `Deny`",
|
|
354
|
+
example='"Effect": "Allow"',
|
|
355
|
+
field_name="effect",
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
elif statement_dict["Effect"] not in VALID_EFFECTS:
|
|
359
|
+
issues.append(
|
|
360
|
+
ValidationIssue(
|
|
361
|
+
severity="error",
|
|
362
|
+
statement_sid=sid,
|
|
363
|
+
statement_index=statement_idx,
|
|
364
|
+
issue_type="invalid_effect",
|
|
365
|
+
message=f"Invalid `Effect` value: `{statement_dict['Effect']}`. Must be `Allow` or `Deny`",
|
|
366
|
+
suggestion="Change `Effect` to either `Allow` or `Deny`",
|
|
367
|
+
example='"Effect": "Allow"',
|
|
368
|
+
field_name="effect",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Validate SID format (if present)
|
|
373
|
+
if sid is not None:
|
|
374
|
+
if not isinstance(sid, str):
|
|
375
|
+
issues.append(
|
|
376
|
+
ValidationIssue(
|
|
377
|
+
severity="error",
|
|
378
|
+
statement_sid=str(sid),
|
|
379
|
+
statement_index=statement_idx,
|
|
380
|
+
issue_type="invalid_sid_type",
|
|
381
|
+
message=f"`Sid` must be a `string`, not `{type(sid).__name__}`",
|
|
382
|
+
suggestion='Wrap the `Sid` value in quotes to make it a string: `"Sid": "AllowS3Access"`',
|
|
383
|
+
example='"Sid": "AllowS3Access"',
|
|
384
|
+
field_name="sid",
|
|
385
|
+
)
|
|
386
|
+
)
|
|
387
|
+
elif not SID_PATTERN.match(sid):
|
|
388
|
+
# According to AWS grammar, SID should be alphanumeric only
|
|
389
|
+
# However, we issue a warning instead of error since some AWS services accept more
|
|
390
|
+
invalid_chars = "".join(set(c for c in sid if not c.isalnum()))
|
|
391
|
+
issues.append(
|
|
392
|
+
ValidationIssue(
|
|
393
|
+
severity="warning",
|
|
394
|
+
statement_sid=sid,
|
|
395
|
+
statement_index=statement_idx,
|
|
396
|
+
issue_type="invalid_sid_format",
|
|
397
|
+
message=f"`Sid` `{sid}` contains non-alphanumeric characters: `{invalid_chars}`",
|
|
398
|
+
suggestion="According to AWS IAM policy grammar, `Sid` should contain only alphanumeric characters `(A-Z, a-z, 0-9)`.",
|
|
399
|
+
field_name="sid",
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Validate Principal/NotPrincipal mutual exclusivity
|
|
404
|
+
if "Principal" in statement_dict and "NotPrincipal" in statement_dict:
|
|
405
|
+
issues.append(
|
|
406
|
+
ValidationIssue(
|
|
407
|
+
severity="error",
|
|
408
|
+
statement_sid=sid,
|
|
409
|
+
statement_index=statement_idx,
|
|
410
|
+
issue_type="principal_conflict",
|
|
411
|
+
message="`Statement` contains both `Principal` and `NotPrincipal` fields",
|
|
412
|
+
suggestion="Use either `Principal` or `NotPrincipal`, not both",
|
|
413
|
+
field_name="principal",
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
# Validate Action/NotAction mutual exclusivity and presence
|
|
418
|
+
has_action = "Action" in statement_dict
|
|
419
|
+
has_not_action = "NotAction" in statement_dict
|
|
420
|
+
|
|
421
|
+
if has_action and has_not_action:
|
|
422
|
+
issues.append(
|
|
423
|
+
ValidationIssue(
|
|
424
|
+
severity="error",
|
|
425
|
+
statement_sid=sid,
|
|
426
|
+
statement_index=statement_idx,
|
|
427
|
+
issue_type="action_conflict",
|
|
428
|
+
message="`Statement` contains both `Action` and `NotAction` fields",
|
|
429
|
+
suggestion="Use either `Action` or `NotAction`, not both",
|
|
430
|
+
field_name="action",
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
elif not has_action and not has_not_action:
|
|
434
|
+
issues.append(
|
|
435
|
+
ValidationIssue(
|
|
436
|
+
severity="error",
|
|
437
|
+
statement_sid=sid,
|
|
438
|
+
statement_index=statement_idx,
|
|
439
|
+
issue_type="missing_action",
|
|
440
|
+
message="`Statement` is missing both `Action` and `NotAction` fields",
|
|
441
|
+
suggestion="Add either an `Action` or `NotAction` field to specify which AWS actions this statement applies to",
|
|
442
|
+
example=('"Action": [\n "s3:GetObject",\n "s3:PutObject"\n]'),
|
|
443
|
+
field_name="action",
|
|
444
|
+
)
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
# Validate Resource/NotResource mutual exclusivity and presence
|
|
448
|
+
has_resource = "Resource" in statement_dict
|
|
449
|
+
has_not_resource = "NotResource" in statement_dict
|
|
450
|
+
|
|
451
|
+
if has_resource and has_not_resource:
|
|
452
|
+
issues.append(
|
|
453
|
+
ValidationIssue(
|
|
454
|
+
severity="error",
|
|
455
|
+
statement_sid=sid,
|
|
456
|
+
statement_index=statement_idx,
|
|
457
|
+
issue_type="resource_conflict",
|
|
458
|
+
message="`Statement` contains both `Resource` and `NotResource` fields",
|
|
459
|
+
suggestion="Use either `Resource` or `NotResource`, not both",
|
|
460
|
+
field_name="resource",
|
|
461
|
+
)
|
|
462
|
+
)
|
|
463
|
+
elif not has_resource and not has_not_resource:
|
|
464
|
+
# Trust policies don't need Resource field (role itself is the resource)
|
|
465
|
+
if policy_type == "TRUST_POLICY":
|
|
466
|
+
# Skip this check for trust policies - it's expected and correct
|
|
467
|
+
pass
|
|
468
|
+
else:
|
|
469
|
+
# Resource/NotResource are optional in some contexts (e.g., resource policies with Principal)
|
|
470
|
+
# Issue an info-level message
|
|
471
|
+
issues.append(
|
|
472
|
+
ValidationIssue(
|
|
473
|
+
severity="info",
|
|
474
|
+
statement_sid=sid,
|
|
475
|
+
statement_index=statement_idx,
|
|
476
|
+
issue_type="missing_resource",
|
|
477
|
+
message="`Statement` is missing both `Resource` and `NotResource` fields",
|
|
478
|
+
suggestion="Most policies require a `Resource` field. Add a `Resource` or `NotResource` field to specify which AWS resources this statement applies to.",
|
|
479
|
+
example=('"Resource": "*" OR "Resource": "arn:aws:s3:::my-bucket/*"'),
|
|
480
|
+
field_name="resource",
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
|
|
484
|
+
return issues
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
class PolicyStructureCheck(PolicyCheck):
|
|
488
|
+
"""Validates fundamental IAM policy structure.
|
|
489
|
+
|
|
490
|
+
This check ensures policies meet AWS IAM requirements for basic structure:
|
|
491
|
+
- Required fields are present
|
|
492
|
+
- Mutually exclusive fields aren't used together
|
|
493
|
+
- Field values are valid
|
|
494
|
+
- No unknown fields are present
|
|
495
|
+
|
|
496
|
+
This check should run FIRST before all other checks to catch fundamental
|
|
497
|
+
issues early.
|
|
498
|
+
"""
|
|
499
|
+
|
|
500
|
+
check_id: ClassVar[str] = "policy_structure"
|
|
501
|
+
description: ClassVar[str] = (
|
|
502
|
+
"Validates fundamental IAM policy structure (required fields, field conflicts, valid values)"
|
|
503
|
+
)
|
|
504
|
+
default_severity: ClassVar[str] = "error"
|
|
505
|
+
|
|
506
|
+
async def execute_policy(
|
|
507
|
+
self,
|
|
508
|
+
policy: IAMPolicy,
|
|
509
|
+
policy_file: str,
|
|
510
|
+
fetcher: AWSServiceFetcher,
|
|
511
|
+
config: CheckConfig,
|
|
512
|
+
**kwargs,
|
|
513
|
+
) -> list[ValidationIssue]:
|
|
514
|
+
"""Execute the policy structure check on the entire policy.
|
|
515
|
+
|
|
516
|
+
This validates:
|
|
517
|
+
1. Policy document structure (Version, Statement fields)
|
|
518
|
+
2. Each statement's structure (required fields, conflicts, valid values)
|
|
519
|
+
3. Auto-detects policy type for better validation
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
policy: The complete IAM policy to validate
|
|
523
|
+
policy_file: Path to the policy file (unused)
|
|
524
|
+
fetcher: AWS service fetcher (unused)
|
|
525
|
+
config: Check configuration
|
|
526
|
+
**kwargs: Additional arguments (may contain raw_policy_dict)
|
|
527
|
+
|
|
528
|
+
Returns:
|
|
529
|
+
List of ValidationIssue objects for structural problems
|
|
530
|
+
"""
|
|
531
|
+
del policy_file, fetcher, config # Unused
|
|
532
|
+
issues: list[ValidationIssue] = []
|
|
533
|
+
|
|
534
|
+
# Get policy_type from kwargs (passed by validation flow)
|
|
535
|
+
policy_type = kwargs.get("policy_type", "IDENTITY_POLICY")
|
|
536
|
+
|
|
537
|
+
# Validate policy document structure if raw dict is available
|
|
538
|
+
raw_policy_dict = kwargs.get("raw_policy_dict")
|
|
539
|
+
if raw_policy_dict:
|
|
540
|
+
issues.extend(validate_policy_document(raw_policy_dict))
|
|
541
|
+
|
|
542
|
+
# Validate each statement's structure
|
|
543
|
+
if isinstance(raw_policy_dict.get("Statement"), list):
|
|
544
|
+
for idx, stmt_dict in enumerate(raw_policy_dict["Statement"]):
|
|
545
|
+
if isinstance(stmt_dict, dict):
|
|
546
|
+
issues.extend(validate_statement_structure(stmt_dict, idx, policy_type))
|
|
547
|
+
|
|
548
|
+
# Auto-detect policy type
|
|
549
|
+
detected_type = detect_policy_type(policy)
|
|
550
|
+
|
|
551
|
+
# Store detected type in kwargs for other checks to use
|
|
552
|
+
# This allows checks like principal_validation to automatically apply
|
|
553
|
+
if "detected_policy_type" not in kwargs:
|
|
554
|
+
kwargs["detected_policy_type"] = detected_type
|
|
555
|
+
|
|
556
|
+
return issues
|