iam-policy-validator 1.8.0__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.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/METADATA +106 -1
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/RECORD +45 -37
- iam_validator/__init__.py +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +504 -190
- iam_validator/checks/action_resource_matching.py +8 -15
- iam_validator/checks/action_validation.py +6 -12
- iam_validator/checks/condition_key_validation.py +6 -12
- iam_validator/checks/condition_type_mismatch.py +9 -16
- 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 +10 -40
- iam_validator/checks/policy_type_validation.py +18 -19
- iam_validator/checks/principal_validation.py +11 -20
- iam_validator/checks/resource_validation.py +5 -12
- iam_validator/checks/sensitive_action.py +8 -15
- iam_validator/checks/service_wildcard.py +6 -12
- iam_validator/checks/set_operator_validation.py +11 -18
- iam_validator/checks/sid_uniqueness.py +8 -38
- iam_validator/checks/trust_policy_validation.py +8 -14
- iam_validator/checks/utils/wildcard_expansion.py +1 -1
- iam_validator/checks/wildcard_action.py +6 -12
- iam_validator/checks/wildcard_resource.py +6 -12
- iam_validator/commands/cache.py +4 -3
- iam_validator/commands/validate.py +12 -0
- iam_validator/core/__init__.py +1 -1
- iam_validator/core/aws_fetcher.py +24 -1030
- 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 +82 -14
- iam_validator/core/constants.py +17 -0
- iam_validator/core/policy_checks.py +7 -3
- 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.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.8.0.dist-info → iam_policy_validator-1.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Action-Specific Condition Enforcement Check
|
|
2
|
+
Action-Specific Condition Enforcement Check
|
|
3
3
|
|
|
4
|
-
This
|
|
4
|
+
This check ensures that specific actions have required conditions.
|
|
5
5
|
Supports ALL types of conditions: MFA, IP, VPC, time, tags, encryption, etc.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
The entire policy is scanned once, checking all statements for matching actions.
|
|
8
|
+
|
|
9
|
+
ACTION MATCHING MODES:
|
|
10
|
+
- Simple list: Checks each statement for any of the specified actions
|
|
11
|
+
Example: actions: ["iam:PassRole", "iam:CreateUser"]
|
|
12
|
+
|
|
13
|
+
- any_of: Finds statements that contain ANY of the specified actions
|
|
14
|
+
Example: actions: {any_of: ["iam:CreateUser", "iam:AttachUserPolicy"]}
|
|
15
|
+
|
|
16
|
+
- all_of: Finds statements that contain ALL specified actions (overly permissive detection)
|
|
17
|
+
Example: actions: {all_of: ["iam:CreateAccessKey", "iam:UpdateAccessKey"]}
|
|
18
|
+
|
|
19
|
+
- none_of: Flags statements that contain forbidden actions
|
|
20
|
+
Example: actions: {none_of: ["iam:DeleteUser", "s3:DeleteBucket"]}
|
|
9
21
|
|
|
10
22
|
Common use cases:
|
|
11
23
|
- iam:PassRole must have iam:PassedToService condition
|
|
@@ -13,17 +25,17 @@ Common use cases:
|
|
|
13
25
|
- Actions must have source IP restrictions
|
|
14
26
|
- Resources must have required tags
|
|
15
27
|
- Combine multiple conditions (MFA + IP + Tags)
|
|
16
|
-
-
|
|
28
|
+
- Detect overly permissive statements (all_of)
|
|
29
|
+
- Ensure privilege escalation combinations are protected
|
|
17
30
|
|
|
18
31
|
Configuration in iam-validator.yaml:
|
|
19
32
|
|
|
20
33
|
checks:
|
|
21
34
|
action_condition_enforcement:
|
|
22
35
|
enabled: true
|
|
23
|
-
severity:
|
|
36
|
+
severity: high
|
|
24
37
|
description: "Enforce specific conditions for specific actions"
|
|
25
38
|
|
|
26
|
-
# STATEMENT-LEVEL: Check individual statements (default)
|
|
27
39
|
action_condition_requirements:
|
|
28
40
|
# BASIC: Simple action with required condition
|
|
29
41
|
- actions:
|
|
@@ -87,56 +99,38 @@ Configuration in iam-validator.yaml:
|
|
|
87
99
|
expected_value: false
|
|
88
100
|
description: "Ensure insecure transport is never allowed"
|
|
89
101
|
|
|
90
|
-
#
|
|
91
|
-
- actions:
|
|
92
|
-
none_of:
|
|
93
|
-
- "iam:*"
|
|
94
|
-
- "s3:DeleteBucket"
|
|
95
|
-
description: "These dangerous actions should never be used"
|
|
96
|
-
|
|
97
|
-
# POLICY-LEVEL: Scan entire policy and enforce conditions across all matching statements
|
|
98
|
-
policy_level_requirements:
|
|
99
|
-
# Example: If ANY statement grants privilege escalation actions,
|
|
100
|
-
# then ALL such statements must have MFA
|
|
102
|
+
# any_of for actions: If ANY statement grants privilege escalation actions, require MFA
|
|
101
103
|
- actions:
|
|
102
104
|
any_of:
|
|
103
105
|
- "iam:CreateUser"
|
|
104
106
|
- "iam:AttachUserPolicy"
|
|
105
107
|
- "iam:PutUserPolicy"
|
|
106
|
-
scope: "policy"
|
|
107
108
|
required_conditions:
|
|
108
109
|
- condition_key: "aws:MultiFactorAuthPresent"
|
|
109
110
|
expected_value: true
|
|
110
|
-
description: "Privilege escalation actions require MFA
|
|
111
|
+
description: "Privilege escalation actions require MFA"
|
|
111
112
|
severity: "critical"
|
|
112
113
|
|
|
113
|
-
#
|
|
114
|
-
- actions:
|
|
115
|
-
any_of:
|
|
116
|
-
- "iam:*"
|
|
117
|
-
- "s3:*"
|
|
118
|
-
scope: "policy"
|
|
119
|
-
required_conditions:
|
|
120
|
-
all_of:
|
|
121
|
-
- condition_key: "aws:MultiFactorAuthPresent"
|
|
122
|
-
expected_value: true
|
|
123
|
-
- condition_key: "aws:SourceIp"
|
|
124
|
-
apply_to: "all_matching_statements"
|
|
125
|
-
|
|
126
|
-
# Example: Ensure no statement in the policy allows dangerous combinations
|
|
114
|
+
# all_of for actions: Flag statements that contain BOTH dangerous actions (overly permissive)
|
|
127
115
|
- actions:
|
|
128
116
|
all_of:
|
|
129
117
|
- "iam:CreateAccessKey"
|
|
130
118
|
- "iam:UpdateAccessKey"
|
|
131
|
-
scope: "policy"
|
|
132
119
|
severity: "critical"
|
|
133
|
-
description: "
|
|
120
|
+
description: "Statement grants both CreateAccessKey and UpdateAccessKey - too permissive"
|
|
121
|
+
|
|
122
|
+
# none_of for actions: Flag if forbidden actions are present
|
|
123
|
+
- actions:
|
|
124
|
+
none_of:
|
|
125
|
+
- "iam:DeleteUser"
|
|
126
|
+
- "s3:DeleteBucket"
|
|
127
|
+
description: "These dangerous actions should never be used"
|
|
134
128
|
"""
|
|
135
129
|
|
|
136
130
|
import re
|
|
137
|
-
from typing import TYPE_CHECKING, Any
|
|
131
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
138
132
|
|
|
139
|
-
from iam_validator.core.
|
|
133
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
140
134
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
141
135
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
142
136
|
|
|
@@ -147,206 +141,526 @@ if TYPE_CHECKING:
|
|
|
147
141
|
class ActionConditionEnforcementCheck(PolicyCheck):
|
|
148
142
|
"""Enforces specific condition requirements for specific actions with all_of/any_of support."""
|
|
149
143
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
144
|
+
check_id: ClassVar[str] = "action_condition_enforcement"
|
|
145
|
+
description: ClassVar[str] = (
|
|
146
|
+
"Enforces conditions (MFA, IP, tags, etc.) for specific actions (supports all_of/any_of)"
|
|
147
|
+
)
|
|
148
|
+
default_severity: ClassVar[str] = "error"
|
|
149
|
+
|
|
150
|
+
async def execute_policy(
|
|
151
|
+
self,
|
|
152
|
+
policy: "IAMPolicy",
|
|
153
|
+
policy_file: str,
|
|
154
|
+
fetcher: AWSServiceFetcher,
|
|
155
|
+
config: CheckConfig,
|
|
156
|
+
**kwargs,
|
|
157
|
+
) -> list[ValidationIssue]:
|
|
158
|
+
"""
|
|
159
|
+
Execute policy-wide condition enforcement check.
|
|
160
|
+
|
|
161
|
+
This method scans the entire policy once and enforces conditions based on action matching:
|
|
162
|
+
- Simple list: Checks each statement for matching actions
|
|
163
|
+
- all_of: Finds statements that contain ALL specified actions (overly permissive detection)
|
|
164
|
+
- any_of: Finds statements that contain ANY of the specified actions
|
|
165
|
+
- none_of: Flags statements that contain forbidden actions
|
|
166
|
+
|
|
167
|
+
Example use cases:
|
|
168
|
+
- any_of: "If ANY statement grants iam:CreateUser, iam:AttachUserPolicy,
|
|
169
|
+
or iam:PutUserPolicy, then ALL such statements must have MFA condition."
|
|
170
|
+
- all_of: "Flag statements that grant BOTH iam:CreateAccessKey AND
|
|
171
|
+
iam:UpdateAccessKey (overly permissive)"
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
policy: The complete IAM policy to check
|
|
175
|
+
policy_file: Path to the policy file (for context/reporting)
|
|
176
|
+
fetcher: AWS service fetcher for validation against AWS APIs
|
|
177
|
+
config: CheckConfig: Configuration for this check instance
|
|
178
|
+
**kwargs: Additional context (policy_type, etc.)
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
List of ValidationIssue objects found by this check
|
|
182
|
+
"""
|
|
183
|
+
del policy_file, kwargs # Not used in current implementation
|
|
184
|
+
issues = []
|
|
185
|
+
|
|
186
|
+
# Get action condition requirements from config
|
|
187
|
+
# Support both old (policy_level_requirements) and new (action_condition_requirements) keys
|
|
188
|
+
requirements = config.config.get(
|
|
189
|
+
"action_condition_requirements",
|
|
190
|
+
config.config.get("policy_level_requirements", []),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
if not requirements:
|
|
194
|
+
return issues
|
|
195
|
+
|
|
196
|
+
# Process each requirement
|
|
197
|
+
for requirement in requirements:
|
|
198
|
+
# Check if actions use all_of/any_of/none_of (policy-wide) or simple list (per-statement)
|
|
199
|
+
actions_config = requirement.get("actions", [])
|
|
200
|
+
uses_logical_operators = isinstance(actions_config, dict) and any(
|
|
201
|
+
key in actions_config for key in ("all_of", "any_of", "none_of")
|
|
202
|
+
)
|
|
153
203
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
204
|
+
if uses_logical_operators:
|
|
205
|
+
# Policy-wide detection (all_of/any_of/none_of)
|
|
206
|
+
policy_issues = await self._check_policy_wide(policy, requirement, fetcher, config)
|
|
207
|
+
issues.extend(policy_issues)
|
|
208
|
+
else:
|
|
209
|
+
# Per-statement check (simple list)
|
|
210
|
+
statement_issues = await self._check_per_statement(
|
|
211
|
+
policy, requirement, fetcher, config
|
|
212
|
+
)
|
|
213
|
+
issues.extend(statement_issues)
|
|
157
214
|
|
|
158
|
-
|
|
159
|
-
def default_severity(self) -> str:
|
|
160
|
-
return "error"
|
|
215
|
+
return issues
|
|
161
216
|
|
|
162
|
-
async def
|
|
217
|
+
async def _check_policy_wide(
|
|
163
218
|
self,
|
|
164
|
-
|
|
165
|
-
|
|
219
|
+
policy: "IAMPolicy",
|
|
220
|
+
requirement: dict[str, Any],
|
|
221
|
+
fetcher: AWSServiceFetcher,
|
|
222
|
+
config: CheckConfig,
|
|
223
|
+
) -> list[ValidationIssue]:
|
|
224
|
+
"""
|
|
225
|
+
Check actions across the entire policy using all_of/any_of/none_of logic.
|
|
226
|
+
|
|
227
|
+
This enables policy-wide detection patterns:
|
|
228
|
+
- all_of: ALL required actions must exist somewhere in the policy
|
|
229
|
+
- any_of: At least ONE required action must exist somewhere in the policy
|
|
230
|
+
- none_of: NONE of the forbidden actions should exist in the policy
|
|
231
|
+
"""
|
|
232
|
+
issues = []
|
|
233
|
+
actions_config = requirement.get("actions", {})
|
|
234
|
+
all_of = actions_config.get("all_of", [])
|
|
235
|
+
any_of = actions_config.get("any_of", [])
|
|
236
|
+
none_of = actions_config.get("none_of", [])
|
|
237
|
+
|
|
238
|
+
# Collect all actions across the entire policy
|
|
239
|
+
policy_wide_actions: set[str] = set()
|
|
240
|
+
statements_by_action: dict[str, list[tuple[int, Statement]]] = {}
|
|
241
|
+
|
|
242
|
+
for idx, statement in enumerate(policy.statement or []):
|
|
243
|
+
if statement.effect != "Allow":
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
statement_actions = statement.get_actions()
|
|
247
|
+
policy_wide_actions.update(statement_actions)
|
|
248
|
+
|
|
249
|
+
# Track which statements grant which actions
|
|
250
|
+
for action in statement_actions:
|
|
251
|
+
if action not in statements_by_action:
|
|
252
|
+
statements_by_action[action] = []
|
|
253
|
+
statements_by_action[action].append((idx, statement))
|
|
254
|
+
|
|
255
|
+
# Check all_of: ALL required actions must exist in policy
|
|
256
|
+
if all_of:
|
|
257
|
+
all_of_result = await self._check_all_of_policy_wide(
|
|
258
|
+
all_of,
|
|
259
|
+
policy_wide_actions,
|
|
260
|
+
statements_by_action,
|
|
261
|
+
requirement,
|
|
262
|
+
fetcher,
|
|
263
|
+
config,
|
|
264
|
+
)
|
|
265
|
+
issues.extend(all_of_result)
|
|
266
|
+
|
|
267
|
+
# Check any_of: At least ONE required action must exist in policy
|
|
268
|
+
if any_of:
|
|
269
|
+
any_of_result = await self._check_any_of_policy_wide(
|
|
270
|
+
any_of,
|
|
271
|
+
policy_wide_actions,
|
|
272
|
+
statements_by_action,
|
|
273
|
+
requirement,
|
|
274
|
+
fetcher,
|
|
275
|
+
config,
|
|
276
|
+
)
|
|
277
|
+
issues.extend(any_of_result)
|
|
278
|
+
|
|
279
|
+
# Check none_of: NONE of the forbidden actions should exist in policy
|
|
280
|
+
if none_of:
|
|
281
|
+
none_of_result = await self._check_none_of_policy_wide(
|
|
282
|
+
none_of,
|
|
283
|
+
policy_wide_actions,
|
|
284
|
+
statements_by_action,
|
|
285
|
+
requirement,
|
|
286
|
+
config,
|
|
287
|
+
fetcher,
|
|
288
|
+
)
|
|
289
|
+
issues.extend(none_of_result)
|
|
290
|
+
|
|
291
|
+
return issues
|
|
292
|
+
|
|
293
|
+
async def _check_all_of_policy_wide(
|
|
294
|
+
self,
|
|
295
|
+
all_of_actions: list[str],
|
|
296
|
+
policy_wide_actions: set[str],
|
|
297
|
+
statements_by_action: dict[str, list[tuple[int, Statement]]],
|
|
298
|
+
requirement: dict[str, Any],
|
|
166
299
|
fetcher: AWSServiceFetcher,
|
|
167
300
|
config: CheckConfig,
|
|
168
301
|
) -> list[ValidationIssue]:
|
|
169
|
-
"""
|
|
302
|
+
"""
|
|
303
|
+
Check if ALL required actions exist anywhere in the policy.
|
|
304
|
+
|
|
305
|
+
For all_of, we report ONLY statements that contain ALL the required actions,
|
|
306
|
+
not statements that contain just some of them. This is useful for detecting
|
|
307
|
+
overly permissive individual statements.
|
|
308
|
+
"""
|
|
170
309
|
issues = []
|
|
171
310
|
|
|
172
|
-
#
|
|
173
|
-
|
|
311
|
+
# First, check if ALL required actions exist somewhere in the policy
|
|
312
|
+
found_actions_mapping: dict[str, str] = {} # req_action -> matched_policy_action
|
|
313
|
+
missing_actions: list[str] = []
|
|
314
|
+
|
|
315
|
+
for req_action in all_of_actions:
|
|
316
|
+
action_found = False
|
|
317
|
+
for policy_action in policy_wide_actions:
|
|
318
|
+
if await self._action_matches(
|
|
319
|
+
policy_action, req_action, requirement.get("action_patterns", []), fetcher
|
|
320
|
+
):
|
|
321
|
+
action_found = True
|
|
322
|
+
found_actions_mapping[req_action] = policy_action
|
|
323
|
+
break
|
|
324
|
+
|
|
325
|
+
if not action_found:
|
|
326
|
+
missing_actions.append(req_action)
|
|
327
|
+
|
|
328
|
+
# If not all actions exist in the policy, no issue
|
|
329
|
+
if missing_actions:
|
|
174
330
|
return issues
|
|
175
331
|
|
|
176
|
-
#
|
|
177
|
-
|
|
178
|
-
|
|
332
|
+
# ALL required actions exist in the policy
|
|
333
|
+
# Now find statements that have ALL of them (not just some)
|
|
334
|
+
statements_with_all_actions: list[tuple[int, Statement, list[str]]] = []
|
|
335
|
+
|
|
336
|
+
# Check each statement to see if it contains ALL required actions
|
|
337
|
+
for statement in statements_by_action.get(list(found_actions_mapping.values())[0], []):
|
|
338
|
+
stmt_idx, stmt = statement
|
|
339
|
+
stmt_actions = stmt.get_actions()
|
|
340
|
+
|
|
341
|
+
# Check if this statement has ALL required actions
|
|
342
|
+
has_all_actions = True
|
|
343
|
+
matched_actions = []
|
|
344
|
+
|
|
345
|
+
for req_action in all_of_actions:
|
|
346
|
+
req_action_found = False
|
|
347
|
+
for stmt_action in stmt_actions:
|
|
348
|
+
if await self._action_matches(
|
|
349
|
+
stmt_action, req_action, requirement.get("action_patterns", []), fetcher
|
|
350
|
+
):
|
|
351
|
+
req_action_found = True
|
|
352
|
+
if stmt_action not in matched_actions:
|
|
353
|
+
matched_actions.append(stmt_action)
|
|
354
|
+
break
|
|
355
|
+
|
|
356
|
+
if not req_action_found:
|
|
357
|
+
has_all_actions = False
|
|
358
|
+
break
|
|
359
|
+
|
|
360
|
+
if has_all_actions:
|
|
361
|
+
statements_with_all_actions.append((stmt_idx, stmt, matched_actions))
|
|
362
|
+
|
|
363
|
+
# Also check other statements not in the first action's list
|
|
364
|
+
checked_indices = {s[0] for s in statements_with_all_actions}
|
|
365
|
+
for policy_action, stmt_list in statements_by_action.items():
|
|
366
|
+
for stmt_idx, stmt in stmt_list:
|
|
367
|
+
if stmt_idx in checked_indices:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
stmt_actions = stmt.get_actions()
|
|
371
|
+
|
|
372
|
+
# Check if this statement has ALL required actions
|
|
373
|
+
has_all_actions = True
|
|
374
|
+
matched_actions = []
|
|
375
|
+
|
|
376
|
+
for req_action in all_of_actions:
|
|
377
|
+
req_action_found = False
|
|
378
|
+
for stmt_action in stmt_actions:
|
|
379
|
+
if await self._action_matches(
|
|
380
|
+
stmt_action, req_action, requirement.get("action_patterns", []), fetcher
|
|
381
|
+
):
|
|
382
|
+
req_action_found = True
|
|
383
|
+
if stmt_action not in matched_actions:
|
|
384
|
+
matched_actions.append(stmt_action)
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
if not req_action_found:
|
|
388
|
+
has_all_actions = False
|
|
389
|
+
break
|
|
390
|
+
|
|
391
|
+
if has_all_actions:
|
|
392
|
+
statements_with_all_actions.append((stmt_idx, stmt, matched_actions))
|
|
393
|
+
checked_indices.add(stmt_idx)
|
|
394
|
+
|
|
395
|
+
# If no statements have ALL actions, no issue to report
|
|
396
|
+
if not statements_with_all_actions:
|
|
179
397
|
return issues
|
|
180
398
|
|
|
181
|
-
|
|
399
|
+
# Report statements that have ALL the dangerous actions
|
|
400
|
+
return self._generate_policy_wide_issues(
|
|
401
|
+
statements_with_all_actions,
|
|
402
|
+
list(found_actions_mapping.values()),
|
|
403
|
+
requirement,
|
|
404
|
+
config,
|
|
405
|
+
"all_of",
|
|
406
|
+
)
|
|
182
407
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
408
|
+
async def _check_any_of_policy_wide(
|
|
409
|
+
self,
|
|
410
|
+
any_of_actions: list[str],
|
|
411
|
+
policy_wide_actions: set[str],
|
|
412
|
+
statements_by_action: dict[str, list[tuple[int, Statement]]],
|
|
413
|
+
requirement: dict[str, Any],
|
|
414
|
+
fetcher: AWSServiceFetcher,
|
|
415
|
+
config: CheckConfig,
|
|
416
|
+
) -> list[ValidationIssue]:
|
|
417
|
+
"""Check if at least ONE required action exists anywhere in the policy."""
|
|
418
|
+
issues = []
|
|
419
|
+
found_actions: list[str] = []
|
|
420
|
+
statements_with_required_actions: list[tuple[int, Statement, list[str]]] = []
|
|
421
|
+
|
|
422
|
+
for req_action in any_of_actions:
|
|
423
|
+
for policy_action in policy_wide_actions:
|
|
424
|
+
if await self._action_matches(
|
|
425
|
+
policy_action, req_action, requirement.get("action_patterns", []), fetcher
|
|
426
|
+
):
|
|
427
|
+
found_actions.append(policy_action)
|
|
428
|
+
|
|
429
|
+
# Track statements that have this action
|
|
430
|
+
if policy_action in statements_by_action:
|
|
431
|
+
for stmt_idx, stmt in statements_by_action[policy_action]:
|
|
432
|
+
existing = next(
|
|
433
|
+
(s for s in statements_with_required_actions if s[0] == stmt_idx),
|
|
434
|
+
None,
|
|
435
|
+
)
|
|
436
|
+
if existing:
|
|
437
|
+
if policy_action not in existing[2]:
|
|
438
|
+
existing[2].append(policy_action)
|
|
439
|
+
else:
|
|
440
|
+
statements_with_required_actions.append(
|
|
441
|
+
(stmt_idx, stmt, [policy_action])
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
# If no actions found, no issue
|
|
445
|
+
if not found_actions:
|
|
446
|
+
return issues
|
|
189
447
|
|
|
190
|
-
|
|
191
|
-
|
|
448
|
+
# At least one action found - validate conditions
|
|
449
|
+
return self._generate_policy_wide_issues(
|
|
450
|
+
statements_with_required_actions,
|
|
451
|
+
found_actions,
|
|
452
|
+
requirement,
|
|
453
|
+
config,
|
|
454
|
+
"any_of",
|
|
455
|
+
)
|
|
192
456
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
457
|
+
async def _check_none_of_policy_wide(
|
|
458
|
+
self,
|
|
459
|
+
none_of_actions: list[str],
|
|
460
|
+
policy_wide_actions: set[str],
|
|
461
|
+
statements_by_action: dict[str, list[tuple[int, Statement]]],
|
|
462
|
+
requirement: dict[str, Any],
|
|
463
|
+
config: CheckConfig,
|
|
464
|
+
fetcher: AWSServiceFetcher,
|
|
465
|
+
) -> list[ValidationIssue]:
|
|
466
|
+
"""Check if any forbidden actions exist in the policy."""
|
|
467
|
+
issues = []
|
|
468
|
+
forbidden_found: list[str] = []
|
|
469
|
+
statements_with_forbidden: list[tuple[int, Statement, list[str]]] = []
|
|
470
|
+
|
|
471
|
+
for forbidden_action in none_of_actions:
|
|
472
|
+
for policy_action in policy_wide_actions:
|
|
473
|
+
if await self._action_matches(
|
|
474
|
+
policy_action, forbidden_action, requirement.get("action_patterns", []), fetcher
|
|
475
|
+
):
|
|
476
|
+
forbidden_found.append(policy_action)
|
|
477
|
+
|
|
478
|
+
# Track statements with forbidden actions
|
|
479
|
+
if policy_action in statements_by_action:
|
|
480
|
+
for stmt_idx, stmt in statements_by_action[policy_action]:
|
|
481
|
+
existing = next(
|
|
482
|
+
(s for s in statements_with_forbidden if s[0] == stmt_idx), None
|
|
483
|
+
)
|
|
484
|
+
if existing:
|
|
485
|
+
if policy_action not in existing[2]:
|
|
486
|
+
existing[2].append(policy_action)
|
|
487
|
+
else:
|
|
488
|
+
statements_with_forbidden.append((stmt_idx, stmt, [policy_action]))
|
|
489
|
+
|
|
490
|
+
# If forbidden actions found, create issues
|
|
491
|
+
if not forbidden_found:
|
|
492
|
+
return issues
|
|
493
|
+
|
|
494
|
+
description = requirement.get("description", "These actions should not be used")
|
|
495
|
+
severity = requirement.get("severity", self.get_severity(config))
|
|
496
|
+
|
|
497
|
+
for stmt_idx, stmt, actions in statements_with_forbidden:
|
|
498
|
+
actions_formatted = ", ".join(f"`{a}`" for a in actions)
|
|
499
|
+
statement_refs = [
|
|
500
|
+
f"Statement #{idx + 1}{' (SID: ' + s.sid + ')' if s.sid else ''}"
|
|
501
|
+
for idx, s, _ in statements_with_forbidden
|
|
502
|
+
]
|
|
503
|
+
|
|
504
|
+
issues.append(
|
|
505
|
+
ValidationIssue(
|
|
506
|
+
severity=severity,
|
|
507
|
+
statement_sid=stmt.sid,
|
|
508
|
+
statement_index=stmt_idx,
|
|
509
|
+
issue_type="forbidden_action",
|
|
510
|
+
message=f"Forbidden actions {actions_formatted} found. {description}",
|
|
511
|
+
action=", ".join(actions),
|
|
512
|
+
suggestion=f"Remove these forbidden actions. Found in: {', '.join(statement_refs)}. {description}",
|
|
513
|
+
line_number=stmt.line_number,
|
|
212
514
|
)
|
|
213
|
-
|
|
515
|
+
)
|
|
214
516
|
|
|
215
|
-
|
|
216
|
-
required_conditions_config = requirement.get("required_conditions", [])
|
|
217
|
-
if not required_conditions_config:
|
|
218
|
-
continue
|
|
517
|
+
return issues
|
|
219
518
|
|
|
220
|
-
|
|
519
|
+
def _generate_policy_wide_issues(
|
|
520
|
+
self,
|
|
521
|
+
statements_with_actions: list[tuple[int, Statement, list[str]]],
|
|
522
|
+
found_actions: list[str],
|
|
523
|
+
requirement: dict[str, Any],
|
|
524
|
+
config: CheckConfig,
|
|
525
|
+
operator_type: str,
|
|
526
|
+
) -> list[ValidationIssue]:
|
|
527
|
+
"""Generate validation issues for policy-wide checks."""
|
|
528
|
+
issues = []
|
|
529
|
+
required_conditions_config = requirement.get("required_conditions", [])
|
|
530
|
+
description = requirement.get("description", "")
|
|
531
|
+
severity = requirement.get("severity", self.get_severity(config))
|
|
532
|
+
|
|
533
|
+
if not required_conditions_config:
|
|
534
|
+
# No conditions specified, just report that actions were found
|
|
535
|
+
all_actions_formatted = ", ".join(f"`{a}`" for a in sorted(set(found_actions)))
|
|
536
|
+
statement_refs = [
|
|
537
|
+
f"Statement #{idx + 1}{' (SID: ' + stmt.sid + ')' if stmt.sid else ''}"
|
|
538
|
+
for idx, stmt, _ in statements_with_actions
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
first_idx, first_stmt, _ = statements_with_actions[0]
|
|
542
|
+
issues.append(
|
|
543
|
+
ValidationIssue(
|
|
544
|
+
severity=severity,
|
|
545
|
+
statement_sid=first_stmt.sid,
|
|
546
|
+
statement_index=first_idx,
|
|
547
|
+
issue_type="action_detected",
|
|
548
|
+
message=f"Actions {all_actions_formatted} found across {len(statements_with_actions)} statement(s) ({operator_type}). {description}",
|
|
549
|
+
action=", ".join(sorted(set(found_actions))),
|
|
550
|
+
suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
|
|
551
|
+
line_number=first_stmt.line_number,
|
|
552
|
+
)
|
|
553
|
+
)
|
|
554
|
+
return issues
|
|
555
|
+
|
|
556
|
+
# Validate conditions for each statement
|
|
557
|
+
for idx, statement, matching_actions in statements_with_actions:
|
|
221
558
|
condition_issues = self._validate_conditions(
|
|
222
559
|
statement,
|
|
223
|
-
|
|
560
|
+
idx,
|
|
224
561
|
required_conditions_config,
|
|
225
562
|
matching_actions,
|
|
226
563
|
config,
|
|
227
|
-
requirement,
|
|
564
|
+
requirement,
|
|
228
565
|
)
|
|
229
566
|
|
|
567
|
+
# Add context
|
|
568
|
+
for issue in condition_issues:
|
|
569
|
+
issue.suggestion = (
|
|
570
|
+
f"{issue.suggestion}\n\n"
|
|
571
|
+
f"Note: Found {len(statements_with_actions)} statement(s) with these actions in the policy ({operator_type})."
|
|
572
|
+
)
|
|
573
|
+
|
|
230
574
|
issues.extend(condition_issues)
|
|
231
575
|
|
|
232
576
|
return issues
|
|
233
577
|
|
|
234
|
-
async def
|
|
578
|
+
async def _check_per_statement(
|
|
235
579
|
self,
|
|
236
580
|
policy: "IAMPolicy",
|
|
237
|
-
|
|
581
|
+
requirement: dict[str, Any],
|
|
238
582
|
fetcher: AWSServiceFetcher,
|
|
239
583
|
config: CheckConfig,
|
|
240
|
-
**kwargs,
|
|
241
584
|
) -> list[ValidationIssue]:
|
|
242
585
|
"""
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
This method scans the entire policy and enforces that ALL statements granting
|
|
246
|
-
certain actions must have specific conditions. This is useful for ensuring
|
|
247
|
-
consistent security controls across the entire policy.
|
|
248
|
-
|
|
249
|
-
Example use case:
|
|
250
|
-
- "If ANY statement in the policy grants iam:CreateUser, iam:AttachUserPolicy,
|
|
251
|
-
or iam:PutUserPolicy, then ALL such statements must have MFA condition."
|
|
252
|
-
|
|
253
|
-
Args:
|
|
254
|
-
policy: The complete IAM policy to check
|
|
255
|
-
policy_file: Path to the policy file (for context/reporting)
|
|
256
|
-
fetcher: AWS service fetcher for validation against AWS APIs
|
|
257
|
-
config: Configuration for this check instance
|
|
258
|
-
**kwargs: Additional context (policy_type, etc.)
|
|
586
|
+
Check each statement individually for matching actions (simple list format).
|
|
259
587
|
|
|
260
|
-
|
|
261
|
-
List of ValidationIssue objects found by this check
|
|
588
|
+
Used when actions are specified as a simple list (not using all_of/any_of/none_of).
|
|
262
589
|
"""
|
|
263
|
-
del policy_file, kwargs # Not used in current implementation
|
|
264
590
|
issues = []
|
|
591
|
+
matching_statements: list[tuple[int, Statement, list[str]]] = []
|
|
265
592
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
# Process each policy-level requirement
|
|
272
|
-
for requirement in policy_level_requirements:
|
|
273
|
-
# Collect all statements that match the action criteria
|
|
274
|
-
matching_statements: list[tuple[int, Statement, list[str]]] = []
|
|
275
|
-
|
|
276
|
-
for idx, statement in enumerate(policy.statement or []):
|
|
277
|
-
# Only check Allow statements
|
|
278
|
-
if statement.effect != "Allow":
|
|
279
|
-
continue
|
|
593
|
+
for idx, statement in enumerate(policy.statement or []):
|
|
594
|
+
# Only check Allow statements
|
|
595
|
+
if statement.effect != "Allow":
|
|
596
|
+
continue
|
|
280
597
|
|
|
281
|
-
|
|
598
|
+
statement_actions = statement.get_actions()
|
|
282
599
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
600
|
+
# Check if this statement matches the action requirement
|
|
601
|
+
actions_match, matching_actions = await self._check_action_match(
|
|
602
|
+
statement_actions, requirement, fetcher
|
|
603
|
+
)
|
|
287
604
|
|
|
288
|
-
|
|
289
|
-
|
|
605
|
+
if actions_match and matching_actions:
|
|
606
|
+
matching_statements.append((idx, statement, matching_actions))
|
|
290
607
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
608
|
+
# If no statements match, skip this requirement
|
|
609
|
+
if not matching_statements:
|
|
610
|
+
return issues
|
|
294
611
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
)
|
|
612
|
+
# Now validate that ALL matching statements have the required conditions
|
|
613
|
+
required_conditions_config = requirement.get("required_conditions", [])
|
|
614
|
+
if not required_conditions_config:
|
|
615
|
+
# No conditions specified, just report that actions were found
|
|
616
|
+
description = requirement.get("description", "")
|
|
617
|
+
severity = requirement.get("severity", self.get_severity(config))
|
|
618
|
+
|
|
619
|
+
# Create a summary issue for all matching statements
|
|
620
|
+
all_actions = set()
|
|
621
|
+
statement_refs = []
|
|
622
|
+
for idx, stmt, actions in matching_statements:
|
|
623
|
+
all_actions.update(actions)
|
|
624
|
+
sid_info = f" (SID: {stmt.sid})" if stmt.sid else ""
|
|
625
|
+
statement_refs.append(f"Statement #{idx + 1}{sid_info}")
|
|
626
|
+
|
|
627
|
+
# Use the first matching statement's index for the issue
|
|
628
|
+
first_idx, first_stmt, _ = matching_statements[0]
|
|
629
|
+
all_actions_formatted = ", ".join(f"`{a}`" for a in sorted(all_actions))
|
|
630
|
+
|
|
631
|
+
issues.append(
|
|
632
|
+
ValidationIssue(
|
|
633
|
+
severity=severity,
|
|
634
|
+
statement_sid=first_stmt.sid,
|
|
635
|
+
statement_index=first_idx,
|
|
636
|
+
issue_type="action_detected",
|
|
637
|
+
message=f"Actions {all_actions_formatted} found in {len(matching_statements)} statement(s). {description}",
|
|
638
|
+
action=", ".join(sorted(all_actions)),
|
|
639
|
+
suggestion=f"Review these statements: {', '.join(statement_refs)}. {description}",
|
|
640
|
+
line_number=first_stmt.line_number,
|
|
325
641
|
)
|
|
326
|
-
|
|
642
|
+
)
|
|
643
|
+
return issues
|
|
327
644
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
645
|
+
# Validate conditions for each matching statement
|
|
646
|
+
for idx, statement, matching_actions in matching_statements:
|
|
647
|
+
condition_issues = self._validate_conditions(
|
|
648
|
+
statement,
|
|
649
|
+
idx,
|
|
650
|
+
required_conditions_config,
|
|
651
|
+
matching_actions,
|
|
652
|
+
config,
|
|
653
|
+
requirement,
|
|
654
|
+
)
|
|
338
655
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
f"Note: This is enforced at the policy level. "
|
|
346
|
-
f"Found {len(matching_statements)} statement(s) with these actions in the policy."
|
|
347
|
-
)
|
|
656
|
+
# Add context to each issue
|
|
657
|
+
for issue in condition_issues:
|
|
658
|
+
issue.suggestion = (
|
|
659
|
+
f"{issue.suggestion}\n\n"
|
|
660
|
+
f"Note: Found {len(matching_statements)} statement(s) with these actions in the policy."
|
|
661
|
+
)
|
|
348
662
|
|
|
349
|
-
|
|
663
|
+
issues.extend(condition_issues)
|
|
350
664
|
|
|
351
665
|
return issues
|
|
352
666
|
|
|
@@ -815,7 +1129,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
815
1129
|
Returns:
|
|
816
1130
|
Tuple of (suggestion_text, example_code)
|
|
817
1131
|
"""
|
|
818
|
-
suggestion = description if description else f"Add condition: {condition_key}"
|
|
1132
|
+
suggestion = description if description else f"Add condition: `{condition_key}`"
|
|
819
1133
|
|
|
820
1134
|
# Build example based on condition key type
|
|
821
1135
|
if example:
|