iam-policy-validator 1.4.0__py3-none-any.whl → 1.6.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.
Files changed (57) hide show
  1. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/METADATA +106 -78
  2. iam_policy_validator-1.6.0.dist-info/RECORD +82 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +20 -4
  5. iam_validator/checks/action_condition_enforcement.py +165 -8
  6. iam_validator/checks/action_resource_matching.py +424 -0
  7. iam_validator/checks/condition_key_validation.py +24 -2
  8. iam_validator/checks/condition_type_mismatch.py +259 -0
  9. iam_validator/checks/full_wildcard.py +67 -0
  10. iam_validator/checks/mfa_condition_check.py +112 -0
  11. iam_validator/checks/principal_validation.py +497 -3
  12. iam_validator/checks/sensitive_action.py +250 -0
  13. iam_validator/checks/service_wildcard.py +105 -0
  14. iam_validator/checks/set_operator_validation.py +157 -0
  15. iam_validator/checks/utils/sensitive_action_matcher.py +74 -32
  16. iam_validator/checks/wildcard_action.py +62 -0
  17. iam_validator/checks/wildcard_resource.py +131 -0
  18. iam_validator/commands/cache.py +1 -1
  19. iam_validator/commands/download_services.py +3 -8
  20. iam_validator/commands/validate.py +72 -13
  21. iam_validator/core/aws_fetcher.py +114 -64
  22. iam_validator/core/check_registry.py +167 -29
  23. iam_validator/core/condition_validators.py +626 -0
  24. iam_validator/core/config/__init__.py +81 -0
  25. iam_validator/core/config/aws_api.py +35 -0
  26. iam_validator/core/config/aws_global_conditions.py +160 -0
  27. iam_validator/core/config/category_suggestions.py +104 -0
  28. iam_validator/core/config/condition_requirements.py +155 -0
  29. iam_validator/core/{config_loader.py → config/config_loader.py} +32 -9
  30. iam_validator/core/config/defaults.py +523 -0
  31. iam_validator/core/config/principal_requirements.py +421 -0
  32. iam_validator/core/config/sensitive_actions.py +672 -0
  33. iam_validator/core/config/service_principals.py +95 -0
  34. iam_validator/core/config/wildcards.py +124 -0
  35. iam_validator/core/formatters/enhanced.py +11 -5
  36. iam_validator/core/formatters/sarif.py +78 -14
  37. iam_validator/core/models.py +14 -1
  38. iam_validator/core/policy_checks.py +4 -4
  39. iam_validator/core/pr_commenter.py +1 -1
  40. iam_validator/sdk/__init__.py +187 -0
  41. iam_validator/sdk/arn_matching.py +274 -0
  42. iam_validator/sdk/context.py +222 -0
  43. iam_validator/sdk/exceptions.py +48 -0
  44. iam_validator/sdk/helpers.py +177 -0
  45. iam_validator/sdk/policy_utils.py +425 -0
  46. iam_validator/sdk/shortcuts.py +283 -0
  47. iam_validator/utils/__init__.py +31 -0
  48. iam_validator/utils/cache.py +105 -0
  49. iam_validator/utils/regex.py +206 -0
  50. iam_policy_validator-1.4.0.dist-info/RECORD +0 -56
  51. iam_validator/checks/action_resource_constraint.py +0 -151
  52. iam_validator/checks/security_best_practices.py +0 -536
  53. iam_validator/core/aws_global_conditions.py +0 -137
  54. iam_validator/core/defaults.py +0 -393
  55. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/WHEEL +0 -0
  56. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/entry_points.txt +0 -0
  57. {iam_policy_validator-1.4.0.dist-info → iam_policy_validator-1.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,250 @@
1
+ """Sensitive action check - detects sensitive actions without IAM conditions."""
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from iam_validator.checks.utils.policy_level_checks import check_policy_level_actions
6
+ from iam_validator.checks.utils.sensitive_action_matcher import (
7
+ DEFAULT_SENSITIVE_ACTIONS,
8
+ check_sensitive_actions,
9
+ )
10
+ from iam_validator.checks.utils.wildcard_expansion import expand_wildcard_actions
11
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
12
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
13
+ from iam_validator.core.config.sensitive_actions import get_category_for_action
14
+ from iam_validator.core.models import Statement, ValidationIssue
15
+
16
+ if TYPE_CHECKING:
17
+ from iam_validator.core.models import IAMPolicy
18
+
19
+
20
+ class SensitiveActionCheck(PolicyCheck):
21
+ """Checks for sensitive actions without IAM conditions to limit their use."""
22
+
23
+ @property
24
+ def check_id(self) -> str:
25
+ return "sensitive_action"
26
+
27
+ @property
28
+ def description(self) -> str:
29
+ return "Checks for sensitive actions without conditions"
30
+
31
+ @property
32
+ def default_severity(self) -> str:
33
+ return "medium"
34
+
35
+ def _get_severity_for_action(self, action: str, config: CheckConfig) -> str:
36
+ """
37
+ Get severity for a specific action, considering category-based overrides.
38
+
39
+ Args:
40
+ action: The AWS action to check
41
+ config: Check configuration
42
+
43
+ Returns:
44
+ Severity level for the action (considers category overrides)
45
+ """
46
+ # Check if category severities are configured
47
+ category_severities = config.config.get("category_severities", {})
48
+ if not category_severities:
49
+ return self.get_severity(config)
50
+
51
+ # Get the category for this action
52
+ category = get_category_for_action(action)
53
+ if category and category in category_severities:
54
+ return category_severities[category]
55
+
56
+ # Fall back to default severity
57
+ return self.get_severity(config)
58
+
59
+ def _get_category_specific_suggestion(
60
+ self, action: str, config: CheckConfig
61
+ ) -> tuple[str, str]:
62
+ """
63
+ Get category-specific suggestion and example for an action.
64
+
65
+ Args:
66
+ action: The AWS action to check
67
+ config: Check configuration
68
+
69
+ Returns:
70
+ Tuple of (suggestion_text, example_text) tailored to the action's category
71
+ """
72
+ category = get_category_for_action(action)
73
+
74
+ # Get category suggestions from config (ABAC-focused by default)
75
+ # See: iam_validator/core/config/category_suggestions.py
76
+ category_suggestions = config.config.get("category_suggestions", {})
77
+
78
+ # Get category-specific content or fall back to generic ABAC guidance
79
+ if category and category in category_suggestions:
80
+ return (
81
+ category_suggestions[category]["suggestion"],
82
+ category_suggestions[category]["example"],
83
+ )
84
+
85
+ # Generic ABAC fallback for uncategorized actions
86
+ return (
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/X = aws:ResourceTag/X)\n"
89
+ "• Require MFA (aws:MultiFactorAuthPresent = true)\n"
90
+ "• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
91
+ '"Condition": {\n'
92
+ ' "StringEquals": {\n'
93
+ ' "aws:PrincipalTag/owner": "${aws:ResourceTag/owner}"\n'
94
+ " }\n"
95
+ "}",
96
+ )
97
+
98
+ async def execute(
99
+ self,
100
+ statement: Statement,
101
+ statement_idx: int,
102
+ fetcher: AWSServiceFetcher,
103
+ config: CheckConfig,
104
+ ) -> list[ValidationIssue]:
105
+ """Execute sensitive action check on a statement."""
106
+ issues = []
107
+
108
+ # Only check Allow statements
109
+ if statement.effect != "Allow":
110
+ return issues
111
+
112
+ actions = statement.get_actions()
113
+ has_conditions = statement.condition is not None and len(statement.condition) > 0
114
+
115
+ # Expand wildcards to actual actions using AWS API
116
+ expanded_actions = await expand_wildcard_actions(actions, fetcher)
117
+
118
+ # Check if sensitive actions match using any_of/all_of logic
119
+ is_sensitive, matched_actions = check_sensitive_actions(
120
+ expanded_actions, config, DEFAULT_SENSITIVE_ACTIONS
121
+ )
122
+
123
+ if is_sensitive and not has_conditions:
124
+ # Create appropriate message based on matched actions using configurable templates
125
+ if len(matched_actions) == 1:
126
+ message_template = config.config.get(
127
+ "message_single",
128
+ "Sensitive action '{action}' should have conditions to limit when it can be used",
129
+ )
130
+ message = message_template.format(action=matched_actions[0])
131
+ else:
132
+ action_list = "', '".join(matched_actions)
133
+ message_template = config.config.get(
134
+ "message_multiple",
135
+ "Sensitive actions '{actions}' should have conditions to limit when they can be used",
136
+ )
137
+ message = message_template.format(actions=action_list)
138
+
139
+ # Get category-specific suggestion and example (or use config defaults)
140
+ # Use the first matched action to determine the category
141
+ suggestion_text, example = self._get_category_specific_suggestion(
142
+ matched_actions[0], config
143
+ )
144
+
145
+ # Combine suggestion + example
146
+ suggestion = f"{suggestion_text}\n\nExample:\n{example}" if example else suggestion_text
147
+
148
+ # Determine severity based on the highest severity action in the list
149
+ # If single action, use its category severity
150
+ # If multiple actions, use the highest severity among them
151
+ severity = self.get_severity(config) # Default
152
+ if matched_actions:
153
+ # Get severity for first action (or highest if we want to be more sophisticated)
154
+ severity = self._get_severity_for_action(matched_actions[0], config)
155
+
156
+ issues.append(
157
+ ValidationIssue(
158
+ severity=severity,
159
+ statement_sid=statement.sid,
160
+ statement_index=statement_idx,
161
+ issue_type="missing_condition",
162
+ message=message,
163
+ action=(matched_actions[0] if len(matched_actions) == 1 else None),
164
+ suggestion=suggestion,
165
+ line_number=statement.line_number,
166
+ )
167
+ )
168
+
169
+ return issues
170
+
171
+ async def execute_policy(
172
+ self,
173
+ policy: "IAMPolicy",
174
+ policy_file: str,
175
+ fetcher: AWSServiceFetcher,
176
+ config: CheckConfig,
177
+ **kwargs,
178
+ ) -> list[ValidationIssue]:
179
+ """
180
+ Execute policy-level sensitive action checks.
181
+
182
+ This method examines the entire policy to detect privilege escalation patterns
183
+ and other security issues that span multiple statements.
184
+
185
+ Args:
186
+ policy: The complete IAM policy to check
187
+ policy_file: Path to the policy file (for context/reporting)
188
+ fetcher: AWS service fetcher for validation against AWS APIs
189
+ config: Configuration for this check instance
190
+
191
+ Returns:
192
+ List of ValidationIssue objects found by this check
193
+ """
194
+ del policy_file, fetcher # Not used in current implementation
195
+ issues = []
196
+
197
+ # Collect all actions from all Allow statements across the entire policy
198
+ all_actions: set[str] = set()
199
+ statement_map: dict[
200
+ str, list[tuple[int, str | None]]
201
+ ] = {} # action -> [(stmt_idx, sid), ...]
202
+
203
+ for idx, statement in enumerate(policy.statement):
204
+ if statement.effect == "Allow":
205
+ actions = statement.get_actions()
206
+ # Filter out wildcards for privilege escalation detection
207
+ filtered_actions = [a for a in actions if a != "*"]
208
+
209
+ for action in filtered_actions:
210
+ all_actions.add(action)
211
+ if action not in statement_map:
212
+ statement_map[action] = []
213
+ statement_map[action].append((idx, statement.sid))
214
+
215
+ # Get configuration for sensitive actions
216
+ sensitive_actions_config = config.config.get("sensitive_actions")
217
+ sensitive_patterns_config = config.config.get("sensitive_action_patterns")
218
+
219
+ # Check for privilege escalation patterns using all_of logic
220
+ # We need to check both exact actions and patterns
221
+ policy_issues = []
222
+
223
+ # Check sensitive_actions configuration
224
+ if sensitive_actions_config:
225
+ policy_issues.extend(
226
+ check_policy_level_actions(
227
+ list(all_actions),
228
+ statement_map,
229
+ sensitive_actions_config,
230
+ config,
231
+ "actions",
232
+ self.get_severity,
233
+ )
234
+ )
235
+
236
+ # Check sensitive_action_patterns configuration
237
+ if sensitive_patterns_config:
238
+ policy_issues.extend(
239
+ check_policy_level_actions(
240
+ list(all_actions),
241
+ statement_map,
242
+ sensitive_patterns_config,
243
+ config,
244
+ "patterns",
245
+ self.get_severity,
246
+ )
247
+ )
248
+
249
+ issues.extend(policy_issues)
250
+ return issues
@@ -0,0 +1,105 @@
1
+ """Service wildcard check - detects service-level wildcards like 'iam:*', 's3:*'."""
2
+
3
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
4
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
5
+ from iam_validator.core.models import Statement, ValidationIssue
6
+
7
+
8
+ class ServiceWildcardCheck(PolicyCheck):
9
+ """Checks for service-level wildcards (e.g., 'iam:*', 's3:*') which grant all permissions for a service."""
10
+
11
+ @property
12
+ def check_id(self) -> str:
13
+ return "service_wildcard"
14
+
15
+ @property
16
+ def description(self) -> str:
17
+ return "Checks for service-level wildcards (e.g., 'iam:*', 's3:*')"
18
+
19
+ @property
20
+ def default_severity(self) -> str:
21
+ return "high"
22
+
23
+ async def execute(
24
+ self,
25
+ statement: Statement,
26
+ statement_idx: int,
27
+ fetcher: AWSServiceFetcher,
28
+ config: CheckConfig,
29
+ ) -> list[ValidationIssue]:
30
+ """Execute service wildcard check on a statement."""
31
+ issues = []
32
+
33
+ # Only check Allow statements
34
+ if statement.effect != "Allow":
35
+ return issues
36
+
37
+ actions = statement.get_actions()
38
+ allowed_services = self._get_allowed_service_wildcards(config)
39
+
40
+ for action in actions:
41
+ # Skip full wildcard (covered by wildcard_action check)
42
+ if action == "*":
43
+ continue
44
+
45
+ # Check if it's a service-level wildcard (e.g., "iam:*", "s3:*")
46
+ if ":" in action and action.endswith(":*"):
47
+ service = action.split(":")[0]
48
+
49
+ # Check if this service is in the allowed list
50
+ if service not in allowed_services:
51
+ # Get message template and replace placeholders
52
+ message_template = config.config.get(
53
+ "message",
54
+ "Service-level wildcard '{action}' grants all permissions for {service} service",
55
+ )
56
+ suggestion_template = config.config.get(
57
+ "suggestion",
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
+ )
60
+ example_template = config.config.get("example", "")
61
+
62
+ message = message_template.format(action=action, service=service)
63
+ suggestion_text = suggestion_template.format(action=action, service=service)
64
+ example = (
65
+ example_template.format(action=action, service=service)
66
+ if example_template
67
+ else ""
68
+ )
69
+
70
+ # Combine suggestion + example
71
+ suggestion = (
72
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
73
+ )
74
+
75
+ issues.append(
76
+ ValidationIssue(
77
+ severity=self.get_severity(config),
78
+ statement_sid=statement.sid,
79
+ statement_index=statement_idx,
80
+ issue_type="overly_permissive",
81
+ message=message,
82
+ action=action,
83
+ suggestion=suggestion,
84
+ line_number=statement.line_number,
85
+ )
86
+ )
87
+
88
+ return issues
89
+
90
+ def _get_allowed_service_wildcards(self, config: CheckConfig) -> set[str]:
91
+ """
92
+ Get list of services that are allowed to use service-level wildcards.
93
+
94
+ This allows configuration like:
95
+ service_wildcard:
96
+ allowed_services:
97
+ - "logs" # Allow "logs:*"
98
+ - "cloudwatch" # Allow "cloudwatch:*"
99
+
100
+ Returns empty set if no exceptions are configured.
101
+ """
102
+ allowed = config.config.get("allowed_services", [])
103
+ if allowed and isinstance(allowed, list):
104
+ return set(allowed)
105
+ return set()
@@ -0,0 +1,157 @@
1
+ """Set Operator Validation Check.
2
+
3
+ Validates proper usage of ForAllValues and ForAnyValue set operators in IAM policies.
4
+
5
+ Based on AWS IAM best practices:
6
+ https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html
7
+ """
8
+
9
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
10
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
11
+ from iam_validator.core.condition_validators import (
12
+ is_multivalued_context_key,
13
+ normalize_operator,
14
+ )
15
+ from iam_validator.core.models import Statement, ValidationIssue
16
+
17
+
18
+ class SetOperatorValidationCheck(PolicyCheck):
19
+ """Check for proper usage of ForAllValues and ForAnyValue set operators."""
20
+
21
+ @property
22
+ def check_id(self) -> str:
23
+ """Unique identifier for this check."""
24
+ return "set_operator_validation"
25
+
26
+ @property
27
+ def description(self) -> str:
28
+ """Description of what this check does."""
29
+ return "Validates proper usage of ForAllValues and ForAnyValue set operators"
30
+
31
+ @property
32
+ def default_severity(self) -> str:
33
+ """Default severity level for issues found by this check."""
34
+ return "error"
35
+
36
+ async def execute(
37
+ self,
38
+ statement: Statement,
39
+ statement_idx: int,
40
+ fetcher: AWSServiceFetcher,
41
+ config: CheckConfig,
42
+ ) -> list[ValidationIssue]:
43
+ """
44
+ Execute the set operator validation check.
45
+
46
+ Validates:
47
+ 1. ForAllValues/ForAnyValue not used with single-valued context keys (anti-pattern)
48
+ 2. ForAllValues with Allow effect includes Null condition check (security)
49
+ 3. ForAnyValue with Deny effect includes Null condition check (predictability)
50
+
51
+ Args:
52
+ statement: The IAM statement to check
53
+ statement_idx: Index of this statement in the policy
54
+ fetcher: AWS service fetcher (unused but required by interface)
55
+ config: Check configuration
56
+
57
+ Returns:
58
+ List of validation issues found
59
+ """
60
+ issues = []
61
+
62
+ # Only check statements with conditions
63
+ if not statement.condition:
64
+ return issues
65
+
66
+ statement_sid = statement.sid
67
+ line_number = statement.line_number
68
+ effect = statement.effect
69
+
70
+ # Track which condition keys have set operators and Null checks
71
+ set_operator_keys: dict[str, str] = {} # key -> operator prefix
72
+ null_checked_keys: set[str] = set()
73
+
74
+ # First pass: Identify set operators and Null checks
75
+ for operator, conditions in statement.condition.items():
76
+ base_operator, operator_type, set_prefix = normalize_operator(operator)
77
+
78
+ # Track Null checks
79
+ if base_operator == "Null":
80
+ for condition_key in conditions.keys():
81
+ null_checked_keys.add(condition_key)
82
+
83
+ # Track set operators
84
+ if set_prefix in ["ForAllValues", "ForAnyValue"]:
85
+ for condition_key in conditions.keys():
86
+ set_operator_keys[condition_key] = set_prefix
87
+
88
+ # Second pass: Validate set operator usage
89
+ for operator, conditions in statement.condition.items():
90
+ base_operator, operator_type, set_prefix = normalize_operator(operator)
91
+
92
+ if not set_prefix:
93
+ continue
94
+
95
+ # Check each condition key used with a set operator
96
+ for condition_key, condition_values in conditions.items():
97
+ # Issue 1: Set operator used with single-valued context key (anti-pattern)
98
+ if not is_multivalued_context_key(condition_key):
99
+ issues.append(
100
+ ValidationIssue(
101
+ severity=self.get_severity(config),
102
+ message=(
103
+ f"Set operator '{set_prefix}' should not be used with single-valued "
104
+ f"condition key '{condition_key}'. This can lead to overly permissive policies. "
105
+ f"Set operators are designed for multivalued context keys like 'aws:TagKeys'. "
106
+ f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
107
+ ),
108
+ statement_sid=statement_sid,
109
+ statement_index=statement_idx,
110
+ issue_type="set_operator_on_single_valued_key",
111
+ condition_key=condition_key,
112
+ line_number=line_number,
113
+ )
114
+ )
115
+
116
+ # Issue 2: ForAllValues with Allow effect without Null check (security risk)
117
+ if set_prefix == "ForAllValues" and effect == "Allow":
118
+ if condition_key not in null_checked_keys:
119
+ issues.append(
120
+ ValidationIssue(
121
+ severity="warning",
122
+ message=(
123
+ f"Security risk: ForAllValues with Allow effect on '{condition_key}' "
124
+ f"should include a Null condition check. Without it, requests with missing "
125
+ f'\'{condition_key}\' will be granted access. Add: \'"Null": {{"{condition_key}": "false"}}\'. '
126
+ f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
127
+ ),
128
+ statement_sid=statement_sid,
129
+ statement_index=statement_idx,
130
+ issue_type="forallvalues_allow_without_null_check",
131
+ condition_key=condition_key,
132
+ line_number=line_number,
133
+ )
134
+ )
135
+
136
+ # Issue 3: ForAnyValue with Deny effect without Null check (unpredictable)
137
+ if set_prefix == "ForAnyValue" and effect == "Deny":
138
+ if condition_key not in null_checked_keys:
139
+ issues.append(
140
+ ValidationIssue(
141
+ severity="warning",
142
+ message=(
143
+ f"Unpredictable behavior: ForAnyValue with Deny effect on '{condition_key}' "
144
+ f"should include a Null condition check. Without it, requests with missing "
145
+ f"'{condition_key}' will evaluate to 'No match' instead of denying access. "
146
+ f'Add: \'"Null": {{"{condition_key}": "false"}}\'. '
147
+ f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
148
+ ),
149
+ statement_sid=statement_sid,
150
+ statement_index=statement_idx,
151
+ issue_type="foranyvalue_deny_without_null_check",
152
+ condition_key=condition_key,
153
+ line_number=line_number,
154
+ )
155
+ )
156
+
157
+ return issues
@@ -2,6 +2,11 @@
2
2
 
