iam-policy-validator 1.7.2__py3-none-any.whl → 1.8.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.8.0.dist-info}/METADATA +22 -6
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/RECORD +38 -35
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +5 -3
- iam_validator/checks/action_condition_enforcement.py +61 -23
- iam_validator/checks/action_resource_matching.py +6 -2
- iam_validator/checks/action_validation.py +1 -1
- iam_validator/checks/condition_key_validation.py +1 -1
- iam_validator/checks/condition_type_mismatch.py +6 -6
- iam_validator/checks/policy_structure.py +577 -0
- iam_validator/checks/policy_type_validation.py +48 -32
- iam_validator/checks/principal_validation.py +65 -133
- iam_validator/checks/resource_validation.py +8 -8
- iam_validator/checks/sensitive_action.py +7 -3
- iam_validator/checks/service_wildcard.py +2 -2
- iam_validator/checks/set_operator_validation.py +11 -11
- iam_validator/checks/sid_uniqueness.py +8 -4
- iam_validator/checks/trust_policy_validation.py +512 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +26 -26
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +3 -1
- iam_validator/checks/wildcard_resource.py +3 -1
- iam_validator/commands/validate.py +6 -12
- iam_validator/core/__init__.py +1 -2
- iam_validator/core/access_analyzer.py +1 -1
- iam_validator/core/access_analyzer_report.py +2 -2
- iam_validator/core/aws_fetcher.py +45 -43
- iam_validator/core/check_registry.py +83 -79
- 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/ignore_patterns.py +297 -0
- iam_validator/core/models.py +15 -5
- iam_validator/core/policy_checks.py +31 -472
- iam_validator/core/policy_loader.py +27 -4
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.2.dist-info → iam_policy_validator-1.8.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -30,6 +30,10 @@ async def execute_policy(
|
|
|
30
30
|
"""
|
|
31
31
|
issues = []
|
|
32
32
|
|
|
33
|
+
# Handle policies with no statements
|
|
34
|
+
if not policy.statement:
|
|
35
|
+
return issues
|
|
36
|
+
|
|
33
37
|
# Check if any statement has Principal
|
|
34
38
|
has_any_principal = any(
|
|
35
39
|
stmt.principal is not None or stmt.not_principal is not None for stmt in policy.statement
|
|
@@ -37,24 +41,36 @@ async def execute_policy(
|
|
|
37
41
|
|
|
38
42
|
# If policy has Principal but type is IDENTITY_POLICY (default), provide helpful info
|
|
39
43
|
if has_any_principal and policy_type == "IDENTITY_POLICY":
|
|
44
|
+
# Check if it's a trust policy
|
|
45
|
+
from iam_validator.checks.policy_structure import is_trust_policy
|
|
46
|
+
|
|
47
|
+
if is_trust_policy(policy):
|
|
48
|
+
hint_msg = (
|
|
49
|
+
"Policy contains assume role actions - this is a TRUST POLICY. "
|
|
50
|
+
"Use --policy-type TRUST_POLICY for proper validation (suppresses missing Resource warnings, "
|
|
51
|
+
"enables trust-specific validation)"
|
|
52
|
+
)
|
|
53
|
+
suggestion_msg = "iam-validator validate --path <file> --policy-type TRUST_POLICY"
|
|
54
|
+
else:
|
|
55
|
+
hint_msg = "Policy contains Principal element - this suggests it's a RESOURCE POLICY. Use --policy-type RESOURCE_POLICY"
|
|
56
|
+
suggestion_msg = "iam-validator validate --path <file> --policy-type RESOURCE_POLICY"
|
|
57
|
+
|
|
40
58
|
issues.append(
|
|
41
59
|
ValidationIssue(
|
|
42
60
|
severity="info",
|
|
43
61
|
issue_type="policy_type_hint",
|
|
44
|
-
message=
|
|
45
|
-
"Use --policy-type RESOURCE_POLICY for proper validation.",
|
|
62
|
+
message=hint_msg,
|
|
46
63
|
statement_index=0,
|
|
47
64
|
statement_sid=None,
|
|
48
65
|
line_number=None,
|
|
49
|
-
suggestion=
|
|
50
|
-
"run validation with: iam-validator validate --path <file> --policy-type RESOURCE_POLICY",
|
|
66
|
+
suggestion=suggestion_msg,
|
|
51
67
|
)
|
|
52
68
|
)
|
|
53
69
|
# Don't run further checks if we're just hinting
|
|
54
70
|
return issues
|
|
55
71
|
|
|
56
|
-
# Resource policies MUST have Principal
|
|
57
|
-
if policy_type
|
|
72
|
+
# Resource policies and Trust policies MUST have Principal
|
|
73
|
+
if policy_type in ("RESOURCE_POLICY", "TRUST_POLICY"):
|
|
58
74
|
for idx, statement in enumerate(policy.statement):
|
|
59
75
|
has_principal = statement.principal is not None or statement.not_principal is not None
|
|
60
76
|
|
|
@@ -63,13 +79,13 @@ async def execute_policy(
|
|
|
63
79
|
ValidationIssue(
|
|
64
80
|
severity="error",
|
|
65
81
|
issue_type="missing_principal",
|
|
66
|
-
message="Resource policy statement missing required Principal element. "
|
|
82
|
+
message="Resource policy statement missing required `Principal` element. "
|
|
67
83
|
"Resource-based policies (S3 bucket policies, SNS topic policies, etc.) "
|
|
68
|
-
"must include a Principal element to specify who can access the resource.",
|
|
84
|
+
"must include a `Principal` element to specify who can access the resource.",
|
|
69
85
|
statement_index=idx,
|
|
70
86
|
statement_sid=statement.sid,
|
|
71
87
|
line_number=statement.line_number,
|
|
72
|
-
suggestion="Add a Principal element to specify who can access this resource.\n"
|
|
88
|
+
suggestion="Add a `Principal` element to specify who can access this resource.\n"
|
|
73
89
|
"Example:\n"
|
|
74
90
|
"```json\n"
|
|
75
91
|
"{\n"
|
|
@@ -94,14 +110,14 @@ async def execute_policy(
|
|
|
94
110
|
ValidationIssue(
|
|
95
111
|
severity="warning",
|
|
96
112
|
issue_type="unexpected_principal",
|
|
97
|
-
message="Identity policy should not contain Principal element. "
|
|
113
|
+
message="Identity policy should not contain `Principal` element. "
|
|
98
114
|
"Identity-based policies (attached to IAM users, groups, or roles) "
|
|
99
|
-
"do not need a Principal element because the principal is implicit "
|
|
115
|
+
"do not need a `Principal` element because the principal is implicit "
|
|
100
116
|
"(the entity the policy is attached to).",
|
|
101
117
|
statement_index=idx,
|
|
102
118
|
statement_sid=statement.sid,
|
|
103
119
|
line_number=statement.line_number,
|
|
104
|
-
suggestion="Remove the Principal element from this identity policy statement.\n"
|
|
120
|
+
suggestion="Remove the `Principal` element from this identity policy statement.\n"
|
|
105
121
|
"Example:\n"
|
|
106
122
|
"```json\n"
|
|
107
123
|
"{\n"
|
|
@@ -123,13 +139,13 @@ async def execute_policy(
|
|
|
123
139
|
ValidationIssue(
|
|
124
140
|
severity="error",
|
|
125
141
|
issue_type="invalid_principal",
|
|
126
|
-
message="Service Control Policy must not contain Principal element. "
|
|
142
|
+
message="Service Control Policy must not contain `Principal` element. "
|
|
127
143
|
"Service Control Policies (SCPs) in AWS Organizations do not support "
|
|
128
|
-
"the Principal element. They apply to all principals in the organization or OU.",
|
|
144
|
+
"the `Principal` element. They apply to all principals in the organization or OU.",
|
|
129
145
|
statement_index=idx,
|
|
130
146
|
statement_sid=statement.sid,
|
|
131
147
|
line_number=statement.line_number,
|
|
132
|
-
suggestion="Remove the Principal element from this SCP statement.\n"
|
|
148
|
+
suggestion="Remove the `Principal` element from this SCP statement.\n"
|
|
133
149
|
"Example:\n"
|
|
134
150
|
"```json\n"
|
|
135
151
|
"{\n"
|
|
@@ -177,7 +193,7 @@ async def execute_policy(
|
|
|
177
193
|
ValidationIssue(
|
|
178
194
|
severity="error",
|
|
179
195
|
issue_type="invalid_rcp_not_principal",
|
|
180
|
-
message="Resource Control Policy must not contain NotPrincipal element. "
|
|
196
|
+
message="Resource Control Policy must not contain `NotPrincipal` element. "
|
|
181
197
|
"RCPs only support Principal with value '*'. Use Condition elements "
|
|
182
198
|
"to restrict specific principals.",
|
|
183
199
|
statement_index=idx,
|
|
@@ -192,13 +208,13 @@ async def execute_policy(
|
|
|
192
208
|
ValidationIssue(
|
|
193
209
|
severity="error",
|
|
194
210
|
issue_type="missing_rcp_principal",
|
|
195
|
-
message="Resource Control Policy statement must have Principal
|
|
196
|
-
"RCPs require the Principal element with value '*'. Use Condition "
|
|
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,13 +250,13 @@ 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
|
|
259
|
+
suggestion="Replace `*` with service-specific actions from supported "
|
|
244
260
|
f"services: {', '.join(sorted(rcp_supported_services))}",
|
|
245
261
|
)
|
|
246
262
|
)
|
|
@@ -275,12 +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 "
|
|
299
|
+
suggestion="Replace `NotAction` with `Action` element listing the specific "
|
|
284
300
|
"actions to deny.",
|
|
285
301
|
)
|
|
286
302
|
)
|
|
@@ -294,11 +310,11 @@ async def execute_policy(
|
|
|
294
310
|
ValidationIssue(
|
|
295
311
|
severity="error",
|
|
296
312
|
issue_type="missing_rcp_resource",
|
|
297
|
-
message="Resource Control Policy statement must have Resource or NotResource element.",
|
|
313
|
+
message="Resource Control Policy statement must have `Resource` or `NotResource` element.",
|
|
298
314
|
statement_index=idx,
|
|
299
315
|
statement_sid=statement.sid,
|
|
300
316
|
line_number=statement.line_number,
|
|
301
|
-
suggestion='Add Resource: "*" or specify specific resource ARNs.',
|
|
317
|
+
suggestion='Add `Resource: "*"` or specify specific resource ARNs.',
|
|
302
318
|
)
|
|
303
319
|
)
|
|
304
320
|
|
|
@@ -4,36 +4,34 @@ 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
|
|
@@ -41,6 +39,7 @@ from typing import Any
|
|
|
41
39
|
|
|
42
40
|
from iam_validator.core.aws_fetcher 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
|
|
|
@@ -83,22 +82,13 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
83
82
|
if statement.principal is None and statement.not_principal is None:
|
|
84
83
|
return issues
|
|
85
84
|
|
|
86
|
-
# Get configuration
|
|
85
|
+
# Get configuration (defaults match defaults.py)
|
|
87
86
|
blocked_principals = config.config.get("blocked_principals", ["*"])
|
|
88
87
|
allowed_principals = config.config.get("allowed_principals", [])
|
|
89
|
-
require_conditions_for = config.config.get("require_conditions_for", {})
|
|
90
88
|
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
|
-
)
|
|
89
|
+
# Default: "aws:*" allows ALL AWS service principals (*.amazonaws.com)
|
|
90
|
+
# This matches the default in defaults.py:251
|
|
91
|
+
allowed_service_principals = config.config.get("allowed_service_principals", ["aws:*"])
|
|
102
92
|
|
|
103
93
|
# Extract principals from statement
|
|
104
94
|
principals = self._extract_principals(statement)
|
|
@@ -117,8 +107,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
117
107
|
statement_index=statement_idx,
|
|
118
108
|
statement_sid=statement.sid,
|
|
119
109
|
line_number=statement.line_number,
|
|
120
|
-
suggestion=f"Remove the
|
|
121
|
-
"Consider using more specific
|
|
110
|
+
suggestion=f"Remove the `Principal` `{principal}` or add appropriate `Condition`s to restrict access. "
|
|
111
|
+
"Consider using more specific `Principal`s instead of `*` (wildcard).",
|
|
122
112
|
)
|
|
123
113
|
)
|
|
124
114
|
continue
|
|
@@ -131,42 +121,18 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
131
121
|
ValidationIssue(
|
|
132
122
|
severity=self.get_severity(config),
|
|
133
123
|
issue_type="unauthorized_principal",
|
|
134
|
-
message=f"Principal not in allowed list: `{principal}`. "
|
|
124
|
+
message=f"`Principal` not in allowed list: `{principal}`. "
|
|
135
125
|
f"Only principals in the `allowed_principals` whitelist are permitted.",
|
|
136
126
|
statement_index=statement_idx,
|
|
137
127
|
statement_sid=statement.sid,
|
|
138
128
|
line_number=statement.line_number,
|
|
139
|
-
suggestion=f"Add
|
|
140
|
-
"or use a
|
|
129
|
+
suggestion=f"Add `{principal}` to the `allowed_principals` list in your config, "
|
|
130
|
+
"or use a `Principal` that matches an allowed pattern.",
|
|
141
131
|
)
|
|
142
132
|
)
|
|
143
133
|
continue
|
|
144
134
|
|
|
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
|
|
135
|
+
# Check principal_condition_requirements (supports any_of/all_of/none_of)
|
|
170
136
|
if principal_condition_requirements:
|
|
171
137
|
condition_issues = self._validate_principal_condition_requirements(
|
|
172
138
|
statement,
|
|
@@ -197,7 +163,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
197
163
|
principals.append(statement.principal)
|
|
198
164
|
elif isinstance(statement.principal, dict):
|
|
199
165
|
# Dict with AWS, Service, Federated, etc.
|
|
200
|
-
for
|
|
166
|
+
for _, value in statement.principal.items():
|
|
201
167
|
if isinstance(value, str):
|
|
202
168
|
principals.append(value)
|
|
203
169
|
elif isinstance(value, list):
|
|
@@ -208,7 +174,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
208
174
|
if isinstance(statement.not_principal, str):
|
|
209
175
|
principals.append(statement.not_principal)
|
|
210
176
|
elif isinstance(statement.not_principal, dict):
|
|
211
|
-
for
|
|
177
|
+
for _, value in statement.not_principal.items():
|
|
212
178
|
if isinstance(value, str):
|
|
213
179
|
principals.append(value)
|
|
214
180
|
elif isinstance(value, list):
|
|
@@ -224,13 +190,17 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
224
190
|
Args:
|
|
225
191
|
principal: The principal to check
|
|
226
192
|
blocked_list: List of blocked principal patterns
|
|
227
|
-
service_whitelist: List of allowed service principals
|
|
193
|
+
service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
|
|
228
194
|
|
|
229
195
|
Returns:
|
|
230
196
|
True if the principal is blocked
|
|
231
197
|
"""
|
|
232
|
-
#
|
|
233
|
-
if
|
|
198
|
+
# Check if service_whitelist contains "aws:*" (allow all AWS service principals)
|
|
199
|
+
if "aws:*" in service_whitelist and is_aws_service_principal(principal):
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
# Service principals in explicit whitelist are never blocked
|
|
203
|
+
if is_aws_service_principal(principal) and principal in service_whitelist:
|
|
234
204
|
return False
|
|
235
205
|
|
|
236
206
|
# Check against blocked list (supports wildcards)
|
|
@@ -253,13 +223,17 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
253
223
|
Args:
|
|
254
224
|
principal: The principal to check
|
|
255
225
|
allowed_list: List of allowed principal patterns
|
|
256
|
-
service_whitelist: List of allowed service principals
|
|
226
|
+
service_whitelist: List of allowed service principals (supports "aws:*" for all AWS services)
|
|
257
227
|
|
|
258
228
|
Returns:
|
|
259
229
|
True if the principal is allowed
|
|
260
230
|
"""
|
|
261
|
-
#
|
|
262
|
-
if
|
|
231
|
+
# Check if service_whitelist contains "aws:*" (allow all AWS service principals)
|
|
232
|
+
if "aws:*" in service_whitelist and is_aws_service_principal(principal):
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
# Service principals in explicit whitelist are always allowed
|
|
236
|
+
if is_aws_service_principal(principal) and principal in service_whitelist:
|
|
263
237
|
return True
|
|
264
238
|
|
|
265
239
|
# Check against allowed list (supports wildcards)
|
|
@@ -274,52 +248,6 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
274
248
|
|
|
275
249
|
return False
|
|
276
250
|
|
|
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
251
|
def _validate_principal_condition_requirements(
|
|
324
252
|
self,
|
|
325
253
|
statement: Statement,
|
|
@@ -473,6 +401,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
473
401
|
# Create a combined error for any_of
|
|
474
402
|
condition_keys = [cond.get("condition_key", "unknown") for cond in any_of]
|
|
475
403
|
severity = requirement.get("severity", self.get_severity(config))
|
|
404
|
+
matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
|
|
476
405
|
issues.append(
|
|
477
406
|
ValidationIssue(
|
|
478
407
|
severity=severity,
|
|
@@ -480,8 +409,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
480
409
|
statement_index=statement_idx,
|
|
481
410
|
issue_type="missing_principal_condition_any_of",
|
|
482
411
|
message=(
|
|
483
|
-
f"
|
|
484
|
-
f"{', '.join(condition_keys)}"
|
|
412
|
+
f"`Principal`s `{matching_principals_str}` require at least ONE of these conditions: "
|
|
413
|
+
f"{', '.join(f'`{c}`' for c in condition_keys)}"
|
|
485
414
|
),
|
|
486
415
|
suggestion=self._build_any_of_suggestion(any_of),
|
|
487
416
|
line_number=statement.line_number,
|
|
@@ -637,12 +566,14 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
637
566
|
condition_key, description, example, expected_value, operator
|
|
638
567
|
)
|
|
639
568
|
|
|
569
|
+
matching_principals_formatted = ", ".join(f"`{p}`" for p in matching_principals)
|
|
570
|
+
|
|
640
571
|
return ValidationIssue(
|
|
641
572
|
severity=severity,
|
|
642
573
|
statement_sid=statement.sid,
|
|
643
574
|
statement_index=statement_idx,
|
|
644
575
|
issue_type="missing_principal_condition",
|
|
645
|
-
message=f"{message_prefix} Principal(s) {
|
|
576
|
+
message=f"{message_prefix} Principal(s) {matching_principals_formatted} require condition `{condition_key}`",
|
|
646
577
|
suggestion=suggestion_text,
|
|
647
578
|
example=example_code,
|
|
648
579
|
line_number=statement.line_number,
|
|
@@ -723,11 +654,11 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
723
654
|
description = cond.get("description", "")
|
|
724
655
|
expected_value = cond.get("expected_value")
|
|
725
656
|
|
|
726
|
-
option = f"\
|
|
657
|
+
option = f"\n- **Option {i}**: `{condition_key}`"
|
|
727
658
|
if description:
|
|
728
659
|
option += f" - {description}"
|
|
729
660
|
if expected_value is not None:
|
|
730
|
-
option += f" (value: {expected_value})"
|
|
661
|
+
option += f" (value: `{expected_value}`)"
|
|
731
662
|
|
|
732
663
|
suggestions.append(option)
|
|
733
664
|
|
|
@@ -759,11 +690,12 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
759
690
|
description = condition_requirement.get("description", "")
|
|
760
691
|
expected_value = condition_requirement.get("expected_value")
|
|
761
692
|
|
|
762
|
-
|
|
693
|
+
matching_principals_str = ", ".join(f"`{p}`" for p in matching_principals)
|
|
694
|
+
message = f"FORBIDDEN: `Principal`s `{matching_principals_str}` must NOT have `Condition` `{condition_key}`"
|
|
763
695
|
if expected_value is not None:
|
|
764
|
-
message += f" with value
|
|
696
|
+
message += f" with value `{expected_value}`"
|
|
765
697
|
|
|
766
|
-
suggestion = f"Remove the
|
|
698
|
+
suggestion = f"Remove the `{condition_key}` `Condition` from the statement"
|
|
767
699
|
if description:
|
|
768
700
|
suggestion += f". {description}"
|
|
769
701
|
|
|
@@ -75,7 +75,7 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
75
75
|
issue_type="invalid_resource",
|
|
76
76
|
message=f"Resource ARN exceeds maximum length ({len(resource)} > {MAX_ARN_LENGTH}): {resource[:100]}...",
|
|
77
77
|
resource=resource[:100] + "...",
|
|
78
|
-
suggestion="ARN is too long and may be invalid",
|
|
78
|
+
suggestion="`ARN` is too long and may be invalid",
|
|
79
79
|
line_number=line_number,
|
|
80
80
|
)
|
|
81
81
|
)
|
|
@@ -101,9 +101,9 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
101
101
|
statement_sid=statement_sid,
|
|
102
102
|
statement_index=statement_idx,
|
|
103
103
|
issue_type="invalid_resource",
|
|
104
|
-
message=f"Invalid ARN format even after normalizing template variables: {resource}",
|
|
104
|
+
message=f"Invalid `ARN` format even after normalizing template variables: `{resource}`",
|
|
105
105
|
resource=resource,
|
|
106
|
-
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource (template variables like
|
|
106
|
+
suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource` (template variables like `${aws_account_id}` are supported)",
|
|
107
107
|
line_number=line_number,
|
|
108
108
|
)
|
|
109
109
|
)
|
|
@@ -114,13 +114,13 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
114
114
|
statement_sid=statement_sid,
|
|
115
115
|
statement_index=statement_idx,
|
|
116
116
|
issue_type="invalid_resource",
|
|
117
|
-
message=f"Invalid ARN format: {resource}",
|
|
117
|
+
message=f"Invalid `ARN` format: `{resource}`",
|
|
118
118
|
resource=resource,
|
|
119
|
-
suggestion="ARN should follow format: arn:partition:service:region:account-id:resource",
|
|
119
|
+
suggestion="`ARN` should follow format: `arn:partition:service:region:account-id:resource`",
|
|
120
120
|
line_number=line_number,
|
|
121
121
|
)
|
|
122
122
|
)
|
|
123
|
-
except Exception:
|
|
123
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
124
124
|
# If regex matching fails (shouldn't happen with length check), treat as invalid
|
|
125
125
|
issues.append(
|
|
126
126
|
ValidationIssue(
|
|
@@ -128,9 +128,9 @@ class ResourceValidationCheck(PolicyCheck):
|
|
|
128
128
|
statement_sid=statement_sid,
|
|
129
129
|
statement_index=statement_idx,
|
|
130
130
|
issue_type="invalid_resource",
|
|
131
|
-
message=f"Could not validate ARN format: {resource}",
|
|
131
|
+
message=f"Could not validate `ARN` format: `{resource}`",
|
|
132
132
|
resource=resource,
|
|
133
|
-
suggestion="ARN validation failed - may contain unexpected characters",
|
|
133
|
+
suggestion="`ARN` validation failed - may contain unexpected characters",
|
|
134
134
|
line_number=line_number,
|
|
135
135
|
)
|
|
136
136
|
)
|
|
@@ -85,9 +85,9 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
85
85
|
# Generic ABAC fallback for uncategorized actions
|
|
86
86
|
return (
|
|
87
87
|
"Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
|
|
88
|
-
"• Match principal tags to resource tags (aws:PrincipalTag/<tag-name
|
|
89
|
-
"• Require MFA (aws:MultiFactorAuthPresent = true)\n"
|
|
90
|
-
"• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
|
|
88
|
+
"• Match principal tags to resource tags (`aws:PrincipalTag/<tag-name>` = `aws:ResourceTag/<tag-name>`)\n"
|
|
89
|
+
"• Require MFA (`aws:MultiFactorAuthPresent` = `true`)\n"
|
|
90
|
+
"• Restrict by IP (`aws:SourceIp`) or VPC (`aws:SourceVpc`)",
|
|
91
91
|
'"Condition": {\n'
|
|
92
92
|
' "StringEquals": {\n'
|
|
93
93
|
' "aws:PrincipalTag/owner": "${aws:ResourceTag/owner}"\n'
|
|
@@ -192,6 +192,10 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
192
192
|
del policy_file, fetcher # Not used in current implementation
|
|
193
193
|
issues = []
|
|
194
194
|
|
|
195
|
+
# Handle policies with no statements
|
|
196
|
+
if not policy.statement:
|
|
197
|
+
return []
|
|
198
|
+
|
|
195
199
|
# Collect all actions from all Allow statements across the entire policy
|
|
196
200
|
all_actions: set[str] = set()
|
|
197
201
|
statement_map: dict[
|
|
@@ -51,11 +51,11 @@ class ServiceWildcardCheck(PolicyCheck):
|
|
|
51
51
|
# Get message template and replace placeholders
|
|
52
52
|
message_template = config.config.get(
|
|
53
53
|
"message",
|
|
54
|
-
"Service-level wildcard
|
|
54
|
+
"Service-level wildcard `{action}` grants all permissions for `{service}` service",
|
|
55
55
|
)
|
|
56
56
|
suggestion_template = config.config.get(
|
|
57
57
|
"suggestion",
|
|
58
|
-
"Consider specifying explicit actions instead of
|
|
58
|
+
"Consider specifying explicit actions instead of `{action}`. If you need multiple actions, list them individually or use more specific wildcards like `{service}:Get*` or `{service}:List*`.",
|
|
59
59
|
)
|
|
60
60
|
example_template = config.config.get("example", "")
|
|
61
61
|
|