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
|
@@ -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,204 +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"
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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.)
|
|
157
179
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
Returns:
|
|
181
|
+
List of ValidationIssue objects found by this check
|
|
182
|
+
"""
|
|
183
|
+
del policy_file, kwargs # Not used in current implementation
|
|
184
|
+
issues = []
|
|
161
185
|
|
|
162
|
-
|
|
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
|
+
)
|
|
203
|
+
|
|
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)
|
|
214
|
+
|
|
215
|
+
return issues
|
|
216
|
+
|
|
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
|
-
)
|
|
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,
|
|
211
514
|
)
|
|
212
|
-
|
|
515
|
+
)
|
|
213
516
|
|
|
214
|
-
|
|
215
|
-
required_conditions_config = requirement.get("required_conditions", [])
|
|
216
|
-
if not required_conditions_config:
|
|
217
|
-
continue
|
|
517
|
+
return issues
|
|
218
518
|
|
|
219
|
-
|
|
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:
|
|
220
558
|
condition_issues = self._validate_conditions(
|
|
221
559
|
statement,
|
|
222
|
-
|
|
560
|
+
idx,
|
|
223
561
|
required_conditions_config,
|
|
224
562
|
matching_actions,
|
|
225
563
|
config,
|
|
226
|
-
requirement,
|
|
564
|
+
requirement,
|
|
227
565
|
)
|
|
228
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
|
+
|
|
229
574
|
issues.extend(condition_issues)
|
|
230
575
|
|
|
231
576
|
return issues
|
|
232
577
|
|
|
233
|
-
async def
|
|
578
|
+
async def _check_per_statement(
|
|
234
579
|
self,
|
|
235
580
|
policy: "IAMPolicy",
|
|
236
|
-
|
|
581
|
+
requirement: dict[str, Any],
|
|
237
582
|
fetcher: AWSServiceFetcher,
|
|
238
583
|
config: CheckConfig,
|
|
239
|
-
**kwargs,
|
|
240
584
|
) -> list[ValidationIssue]:
|
|
241
585
|
"""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
This method scans the entire policy and enforces that ALL statements granting
|
|
245
|
-
certain actions must have specific conditions. This is useful for ensuring
|
|
246
|
-
consistent security controls across the entire policy.
|
|
247
|
-
|
|
248
|
-
Example use case:
|
|
249
|
-
- "If ANY statement in the policy grants iam:CreateUser, iam:AttachUserPolicy,
|
|
250
|
-
or iam:PutUserPolicy, then ALL such statements must have MFA condition."
|
|
251
|
-
|
|
252
|
-
Args:
|
|
253
|
-
policy: The complete IAM policy to check
|
|
254
|
-
policy_file: Path to the policy file (for context/reporting)
|
|
255
|
-
fetcher: AWS service fetcher for validation against AWS APIs
|
|
256
|
-
config: Configuration for this check instance
|
|
257
|
-
**kwargs: Additional context (policy_type, etc.)
|
|
586
|
+
Check each statement individually for matching actions (simple list format).
|
|
258
587
|
|
|
259
|
-
|
|
260
|
-
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).
|
|
261
589
|
"""
|
|
262
|
-
del policy_file, kwargs # Not used in current implementation
|
|
263
590
|
issues = []
|
|
591
|
+
matching_statements: list[tuple[int, Statement, list[str]]] = []
|
|
264
592
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
# Process each policy-level requirement
|
|
271
|
-
for requirement in policy_level_requirements:
|
|
272
|
-
# Collect all statements that match the action criteria
|
|
273
|
-
matching_statements: list[tuple[int, Statement, list[str]]] = []
|
|
274
|
-
|
|
275
|
-
for idx, statement in enumerate(policy.statement):
|
|
276
|
-
# Only check Allow statements
|
|
277
|
-
if statement.effect != "Allow":
|
|
278
|
-
continue
|
|
593
|
+
for idx, statement in enumerate(policy.statement or []):
|
|
594
|
+
# Only check Allow statements
|
|
595
|
+
if statement.effect != "Allow":
|
|
596
|
+
continue
|
|
279
597
|
|
|
280
|
-
|
|
598
|
+
statement_actions = statement.get_actions()
|
|
281
599
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
+
)
|
|
286
604
|
|
|
287
|
-
|
|
288
|
-
|
|
605
|
+
if actions_match and matching_actions:
|
|
606
|
+
matching_statements.append((idx, statement, matching_actions))
|
|
289
607
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
608
|
+
# If no statements match, skip this requirement
|
|
609
|
+
if not matching_statements:
|
|
610
|
+
return issues
|
|
293
611
|
|
|
294
|
-
|
|
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
|
-
|
|
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,
|
|
323
641
|
)
|
|
324
|
-
|
|
642
|
+
)
|
|
643
|
+
return issues
|
|
325
644
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
)
|
|
336
655
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
f"Note: This is enforced at the policy level. "
|
|
344
|
-
f"Found {len(matching_statements)} statement(s) with these actions in the policy."
|
|
345
|
-
)
|
|
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
|
+
)
|
|
346
662
|
|
|
347
|
-
|
|
663
|
+
issues.extend(condition_issues)
|
|
348
664
|
|
|
349
665
|
return issues
|
|
350
666
|
|
|
@@ -530,7 +846,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
530
846
|
available_actions = list(service_detail.actions.keys())
|
|
531
847
|
|
|
532
848
|
# Find which actual AWS actions the wildcard would grant
|
|
533
|
-
_, granted_actions = fetcher.
|
|
849
|
+
_, granted_actions = fetcher.match_wildcard_action(
|
|
534
850
|
statement_action.split(":", 1)[1], # Just the action part (e.g., "C*")
|
|
535
851
|
available_actions,
|
|
536
852
|
)
|
|
@@ -545,7 +861,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
545
861
|
except re.error:
|
|
546
862
|
continue
|
|
547
863
|
|
|
548
|
-
except (ValueError, Exception):
|
|
864
|
+
except (ValueError, Exception): # pylint: disable=broad-exception-caught
|
|
549
865
|
# If we can't fetch the service or parse the action, fall back to prefix matching
|
|
550
866
|
stmt_prefix = statement_action.rstrip("*")
|
|
551
867
|
for pattern in patterns:
|
|
@@ -627,7 +943,20 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
627
943
|
|
|
628
944
|
if not any_present:
|
|
629
945
|
# Create a combined error for any_of
|
|
630
|
-
|
|
946
|
+
# Handle both simple conditions and nested all_of
|
|
947
|
+
condition_keys = []
|
|
948
|
+
for cond in any_of:
|
|
949
|
+
if "all_of" in cond:
|
|
950
|
+
# Nested all_of - collect all condition keys
|
|
951
|
+
nested_keys = [
|
|
952
|
+
c.get("condition_key", "unknown") for c in cond["all_of"]
|
|
953
|
+
]
|
|
954
|
+
condition_keys.append(f"({' + '.join(f'`{k}`' for k in nested_keys)})")
|
|
955
|
+
else:
|
|
956
|
+
# Simple condition
|
|
957
|
+
condition_keys.append(f"`{cond.get('condition_key', 'unknown')}`")
|
|
958
|
+
condition_keys_formatted = " OR ".join(condition_keys)
|
|
959
|
+
matching_actions_formatted = ", ".join(f"`{a}`" for a in matching_actions)
|
|
631
960
|
issues.append(
|
|
632
961
|
ValidationIssue(
|
|
633
962
|
severity=self.get_severity(config),
|
|
@@ -635,8 +964,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
635
964
|
statement_index=statement_idx,
|
|
636
965
|
issue_type="missing_required_condition_any_of",
|
|
637
966
|
message=(
|
|
638
|
-
f"Actions {
|
|
639
|
-
f"{
|
|
967
|
+
f"Actions `{matching_actions_formatted}` require at least ONE of these conditions: "
|
|
968
|
+
f"{condition_keys_formatted}"
|
|
640
969
|
),
|
|
641
970
|
action=", ".join(matching_actions),
|
|
642
971
|
suggestion=self._build_any_of_suggestion(any_of),
|
|
@@ -773,12 +1102,13 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
773
1102
|
condition_key, description, example, expected_value, operator
|
|
774
1103
|
)
|
|
775
1104
|
|
|
1105
|
+
matching_actions_str = ", ".join(f"`{a}`" for a in matching_actions)
|
|
776
1106
|
return ValidationIssue(
|
|
777
1107
|
severity=severity,
|
|
778
1108
|
statement_sid=statement.sid,
|
|
779
1109
|
statement_index=statement_idx,
|
|
780
1110
|
issue_type="missing_required_condition",
|
|
781
|
-
message=f"{message_prefix} Action(s) {
|
|
1111
|
+
message=f"{message_prefix} Action(s) `{matching_actions_str}` require condition `{condition_key}`",
|
|
782
1112
|
action=", ".join(matching_actions),
|
|
783
1113
|
condition_key=condition_key,
|
|
784
1114
|
suggestion=suggestion_text,
|
|
@@ -799,7 +1129,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
799
1129
|
Returns:
|
|
800
1130
|
Tuple of (suggestion_text, example_code)
|
|
801
1131
|
"""
|
|
802
|
-
suggestion = description if description else f"Add condition: {condition_key}"
|
|
1132
|
+
suggestion = description if description else f"Add condition: `{condition_key}`"
|
|
803
1133
|
|
|
804
1134
|
# Build example based on condition key type
|
|
805
1135
|
if example:
|
|
@@ -843,15 +1173,38 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
843
1173
|
suggestions.append("Add at least ONE of these conditions:")
|
|
844
1174
|
|
|
845
1175
|
for i, cond in enumerate(any_of_conditions, 1):
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1176
|
+
# Handle nested all_of blocks
|
|
1177
|
+
if "all_of" in cond:
|
|
1178
|
+
# Nested all_of - show all required conditions together
|
|
1179
|
+
all_of_list = cond["all_of"]
|
|
1180
|
+
condition_keys = [c.get("condition_key", "unknown") for c in all_of_list]
|
|
1181
|
+
condition_keys_formatted = " + ".join(f"`{k}`" for k in condition_keys)
|
|
1182
|
+
|
|
1183
|
+
option = f"\n- **Option {i}**: {condition_keys_formatted} (both required)"
|
|
1184
|
+
|
|
1185
|
+
# Use description from first condition or combine them
|
|
1186
|
+
descriptions = [
|
|
1187
|
+
c.get("description", "") for c in all_of_list if c.get("description")
|
|
1188
|
+
]
|
|
1189
|
+
if descriptions:
|
|
1190
|
+
option += f" - {descriptions[0]}"
|
|
1191
|
+
|
|
1192
|
+
# Show example from first condition that has one
|
|
1193
|
+
for c in all_of_list:
|
|
1194
|
+
if c.get("example"):
|
|
1195
|
+
# Example will be shown separately, just note it's available
|
|
1196
|
+
break
|
|
1197
|
+
else:
|
|
1198
|
+
# Simple condition (original behavior)
|
|
1199
|
+
condition_key = cond.get("condition_key", "unknown")
|
|
1200
|
+
description = cond.get("description", "")
|
|
1201
|
+
expected_value = cond.get("expected_value")
|
|
849
1202
|
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
1203
|
+
option = f"\n- **Option {i}**: `{condition_key}`"
|
|
1204
|
+
if description:
|
|
1205
|
+
option += f" - {description}"
|
|
1206
|
+
if expected_value is not None:
|
|
1207
|
+
option += f" (value: `{expected_value}`)"
|
|
855
1208
|
|
|
856
1209
|
suggestions.append(option)
|
|
857
1210
|
|
|
@@ -870,13 +1223,12 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
870
1223
|
description = condition_requirement.get("description", "")
|
|
871
1224
|
expected_value = condition_requirement.get("expected_value")
|
|
872
1225
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
)
|
|
1226
|
+
matching_actions_str = ", ".join(f"`{a}`" for a in matching_actions)
|
|
1227
|
+
message = f"FORBIDDEN: Action(s) `{matching_actions_str}` must NOT have condition `{condition_key}`"
|
|
876
1228
|
if expected_value is not None:
|
|
877
|
-
message += f" with value
|
|
1229
|
+
message += f" with value `{expected_value}`"
|
|
878
1230
|
|
|
879
|
-
suggestion = f"Remove the
|
|
1231
|
+
suggestion = f"Remove the `{condition_key}` condition from the statement"
|
|
880
1232
|
if description:
|
|
881
1233
|
suggestion += f". {description}"
|
|
882
1234
|
|