3
3
  This module provides functionality to match actions against sensitive action
4
4
  configurations, supporting exact matches, regex patterns, and any_of/all_of logic.
5
+
6
+ Performance optimizations:
7
+ - Uses frozenset for O(1) lookups
8
+ - LRU cache for compiled regex patterns
9
+ - Lazy loading of default actions from modular data structure
5
10
  """
6
11
 
7
12
  import re
@@ -9,30 +14,57 @@ from functools import lru_cache
9
14
  from re import Pattern
10
15
 
11
16
  from iam_validator.core.check_registry import CheckConfig
17
+ from iam_validator.core.config.sensitive_actions import get_sensitive_actions
18
+
19
+ # Lazy-loaded default set of sensitive actions
20
+ # This will be loaded only when first accessed
21
+ _DEFAULT_SENSITIVE_ACTIONS_CACHE: frozenset[str] | None = None
22
+
23
+
24
+ def _get_default_sensitive_actions() -> frozenset[str]:
25
+ """
26
+ Get default sensitive actions with lazy loading and caching.
27
+
28
+ Returns:
29
+ Frozenset of all default sensitive actions
30
+
31
+ Performance:
32
+ - First call: Loads from sensitive actions list
33
+ - Subsequent calls: O(1) cached lookup
34
+ """
35
+ global _DEFAULT_SENSITIVE_ACTIONS_CACHE
36
+ if _DEFAULT_SENSITIVE_ACTIONS_CACHE is None:
37
+ _DEFAULT_SENSITIVE_ACTIONS_CACHE = get_sensitive_actions()
38
+ return _DEFAULT_SENSITIVE_ACTIONS_CACHE
39
+
40
+
41
+ def get_sensitive_actions_by_categories(categories: list[str] | None = None) -> frozenset[str]:
42
+ """
43
+ Get sensitive actions filtered by categories.
12
44
 
