iam-policy-validator 1.7.2__py3-none-any.whl → 1.9.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.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +127 -6
- iam_policy_validator-1.9.0.dist-info/RECORD +95 -0
- iam_validator/__init__.py +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +559 -207
- iam_validator/checks/action_resource_matching.py +12 -15
- iam_validator/checks/action_validation.py +7 -13
- iam_validator/checks/condition_key_validation.py +7 -13
- iam_validator/checks/condition_type_mismatch.py +15 -22
- iam_validator/checks/full_wildcard.py +9 -13
- iam_validator/checks/mfa_condition_check.py +8 -17
- iam_validator/checks/policy_size.py +6 -39
- iam_validator/checks/policy_structure.py +547 -0
- iam_validator/checks/policy_type_validation.py +61 -46
- iam_validator/checks/principal_validation.py +71 -148
- iam_validator/checks/resource_validation.py +13 -20
- iam_validator/checks/sensitive_action.py +15 -18
- iam_validator/checks/service_wildcard.py +8 -14
- iam_validator/checks/set_operator_validation.py +21 -28
- iam_validator/checks/sid_uniqueness.py +16 -42
- iam_validator/checks/trust_policy_validation.py +506 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +2 -2
- iam_validator/checks/wildcard_action.py +9 -13
- iam_validator/checks/wildcard_resource.py +9 -13
- iam_validator/commands/cache.py +4 -3
- iam_validator/commands/validate.py +15 -9
- iam_validator/core/__init__.py +2 -3
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +24 -1028
- 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 +612 -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 +379 -0
- iam_validator/core/check_registry.py +165 -93
- iam_validator/core/config/condition_requirements.py +69 -17
- iam_validator/core/config/defaults.py +58 -52
- iam_validator/core/config/service_principals.py +40 -3
- iam_validator/core/constants.py +17 -0
- iam_validator/core/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +38 -475
- iam_validator/core/policy_loader.py +27 -4
- iam_validator/sdk/__init__.py +1 -1
- iam_validator/sdk/context.py +1 -1
- iam_validator/sdk/helpers.py +1 -1
- iam_policy_validator-1.7.2.dist-info/RECORD +0 -84
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -11,6 +11,7 @@ This check runs automatically based on:
|
|
|
11
11
|
2. Auto-detection: If any statement has a Principal, provides helpful guidance
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
+
from iam_validator.core.constants import RCP_SUPPORTED_SERVICES
|
|
14
15
|
from iam_validator.core.models import IAMPolicy, ValidationIssue
|
|
15
16
|
|
|
16
17
|
|
|
@@ -30,6 +31,10 @@ async def execute_policy(
|
|
|
30
31
|
"""
|
|
31
32
|
issues = []
|
|
32
33
|
|
|
34
|
+
# Handle policies with no statements
|
|
35
|
+
if not policy.statement:
|
|
36
|
+
return issues
|
|
37
|
+
|
|
33
38
|
# Check if any statement has Principal
|
|
34
39
|
has_any_principal = any(
|
|
35
40
|
stmt.principal is not None or stmt.not_principal is not None for stmt in policy.statement
|
|
@@ -37,24 +42,36 @@ async def execute_policy(
|
|
|
37
42
|
|
|
38
43
|
# If policy has Principal but type is IDENTITY_POLICY (default), provide helpful info
|
|
39
44
|
if has_any_principal and policy_type == "IDENTITY_POLICY":
|
|
45
|
+
# Check if it's a trust policy
|
|
46
|
+
from iam_validator.checks.policy_structure import is_trust_policy
|
|
47
|
+
|
|
48
|
+
if is_trust_policy(policy):
|
|
49
|
+
hint_msg = (
|
|
50
|
+
"Policy contains assume role actions - this is a TRUST POLICY. "
|
|
51
|
+
"Use `--policy-type TRUST_POLICY` for proper validation (suppresses missing Resource warnings, "
|
|
52
|
+
"enables trust-specific validation)"
|
|
53
|
+
)
|
|
54
|
+
suggestion_msg = "iam-validator validate --path <file> --policy-type TRUST_POLICY"
|
|
55
|
+
else:
|
|
56
|
+
hint_msg = "Policy contains Principal element - this suggests it's a RESOURCE POLICY. Use `--policy-type RESOURCE_POLICY`"
|
|
57
|
+
suggestion_msg = "iam-validator validate --path <file> --policy-type RESOURCE_POLICY"
|
|
58
|
+
|
|
40
59
|
issues.append(
|
|
41
60
|
ValidationIssue(
|
|
42
61
|
severity="info",
|
|
43
62
|
issue_type="policy_type_hint",
|
|
44
|
-
message=
|
|
45
|
-
"Use --policy-type RESOURCE_POLICY for proper validation.",
|
|
63
|
+
message=hint_msg,
|
|
46
64
|
statement_index=0,
|
|
47
65
|
statement_sid=None,
|
|
48
66
|
line_number=None,
|
|
49
|
-
suggestion=
|
|
50
|
-
"run validation with: iam-validator validate --path <file> --policy-type RESOURCE_POLICY",
|
|
67
|
+
suggestion=suggestion_msg,
|
|
51
68
|
)
|
|
52
69
|
)
|
|
53
70
|
# Don't run further checks if we're just hinting
|
|
54
71
|
return issues
|
|
55
72
|
|
|
56
|
-
# Resource policies MUST have Principal
|
|
57
|
-
if policy_type
|
|
73
|
+
# Resource policies and Trust policies MUST have Principal
|
|
74
|
+
if policy_type in ("RESOURCE_POLICY", "TRUST_POLICY"):
|
|
58
75
|
for idx, statement in enumerate(policy.statement):
|
|
59
76
|
has_principal = statement.principal is not None or statement.not_principal is not None
|
|
60
77
|
|
|
@@ -63,13 +80,13 @@ async def execute_policy(
|
|
|
63
80
|
ValidationIssue(
|
|
64
81
|
severity="error",
|
|
65
82
|
issue_type="missing_principal",
|
|
66
|
-
message="Resource policy statement missing required Principal element. "
|
|
83
|
+
message="Resource policy statement missing required `Principal` element. "
|
|
67
84
|
"Resource-based policies (S3 bucket policies, SNS topic policies, etc.) "
|
|
68
|
-
"must include a Principal element to specify who can access the resource.",
|
|
85
|
+
"must include a `Principal` element to specify who can access the resource.",
|
|
69
86
|
statement_index=idx,
|
|
70
87
|
statement_sid=statement.sid,
|
|
71
88
|
line_number=statement.line_number,
|
|
72
|
-
suggestion="Add a Principal element to specify who can access this resource.\n"
|
|
89
|
+
suggestion="Add a `Principal` element to specify who can access this resource.\n"
|
|
73
90
|
"Example:\n"
|
|
74
91
|
"```json\n"
|
|
75
92
|
"{\n"
|
|
@@ -94,14 +111,14 @@ async def execute_policy(
|
|
|
94
111
|
ValidationIssue(
|
|
95
112
|
severity="warning",
|
|
96
113
|
issue_type="unexpected_principal",
|
|
97
|
-
message="Identity policy should not contain Principal element. "
|
|
114
|
+
message="Identity policy should not contain `Principal` element. "
|
|
98
115
|
"Identity-based policies (attached to IAM users, groups, or roles) "
|
|
99
|
-
"do not need a Principal element because the principal is implicit "
|
|
116
|
+
"do not need a `Principal` element because the principal is implicit "
|
|
100
117
|
"(the entity the policy is attached to).",
|
|
101
118
|
statement_index=idx,
|
|
102
119
|
statement_sid=statement.sid,
|
|
103
120
|
line_number=statement.line_number,
|
|
104
|
-
suggestion="Remove the Principal element from this identity policy statement.\n"
|
|
121
|
+
suggestion="Remove the `Principal` element from this identity policy statement.\n"
|
|
105
122
|
"Example:\n"
|
|
106
123
|
"```json\n"
|
|
107
124
|
"{\n"
|
|
@@ -123,13 +140,13 @@ async def execute_policy(
|
|
|
123
140
|
ValidationIssue(
|
|
124
141
|
severity="error",
|
|
125
142
|
issue_type="invalid_principal",
|
|
126
|
-
message="Service Control Policy must not contain Principal element. "
|
|
143
|
+
message="Service Control Policy must not contain `Principal` element. "
|
|
127
144
|
"Service Control Policies (SCPs) in AWS Organizations do not support "
|
|
128
|
-
"the Principal element. They apply to all principals in the organization or OU.",
|
|
145
|
+
"the `Principal` element. They apply to all principals in the organization or OU.",
|
|
129
146
|
statement_index=idx,
|
|
130
147
|
statement_sid=statement.sid,
|
|
131
148
|
line_number=statement.line_number,
|
|
132
|
-
suggestion="Remove the Principal element from this SCP statement.\n"
|
|
149
|
+
suggestion="Remove the `Principal` element from this SCP statement.\n"
|
|
133
150
|
"Example:\n"
|
|
134
151
|
"```json\n"
|
|
135
152
|
"{\n"
|
|
@@ -148,8 +165,8 @@ async def execute_policy(
|
|
|
148
165
|
|
|
149
166
|
# Resource Control Policies (RCPs) have very strict requirements
|
|
150
167
|
elif policy_type == "RESOURCE_CONTROL_POLICY":
|
|
151
|
-
# RCP supported services
|
|
152
|
-
rcp_supported_services =
|
|
168
|
+
# Use the centralized list of RCP supported services from constants
|
|
169
|
+
rcp_supported_services = RCP_SUPPORTED_SERVICES
|
|
153
170
|
|
|
154
171
|
for idx, statement in enumerate(policy.statement):
|
|
155
172
|
# 1. Effect MUST be Deny (only RCPFullAWSAccess can use Allow)
|
|
@@ -158,13 +175,13 @@ async def execute_policy(
|
|
|
158
175
|
ValidationIssue(
|
|
159
176
|
severity="error",
|
|
160
177
|
issue_type="invalid_rcp_effect",
|
|
161
|
-
message="Resource Control Policy statement must have Effect: Deny
|
|
162
|
-
"For RCPs that you create, the Effect value must be
|
|
163
|
-
"Only the AWS-managed RCPFullAWSAccess policy can use
|
|
178
|
+
message="Resource Control Policy statement must have `Effect: Deny`. "
|
|
179
|
+
"For RCPs that you create, the `Effect` value must be `Deny`. "
|
|
180
|
+
"Only the AWS-managed `RCPFullAWSAccess` policy can use `Allow`.",
|
|
164
181
|
statement_index=idx,
|
|
165
182
|
statement_sid=statement.sid,
|
|
166
183
|
line_number=statement.line_number,
|
|
167
|
-
suggestion=
|
|
184
|
+
suggestion="Change the `Effect` to `Deny` for this RCP statement.",
|
|
168
185
|
)
|
|
169
186
|
)
|
|
170
187
|
|
|
@@ -177,14 +194,13 @@ async def execute_policy(
|
|
|
177
194
|
ValidationIssue(
|
|
178
195
|
severity="error",
|
|
179
196
|
issue_type="invalid_rcp_not_principal",
|
|
180
|
-
message="Resource Control Policy must not contain NotPrincipal element. "
|
|
181
|
-
"RCPs only support Principal with value
|
|
197
|
+
message="Resource Control Policy must not contain `NotPrincipal` element. "
|
|
198
|
+
"RCPs only support `Principal` with value `*`. Use `Condition` elements "
|
|
182
199
|
"to restrict specific principals.",
|
|
183
200
|
statement_index=idx,
|
|
184
201
|
statement_sid=statement.sid,
|
|
185
202
|
line_number=statement.line_number,
|
|
186
|
-
suggestion=
|
|
187
|
-
"elements to restrict access.",
|
|
203
|
+
suggestion='Remove `NotPrincipal` and use `Principal: "*"` with `Condition` elements to restrict access.',
|
|
188
204
|
)
|
|
189
205
|
)
|
|
190
206
|
elif not has_principal:
|
|
@@ -192,13 +208,13 @@ async def execute_policy(
|
|
|
192
208
|
ValidationIssue(
|
|
193
209
|
severity="error",
|
|
194
210
|
issue_type="missing_rcp_principal",
|
|
195
|
-
message=
|
|
196
|
-
|
|
211
|
+
message='Resource Control Policy statement must have `Principal: "*"`. '
|
|
212
|
+
'RCPs require the `Principal` element with value `"*"`. Use `Condition` '
|
|
197
213
|
"elements to restrict specific principals.",
|
|
198
214
|
statement_index=idx,
|
|
199
215
|
statement_sid=statement.sid,
|
|
200
216
|
line_number=statement.line_number,
|
|
201
|
-
suggestion='Add Principal: "*" to this RCP statement.',
|
|
217
|
+
suggestion='Add `Principal: "*"` to this RCP statement.',
|
|
202
218
|
)
|
|
203
219
|
)
|
|
204
220
|
elif statement.principal != "*":
|
|
@@ -209,13 +225,13 @@ async def execute_policy(
|
|
|
209
225
|
ValidationIssue(
|
|
210
226
|
severity="error",
|
|
211
227
|
issue_type="invalid_rcp_principal",
|
|
212
|
-
message=
|
|
213
|
-
f
|
|
214
|
-
"Principal element. Use Condition elements to restrict specific principals.",
|
|
228
|
+
message=f'Resource Control Policy `Principal` must be `"*"`. '
|
|
229
|
+
f'Found: `{statement.principal}`. RCPs can only specify `"*"` in the '
|
|
230
|
+
"`Principal` element. Use `Condition` elements to restrict specific principals.",
|
|
215
231
|
statement_index=idx,
|
|
216
232
|
statement_sid=statement.sid,
|
|
217
233
|
line_number=statement.line_number,
|
|
218
|
-
suggestion='Change Principal to "*" and use Condition elements to restrict access.',
|
|
234
|
+
suggestion='Change `Principal` to `"*"` and use `Condition` elements to restrict access.',
|
|
219
235
|
)
|
|
220
236
|
)
|
|
221
237
|
|
|
@@ -234,14 +250,14 @@ async def execute_policy(
|
|
|
234
250
|
ValidationIssue(
|
|
235
251
|
severity="error",
|
|
236
252
|
issue_type="invalid_rcp_wildcard_action",
|
|
237
|
-
message="Resource Control Policy must not use
|
|
238
|
-
"Customer-managed RCPs cannot use
|
|
239
|
-
"Use service-specific wildcards like
|
|
253
|
+
message="Resource Control Policy must not use `*` alone in `Action` element. "
|
|
254
|
+
"Customer-managed RCPs cannot use `*` as the action wildcard. "
|
|
255
|
+
"Use service-specific wildcards like `s3:*` instead.",
|
|
240
256
|
statement_index=idx,
|
|
241
257
|
statement_sid=statement.sid,
|
|
242
258
|
line_number=statement.line_number,
|
|
243
|
-
suggestion="Replace
|
|
244
|
-
f"services: {', '.join(sorted(rcp_supported_services))}",
|
|
259
|
+
suggestion="Replace `*` with service-specific actions from supported "
|
|
260
|
+
f"services: {', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
|
|
245
261
|
)
|
|
246
262
|
)
|
|
247
263
|
else:
|
|
@@ -259,13 +275,13 @@ async def execute_policy(
|
|
|
259
275
|
severity="error",
|
|
260
276
|
issue_type="unsupported_rcp_service",
|
|
261
277
|
message=f"Resource Control Policy contains actions from unsupported services: "
|
|
262
|
-
f"{', '.join(unsupported_actions)}. RCPs only support these services: "
|
|
263
|
-
f"{', '.join(sorted(rcp_supported_services))}",
|
|
278
|
+
f"{', '.join(f'`{a}`' for a in unsupported_actions)}. RCPs only support these services: "
|
|
279
|
+
f"{', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
|
|
264
280
|
statement_index=idx,
|
|
265
281
|
statement_sid=statement.sid,
|
|
266
282
|
line_number=statement.line_number,
|
|
267
283
|
suggestion=f"Use only actions from supported RCP services: "
|
|
268
|
-
f"{', '.join(sorted(rcp_supported_services))}",
|
|
284
|
+
f"{', '.join(f'`{a}`' for a in sorted(rcp_supported_services))}",
|
|
269
285
|
)
|
|
270
286
|
)
|
|
271
287
|
|
|
@@ -275,13 +291,12 @@ async def execute_policy(
|
|
|
275
291
|
ValidationIssue(
|
|
276
292
|
severity="error",
|
|
277
293
|
issue_type="invalid_rcp_not_action",
|
|
278
|
-
message="Resource Control Policy must not contain NotAction element. "
|
|
279
|
-
"RCPs do not support NotAction
|
|
294
|
+
message="Resource Control Policy must not contain `NotAction` element. "
|
|
295
|
+
"RCPs do not support `NotAction`. Use `Action` element instead.",
|
|
280
296
|
statement_index=idx,
|
|
281
297
|
statement_sid=statement.sid,
|
|
282
298
|
line_number=statement.line_number,
|
|
283
|
-
suggestion="Replace NotAction with Action element listing the specific "
|
|
284
|
-
"actions to deny.",
|
|
299
|
+
suggestion="Replace `NotAction` with `Action` element listing the specific actions to deny.",
|
|
285
300
|
)
|
|
286
301
|
)
|
|
287
302
|
|
|
@@ -294,11 +309,11 @@ async def execute_policy(
|
|
|
294
309
|
ValidationIssue(
|
|
295
310
|
severity="error",
|
|
296
311
|
issue_type="missing_rcp_resource",
|
|
297
|
-
message="Resource Control Policy statement must have Resource or NotResource element.",
|
|
312
|
+
message="Resource Control Policy statement must have `Resource` or `NotResource` element.",
|
|
298
313
|
statement_index=idx,
|
|
299
314
|
statement_sid=statement.sid,
|
|
300
315
|
line_number=statement.line_number,
|
|
301
|
-
suggestion='Add Resource: "*" or specify specific resource ARNs.',
|
|
316
|
+
suggestion='Add `Resource: "*"` or specify specific resource ARNs.',
|
|
302
317
|
)
|
|
303
318
|
)
|
|
304
319
|
|
|
@@ -4,60 +4,53 @@ Validates Principal elements in resource-based policies for security best practi
|
|
|
4
4
|
This check enforces:
|
|
5
5
|
- Blocked principals (e.g., public access via "*")
|
|
6
6
|
- Allowed principals whitelist (optional)
|
|
7
|
-
-
|
|
8
|
-
- Rich condition requirements for principals (advanced format with all_of/any_of)
|
|
7
|
+
- Rich condition requirements for principals (supports any_of/all_of/none_of)
|
|
9
8
|
- Service principal validation
|
|
10
9
|
|
|
11
|
-
Only runs for RESOURCE_POLICY
|
|
12
|
-
|
|
13
|
-
Configuration
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
operator: "StringEquals"
|
|
10
|
+
Only runs for RESOURCE_POLICY and TRUST_POLICY types.
|
|
11
|
+
|
|
12
|
+
Configuration format:
|
|
13
|
+
|
|
14
|
+
principal_condition_requirements:
|
|
15
|
+
- principals:
|
|
16
|
+
- "*" # Can be a list of principal patterns
|
|
17
|
+
severity: critical # Optional: override default severity
|
|
18
|
+
required_conditions:
|
|
19
|
+
any_of: # At least ONE of these conditions must be present
|
|
20
|
+
- condition_key: "aws:SourceArn"
|
|
21
|
+
description: "Limit by source ARN"
|
|
22
|
+
- condition_key: "aws:SourceAccount"
|
|
23
|
+
expected_value: "123456789012" # Optional: validate specific value
|
|
24
|
+
operator: "StringEquals" # Optional: validate specific operator
|
|
25
|
+
|
|
26
|
+
- principals:
|
|
27
|
+
- "arn:aws:iam::*:root"
|
|
28
|
+
required_conditions:
|
|
29
|
+
all_of: # ALL of these conditions must be present
|
|
30
|
+
- condition_key: "aws:PrincipalOrgID"
|
|
31
|
+
expected_value: "o-xxxxx"
|
|
32
|
+
- condition_key: "aws:SourceAccount"
|
|
33
|
+
|
|
34
|
+
Supports: any_of, all_of, none_of, and expected_value (single value or list)
|
|
37
35
|
"""
|
|
38
36
|
|
|
39
37
|
import fnmatch
|
|
40
|
-
from typing import Any
|
|
38
|
+
from typing import Any, ClassVar
|
|
41
39
|
|
|
42
|
-
from iam_validator.core.
|
|
40
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
43
41
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
42
|
+
from iam_validator.core.config.service_principals import is_aws_service_principal
|
|
44
43
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
45
44
|
|
|
46
45
|
|
|
47
46
|
class PrincipalValidationCheck(PolicyCheck):
|
|
48
47
|
"""Validates Principal elements in resource policies."""
|
|
49
48
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def description(self) -> str:
|
|
56
|
-
return "Validates Principal elements in resource policies for security best practices"
|
|
57
|
-
|
|
58
|
-
@property
|
|
59
|
-
def default_severity(self) -> str:
|
|
60
|
-
return "high"
|
|
49
|
+
check_id: ClassVar[str] = "principal_validation"
|
|
50
|
+
description: ClassVar[str] = (
|
|
51
|
+
"Validates Principal elements in resource policies for security best practices"
|
|
52
|
+
)
|
|
53
|
+
default_severity: ClassVar[str] = "high"
|
|
61
54
|
|
|
62
55
|
async def execute(
|
|
63
56
|
self,
|
|
@@ -83,22 +76,13 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
83
76
|
if statement.principal is None and statement.not_principal is None:
|
|
84
77
|
return issues
|
|
85
78
|
|
|
86
|
-
# Get configuration
|
|
79
|
+
# Get configuration (defaults match defaults.py)
|
|
87
80
|
blocked_principals = config.config.get("blocked_principals", ["*"])
|
|
88
81
|
allowed_principals = config.config.get("allowed_principals", [])
|
|
89
|
-
require_conditions_for = config.config.get("require_conditions_for", {})
|
|
90
82
|
principal_condition_requirements = config.config.get("principal_condition_requirements", [])
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"cloudfront.amazonaws.com",
|
|
95
|
-
"s3.amazonaws.com",
|
|
96
|
-
"sns.amazonaws.com",
|
|
97
|
-
"lambda.amazonaws.com",
|
|
98
|
-
"logs.amazonaws.com",
|
|
99
|
-
"events.amazonaws.com",
|
|
100
|
-
],
|
|
101
|
-
)
|
|
83
|
+
# Default: "aws:*" allows ALL AWS service principals (*.amazonaws.com)
|
|
84
|
+
# This matches the default in defaults.py:251
|
|
85
|
+
allowed_service_principals = config.config.get("allowed_service_principals", ["aws:*"])
|
|
102
86
|
|
|
103
87
|
# Extract principals from statement
|
|
104
88
|
principals = self._extract_principals(statement)
|
|
@@ -117,8 +101,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
117
101
|
statement_index=statement_idx,
|
|
118
102
|
statement_sid=statement.sid,
|
|
119
103
|
line_number=statement.line_number,
|
|
120
|
-
suggestion=f"Remove the
|
|
121
|
-
"Consider using more specific
|
|
104
|
+
suggestion=f"Remove the `Principal` `{principal}` or add appropriate `Condition`s to restrict access. "
|
|
105
|
+
"Consider using more specific `Principal`s instead of `*` (wildcard).",
|
|
122
106
|
)
|
|
123
107
|
)
|
|
124
108
|
continue
|
|
@@ -131,42 +115,18 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
131
115
|
ValidationIssue(
|
|
132
116
|
severity=self.get_severity(config),
|
|
133
117
|
issue_type="unauthorized_principal",
|
|
134
|
-
message=f"Principal not in allowed list: `{principal}`. "
|
|
135
|
-
f"Only principals in the `allowed_principals`
|
|
118
|
+
message=f"`Principal` not in allowed list: `{principal}`. "
|
|
119
|
+
f"Only principals in the `allowed_principals` allow-list are permitted.",
|
|
136
120
|
statement_index=statement_idx,
|
|
137
121
|
statement_sid=statement.sid,
|
|
138
122
|
line_number=statement.line_number,
|
|
139
|
-
suggestion=f"Add
|
|
140
|
-
"or use a
|
|
123
|
+
suggestion=f"Add `{principal}` to the `allowed_principals` list in your config, "
|
|
124
|
+
"or use a `Principal` that matches an allowed pattern.",
|
|
141
125
|
)
|
|
142
126
|
)
|
|
143
127
|
continue
|
|
144
128
|
|
|
145
|
-
|
|
146
|
-
required_conditions = self._get_required_conditions(principal, require_conditions_for)
|
|
147
|
-
if required_conditions:
|
|
148
|
-
missing_conditions = self._check_required_conditions(statement, required_conditions)
|
|
149
|
-
if missing_conditions:
|
|
150
|
-
issues.append(
|
|
151
|
-
ValidationIssue(
|
|
152
|
-
severity=self.get_severity(config),
|
|
153
|
-
issue_type="missing_principal_conditions",
|
|
154
|
-
message=f"Principal `{principal}` requires conditions: {', '.join(f'`{c}`' for c in missing_conditions)}. "
|
|
155
|
-
f"This principal must have these condition keys to restrict access.",
|
|
156
|
-
statement_index=statement_idx,
|
|
157
|
-
statement_sid=statement.sid,
|
|
158
|
-
line_number=statement.line_number,
|
|
159
|
-
suggestion=f"Add conditions to restrict access:\n"
|
|
160
|
-
f"Example:\n"
|
|
161
|
-
f'"Condition": {{\n'
|
|
162
|
-
f' "StringEquals": {{\n'
|
|
163
|
-
f' "{missing_conditions[0]}": "value"\n'
|
|
164
|
-
f" }}\n"
|
|
165
|
-
f"}}",
|
|
166
|
-
)
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
# Check advanced format: principal_condition_requirements
|
|
129
|
+
# Check principal_condition_requirements (supports any_of/all_of/none_of)
|
|
170
130
|
if principal_condition_requirements:
|
|
171
131
|
condition_issues = self._validate_principal_condition_requirements(
|
|
172
132
|
statement,
|
|
@@ -197,7 +157,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
197
157
|
principals.append(statement.principal)
|
|
198
158
|
elif isinstance(statement.principal, dict):
|
|
199
159
|
# Dict with AWS, Service, Federated, etc.
|
|
200
|
-
for
|
|
160
|
+
for _, value in statement.principal.items():
|
|
201
161
|
if isinstance(value, str):
|
|
202
162
|
principals.append(value)
|
|
203
163
|
elif isinstance(value, list):
|
|
@@ -208,7 +168,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
208
168
|
if isinstance(statement.not_principal, str):
|
|
209
169
|
principals.append(statement.not_principal)
|
|
210
170
|
elif isinstance(statement.not_principal, dict):
|
|
211
|
-
for
|
|
171
|
+
for _, value in statement.not_principal.items():
|
|
212
172
|
if isinstance(value, str):
|
|
213
173
|
principals.append(value)
|
|
214
174
|
elif isinstance(value, list):
|
|
@@ -224,13 +184,17 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
224
184
|
Args:
|
|
225
185
|
principal: The principal to check
|
|
226
186
|
blocked_list: List of blocked principal patterns
|
|
227
|
-
service_whitelist: List of allowed service principals
|
|
187
|
+
service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
|
|
228
188
|
|
|
229
189
|
Returns:
|
|
230
190
|
True if the principal is blocked
|
|
231
191
|
"""
|
|
232
|
-
#
|
|
233
|
-
if
|
|
192
|
+
# Check if service_whitelist contains "aws:*" (allow all AWS service principals)
|
|
193
|
+
if "aws:*" in service_whitelist and is_aws_service_principal(principal):
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
# Service principals in explicit whitelist are never blocked
|
|
197
|
+
if is_aws_service_principal(principal) and principal in service_whitelist:
|
|
234
198
|
return False
|
|
235
199
|
|
|
236
200
|
# Check against blocked list (supports wildcards)
|
|
@@ -253,13 +217,17 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
253
217
|
Args:
|
|
254
218
|
principal: The principal to check
|
|
255
219
|
allowed_list: List of allowed principal patterns
|
|
256
|
-
service_whitelist: List of allowed service principals
|
|
220
|
+
service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
|
|
257
221
|
|
|
258
222
|
Returns:
|
|
259
223
|
True if the principal is allowed
|
|
260
224
|
"""
|
|
261
|
-
#
|
|
262
|
-
if
|
|
225
|
+
# Check if service_whitelist contains "aws:*" (allow all AWS service principals)
|
|
226
|
+
if "aws:*" in service_whitelist and is_aws_service_principal(principal):
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
# Service principals in explicit whitelist are always allowed
|
|
230
|
+
if is_aws_service_principal(principal) and principal in service_whitelist:
|
|
263
231
|
return True
|
|
264
232
|
|
|
265
233
|
# Check against allowed list (supports wildcards)
|
|
@@ -274,52 +242,6 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
274
242
|
|
|
275
243
|
return False
|
|
276
244
|
|
|
277
|
-
def _get_required_conditions(
|
|
278
|
-
self, principal: str, requirements: dict[str, list[str]]
|
|
279
|
-
) -> list[str]:
|
|
280
|
-
"""Get required condition keys for a principal.
|
|
281
|
-
|
|
282
|
-
Args:
|
|
283
|
-
principal: The principal to check
|
|
284
|
-
requirements: Dict mapping principal patterns to required condition keys
|
|
285
|
-
|
|
286
|
-
Returns:
|
|
287
|
-
List of required condition keys
|
|
288
|
-
"""
|
|
289
|
-
for pattern, condition_keys in requirements.items():
|
|
290
|
-
# Special case: "*" pattern should only match literal "*" (public access)
|
|
291
|
-
if pattern == "*":
|
|
292
|
-
if principal == "*":
|
|
293
|
-
return condition_keys
|
|
294
|
-
elif fnmatch.fnmatch(principal, pattern):
|
|
295
|
-
return condition_keys
|
|
296
|
-
return []
|
|
297
|
-
|
|
298
|
-
def _check_required_conditions(
|
|
299
|
-
self, statement: Statement, required_keys: list[str]
|
|
300
|
-
) -> list[str]:
|
|
301
|
-
"""Check if statement has required condition keys.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
statement: The statement to check
|
|
305
|
-
required_keys: List of required condition keys
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
List of missing condition keys
|
|
309
|
-
"""
|
|
310
|
-
if not statement.condition:
|
|
311
|
-
return required_keys
|
|
312
|
-
|
|
313
|
-
# Flatten all condition keys from all condition operators
|
|
314
|
-
present_keys = set()
|
|
315
|
-
for operator_conditions in statement.condition.values():
|
|
316
|
-
if isinstance(operator_conditions, dict):
|
|
317
|
-
present_keys.update(operator_conditions.keys())
|
|
318
|
-
|
|
319
|
-
# Find missing keys
|
|
320
|
-
missing = [key for key in required_keys if key not in present_keys]
|
|
321
|
-
return missing
|
|
322
|
-
|
|
323
245
|
def _validate_principal_condition_requirements(
|
|
324
246
|
self,
|
|
325
247
|
statement: Statement,
|
|
@@ -480,8 +402,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
480
402
|
statement_index=statement_idx,
|
|
481
403
|
issue_type="missing_principal_condition_any_of",
|
|
482
404
|
message=(
|
|
483
|
-
f"
|
|
484
|
-
f"{', '.join(condition_keys)}"
|
|
405
|
+
f"`Principal`s `{', '.join(f'`{p}`' for p in matching_principals)}` require at least ONE of these conditions: "
|
|
406
|
+
f"{', '.join(f'`{c}`' for c in condition_keys)}"
|
|
485
407
|
),
|
|
486
408
|
suggestion=self._build_any_of_suggestion(any_of),
|
|
487
409
|
line_number=statement.line_number,
|
|
@@ -642,7 +564,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
642
564
|
statement_sid=statement.sid,
|
|
643
565
|
statement_index=statement_idx,
|
|
644
566
|
issue_type="missing_principal_condition",
|
|
645
|
-
message=f"{message_prefix} Principal(s) {matching_principals} require condition
|
|
567
|
+
message=f"{message_prefix} Principal(s) {', '.join(f'`{p}`' for p in matching_principals)} require condition `{condition_key}`",
|
|
646
568
|
suggestion=suggestion_text,
|
|
647
569
|
example=example_code,
|
|
648
570
|
line_number=statement.line_number,
|
|
@@ -668,7 +590,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
668
590
|
Returns:
|
|
669
591
|
Tuple of (suggestion_text, example_code)
|
|
670
592
|
"""
|
|
671
|
-
suggestion = description if description else f"Add condition: {condition_key}"
|
|
593
|
+
suggestion = description if description else f"Add condition: `{condition_key}`"
|
|
672
594
|
|
|
673
595
|
# Build example based on condition key type
|
|
674
596
|
if example:
|
|
@@ -723,11 +645,11 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
723
645
|
description = cond.get("description", "")
|
|
724
646
|
expected_value = cond.get("expected_value")
|
|
725
647
|
|
|
726
|
-
option = f"\
|
|
648
|
+
option = f"\n- **Option {i}**: `{condition_key}`"
|
|
727
649
|
if description:
|
|
728
650
|
option += f" - {description}"
|
|
729
651
|
if expected_value is not None:
|
|
730
|
-
option += f" (value: {expected_value})"
|
|
652
|
+
option += f" (value: `{expected_value}`)"
|
|
731
653
|
|
|
732
654
|
suggestions.append(option)
|
|
733
655
|
|
|
@@ -759,11 +681,12 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
759
681
|
description = condition_requirement.get("description", "")
|
|
760
682
|
expected_value = condition_requirement.get("expected_value")
|
|
761
683
|
|
|
762
|
-
|
|
684
|
+
matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
|
|
685
|
+
message = f"FORBIDDEN: `Principal`s `{matching_principals_str}` must NOT have `Condition` `{condition_key}`"
|
|
763
686
|
if expected_value is not None:
|
|
764
|
-
message += f" with value
|
|
687
|
+
message += f" with value `{expected_value}`"
|
|
765
688
|
|
|
766
|
-
suggestion = f"Remove the
|
|
689
|
+
suggestion = f"Remove the `{condition_key}` `Condition` from the statement"
|
|
767
690
|
if description:
|
|
768
691
|
suggestion += f". {description}"
|
|
769
692
|
|