13
- # Default set of sensitive actions for backward compatibility
14
- # Using frozenset for O(1) lookups and immutability
15
- DEFAULT_SENSITIVE_ACTIONS = frozenset(
16
- {
17
- "ec2:DeleteVolume",
18
- "ec2:TerminateInstances",
19
- "eks:DeleteCluster",
20
- "iam:AttachRolePolicy",
21
- "iam:AttachUserPolicy",
22
- "iam:CreateAccessKey",
23
- "iam:CreateRole",
24
- "iam:CreateUser",
25
- "iam:DeleteRole",
26
- "iam:DeleteUser",
27
- "iam:PutRolePolicy",
28
- "iam:PutUserPolicy",
29
- "lambda:DeleteFunction",
30
- "rds:DeleteDBInstance",
31
- "s3:DeleteBucket",
32
- "s3:DeleteBucketPolicy",
33
- "s3:PutBucketPolicy",
34
- }
35
- )
45
+ Args:
46
+ categories: List of category IDs to include. If None, returns all actions.
47
+ Valid categories: 'credential_exposure', 'data_access',
48
+ 'priv_esc', 'resource_exposure'
49
+
50
+ Returns:
51
+ Frozenset of sensitive actions matching the specified categories
52
+
53
+ Examples:
54
+ >>> # Get all sensitive actions (default behavior)
55
+ >>> all_actions = get_sensitive_actions_by_categories()
56
+
57
+ >>> # Get only privilege escalation actions
58
+ >>> priv_esc = get_sensitive_actions_by_categories(['priv_esc'])
59
+
60
+ >>> # Get credential exposure and data access actions
61
+ >>> sensitive = get_sensitive_actions_by_categories(['credential_exposure', 'data_access'])
62
+ """
63
+ return get_sensitive_actions(categories)
64
+
65
+
66
+ # Export for backward compatibility
67
+ DEFAULT_SENSITIVE_ACTIONS = _get_default_sensitive_actions()
36
68
 
37
69
 
38
70
  # Global regex pattern cache for performance
@@ -61,15 +93,28 @@ def check_sensitive_actions(
61
93
  Args:
62
94
  actions: List of actions to check
63
95
  config: Check configuration
64
- default_actions: Default sensitive actions to use if no config (defaults to DEFAULT_SENSITIVE_ACTIONS)
96
+ default_actions: Default sensitive actions to use if no config (lazy-loaded)
65
97
 
66
98
  Returns:
67
99
  tuple[bool, list[str]]: (is_sensitive, matched_actions)
68
100
  - is_sensitive: True if the actions match the sensitive criteria
69
101
  - matched_actions: List of actions that matched the criteria
102
+
103
+ Performance:
104
+ - Uses lazy-loaded defaults (only loaded on first use)
105
+ - O(1) frozenset lookups for action matching
70
106
  """
71
- if default_actions is None:
72
- default_actions = DEFAULT_SENSITIVE_ACTIONS
107
+ # Check if categories are specified in config
108
+ categories = config.config.get("categories")
109
+ if categories is not None:
110
+ # If categories is an empty list, disable the check
111
+ if len(categories) == 0:
112
+ return False, []
113
+ # Get sensitive actions filtered by categories
114
+ default_actions = get_sensitive_actions_by_categories(categories)
115
+ elif default_actions is None:
116
+ # Use all categories if no specific categories configured
117
+ default_actions = _get_default_sensitive_actions()
73
118
 
74
119
  # Filter out wildcards
75
120
  filtered_actions = [a for a in actions if a != "*"]
@@ -77,12 +122,9 @@ def check_sensitive_actions(
77
122
  return False, []
78
123
 
79
124
  # Get configuration for both sensitive_actions and sensitive_action_patterns
80
- sub_check_config = config.config.get("sensitive_action_check", {})
81
- if not isinstance(sub_check_config, dict):
82
- return False, []
83
-
84
- sensitive_actions_config = sub_check_config.get("sensitive_actions")
85
- sensitive_patterns_config = sub_check_config.get("sensitive_action_patterns")
125
+ # Config is now flat (no longer nested under sensitive_action_check)
126
+ sensitive_actions_config = config.config.get("sensitive_actions")
127
+ sensitive_patterns_config = config.config.get("sensitive_action_patterns")
86
128
 
87
129
  # Check sensitive_actions (exact matches)
88
130
  actions_match, actions_matched = check_actions_config(