iam-policy-validator 1.7.1__py3-none-any.whl → 1.7.2__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.1.dist-info → iam_policy_validator-1.7.2.dist-info}/METADATA +1 -2
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +34 -33
- iam_validator/__version__.py +4 -2
- iam_validator/checks/action_condition_enforcement.py +20 -13
- iam_validator/checks/action_resource_matching.py +70 -36
- iam_validator/checks/condition_key_validation.py +7 -7
- iam_validator/checks/condition_type_mismatch.py +8 -6
- iam_validator/checks/full_wildcard.py +2 -8
- iam_validator/checks/mfa_condition_check.py +8 -8
- iam_validator/checks/principal_validation.py +24 -20
- iam_validator/checks/sensitive_action.py +3 -9
- iam_validator/checks/service_wildcard.py +2 -8
- iam_validator/checks/sid_uniqueness.py +1 -1
- iam_validator/checks/wildcard_action.py +2 -8
- iam_validator/checks/wildcard_resource.py +2 -8
- iam_validator/commands/validate.py +2 -2
- iam_validator/core/aws_fetcher.py +115 -22
- iam_validator/core/config/config_loader.py +1 -2
- iam_validator/core/config/defaults.py +16 -7
- iam_validator/core/constants.py +57 -0
- iam_validator/core/formatters/console.py +10 -1
- iam_validator/core/formatters/csv.py +2 -1
- iam_validator/core/formatters/enhanced.py +42 -8
- iam_validator/core/formatters/markdown.py +2 -1
- iam_validator/core/models.py +22 -7
- iam_validator/core/policy_checks.py +5 -4
- iam_validator/core/policy_loader.py +71 -14
- iam_validator/core/report.py +65 -24
- iam_validator/integrations/github_integration.py +4 -5
- iam_validator/utils/__init__.py +4 -0
- iam_validator/utils/terminal.py +22 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -112,12 +112,12 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
112
112
|
ValidationIssue(
|
|
113
113
|
severity=self.get_severity(config),
|
|
114
114
|
issue_type="blocked_principal",
|
|
115
|
-
message=f"Blocked principal detected: {principal}
|
|
115
|
+
message=f"Blocked principal detected: `{principal}`. "
|
|
116
116
|
f"This principal is explicitly blocked by your security policy.",
|
|
117
117
|
statement_index=statement_idx,
|
|
118
118
|
statement_sid=statement.sid,
|
|
119
119
|
line_number=statement.line_number,
|
|
120
|
-
suggestion=f"Remove the principal
|
|
120
|
+
suggestion=f"Remove the principal `{principal}` or add appropriate conditions to restrict access. "
|
|
121
121
|
"Consider using more specific principals instead of wildcards.",
|
|
122
122
|
)
|
|
123
123
|
)
|
|
@@ -131,8 +131,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
131
131
|
ValidationIssue(
|
|
132
132
|
severity=self.get_severity(config),
|
|
133
133
|
issue_type="unauthorized_principal",
|
|
134
|
-
message=f"Principal not in allowed list: {principal}
|
|
135
|
-
f"Only principals in the allowed_principals whitelist are permitted.",
|
|
134
|
+
message=f"Principal not in allowed list: `{principal}`. "
|
|
135
|
+
f"Only principals in the `allowed_principals` whitelist are permitted.",
|
|
136
136
|
statement_index=statement_idx,
|
|
137
137
|
statement_sid=statement.sid,
|
|
138
138
|
line_number=statement.line_number,
|
|
@@ -151,7 +151,7 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
151
151
|
ValidationIssue(
|
|
152
152
|
severity=self.get_severity(config),
|
|
153
153
|
issue_type="missing_principal_conditions",
|
|
154
|
-
message=f"Principal
|
|
154
|
+
message=f"Principal `{principal}` requires conditions: {', '.join(f'`{c}`' for c in missing_conditions)}. "
|
|
155
155
|
f"This principal must have these condition keys to restrict access.",
|
|
156
156
|
statement_index=statement_idx,
|
|
157
157
|
statement_sid=statement.sid,
|
|
@@ -169,7 +169,11 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
169
169
|
# Check advanced format: principal_condition_requirements
|
|
170
170
|
if principal_condition_requirements:
|
|
171
171
|
condition_issues = self._validate_principal_condition_requirements(
|
|
172
|
-
statement,
|
|
172
|
+
statement,
|
|
173
|
+
statement_idx,
|
|
174
|
+
principals,
|
|
175
|
+
principal_condition_requirements,
|
|
176
|
+
config,
|
|
173
177
|
)
|
|
174
178
|
issues.extend(condition_issues)
|
|
175
179
|
|
|
@@ -629,15 +633,18 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
629
633
|
or self.get_severity(config)
|
|
630
634
|
)
|
|
631
635
|
|
|
636
|
+
suggestion_text, example_code = self._build_condition_suggestion(
|
|
637
|
+
condition_key, description, example, expected_value, operator
|
|
638
|
+
)
|
|
639
|
+
|
|
632
640
|
return ValidationIssue(
|
|
633
641
|
severity=severity,
|
|
634
642
|
statement_sid=statement.sid,
|
|
635
643
|
statement_index=statement_idx,
|
|
636
644
|
issue_type="missing_principal_condition",
|
|
637
645
|
message=f"{message_prefix} Principal(s) {matching_principals} require condition '{condition_key}'",
|
|
638
|
-
suggestion=
|
|
639
|
-
|
|
640
|
-
),
|
|
646
|
+
suggestion=suggestion_text,
|
|
647
|
+
example=example_code,
|
|
641
648
|
line_number=statement.line_number,
|
|
642
649
|
)
|
|
643
650
|
|
|
@@ -648,8 +655,8 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
648
655
|
example: str,
|
|
649
656
|
expected_value: Any = None,
|
|
650
657
|
operator: str = "StringEquals",
|
|
651
|
-
) -> str:
|
|
652
|
-
"""Build
|
|
658
|
+
) -> tuple[str, str]:
|
|
659
|
+
"""Build suggestion and example for adding the missing condition.
|
|
653
660
|
|
|
654
661
|
Args:
|
|
655
662
|
condition_key: The condition key
|
|
@@ -659,19 +666,16 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
659
666
|
operator: Condition operator
|
|
660
667
|
|
|
661
668
|
Returns:
|
|
662
|
-
|
|
669
|
+
Tuple of (suggestion_text, example_code)
|
|
663
670
|
"""
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
if description:
|
|
667
|
-
parts.append(description)
|
|
671
|
+
suggestion = description if description else f"Add condition: {condition_key}"
|
|
668
672
|
|
|
669
673
|
# Build example based on condition key type
|
|
670
674
|
if example:
|
|
671
|
-
|
|
675
|
+
example_code = example
|
|
672
676
|
else:
|
|
673
677
|
# Auto-generate example
|
|
674
|
-
example_lines = [
|
|
678
|
+
example_lines = [f' "{operator}": {{']
|
|
675
679
|
|
|
676
680
|
if isinstance(expected_value, list):
|
|
677
681
|
value_str = (
|
|
@@ -698,9 +702,9 @@ class PrincipalValidationCheck(PolicyCheck):
|
|
|
698
702
|
example_lines.append(f' "{condition_key}": {value_str}')
|
|
699
703
|
example_lines.append(" }")
|
|
700
704
|
|
|
701
|
-
|
|
705
|
+
example_code = "\n".join(example_lines)
|
|
702
706
|
|
|
703
|
-
return
|
|
707
|
+
return suggestion, example_code
|
|
704
708
|
|
|
705
709
|
def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
|
|
706
710
|
"""Build suggestion for any_of conditions.
|
|
@@ -85,7 +85,7 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
85
85
|
# Generic ABAC fallback for uncategorized actions
|
|
86
86
|
return (
|
|
87
87
|
"Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
|
|
88
|
-
"• Match principal tags to resource tags (aws:PrincipalTag
|
|
88
|
+
"• Match principal tags to resource tags (aws:PrincipalTag/<tag-name> = aws:ResourceTag/<tag-name>)\n"
|
|
89
89
|
"• Require MFA (aws:MultiFactorAuthPresent = true)\n"
|
|
90
90
|
"• Restrict by IP (aws:SourceIp) or VPC (aws:SourceVpc)",
|
|
91
91
|
'"Condition": {\n'
|
|
@@ -142,13 +142,6 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
142
142
|
matched_actions[0], config
|
|
143
143
|
)
|
|
144
144
|
|
|
145
|
-
# Combine suggestion + example
|
|
146
|
-
suggestion = (
|
|
147
|
-
f"{suggestion_text}\n\nExample:\n```json\n{example}\n```"
|
|
148
|
-
if example
|
|
149
|
-
else suggestion_text
|
|
150
|
-
)
|
|
151
|
-
|
|
152
145
|
# Determine severity based on the highest severity action in the list
|
|
153
146
|
# If single action, use its category severity
|
|
154
147
|
# If multiple actions, use the highest severity among them
|
|
@@ -165,7 +158,8 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
165
158
|
issue_type="missing_condition",
|
|
166
159
|
message=message,
|
|
167
160
|
action=(matched_actions[0] if len(matched_actions) == 1 else None),
|
|
168
|
-
suggestion=
|
|
161
|
+
suggestion=suggestion_text,
|
|
162
|
+
example=example if example else None,
|
|
169
163
|
line_number=statement.line_number,
|
|
170
164
|
)
|
|
171
165
|
)
|
|
@@ -60,20 +60,13 @@ class ServiceWildcardCheck(PolicyCheck):
|
|
|
60
60
|
example_template = config.config.get("example", "")
|
|
61
61
|
|
|
62
62
|
message = message_template.format(action=action, service=service)
|
|
63
|
-
|
|
63
|
+
suggestion = suggestion_template.format(action=action, service=service)
|
|
64
64
|
example = (
|
|
65
65
|
example_template.format(action=action, service=service)
|
|
66
66
|
if example_template
|
|
67
67
|
else ""
|
|
68
68
|
)
|
|
69
69
|
|
|
70
|
-
# Combine suggestion + example
|
|
71
|
-
suggestion = (
|
|
72
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
73
|
-
if example
|
|
74
|
-
else suggestion_text
|
|
75
|
-
)
|
|
76
|
-
|
|
77
70
|
issues.append(
|
|
78
71
|
ValidationIssue(
|
|
79
72
|
severity=self.get_severity(config),
|
|
@@ -83,6 +76,7 @@ class ServiceWildcardCheck(PolicyCheck):
|
|
|
83
76
|
message=message,
|
|
84
77
|
action=action,
|
|
85
78
|
suggestion=suggestion,
|
|
79
|
+
example=example if example else None,
|
|
86
80
|
line_number=statement.line_number,
|
|
87
81
|
)
|
|
88
82
|
)
|
|
@@ -91,7 +91,7 @@ def _check_sid_uniqueness_impl(policy: IAMPolicy, severity: str) -> list[Validat
|
|
|
91
91
|
statement_sid=duplicate_sid,
|
|
92
92
|
statement_index=idx,
|
|
93
93
|
issue_type="duplicate_sid",
|
|
94
|
-
message=f"Statement ID
|
|
94
|
+
message=f"Statement ID `{duplicate_sid}` is used **{count} times** in this policy (found in statements {statement_numbers})",
|
|
95
95
|
suggestion="Change this SID to a unique value. Statement IDs help identify and reference specific statements, so duplicates can cause confusion.",
|
|
96
96
|
line_number=statement.line_number,
|
|
97
97
|
)
|
|
@@ -39,19 +39,12 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
39
39
|
# Check for wildcard action (Action: "*")
|
|
40
40
|
if "*" in actions:
|
|
41
41
|
message = config.config.get("message", "Statement allows all actions (*)")
|
|
42
|
-
|
|
42
|
+
suggestion = config.config.get(
|
|
43
43
|
"suggestion",
|
|
44
44
|
"Replace wildcard with specific actions needed for your use case",
|
|
45
45
|
)
|
|
46
46
|
example = config.config.get("example", "")
|
|
47
47
|
|
|
48
|
-
# Combine suggestion + example
|
|
49
|
-
suggestion = (
|
|
50
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
51
|
-
if example
|
|
52
|
-
else suggestion_text
|
|
53
|
-
)
|
|
54
|
-
|
|
55
48
|
issues.append(
|
|
56
49
|
ValidationIssue(
|
|
57
50
|
severity=self.get_severity(config),
|
|
@@ -60,6 +53,7 @@ class WildcardActionCheck(PolicyCheck):
|
|
|
60
53
|
issue_type="overly_permissive",
|
|
61
54
|
message=message,
|
|
62
55
|
suggestion=suggestion,
|
|
56
|
+
example=example if example else None,
|
|
63
57
|
line_number=statement.line_number,
|
|
64
58
|
)
|
|
65
59
|
)
|
|
@@ -63,18 +63,11 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
63
63
|
|
|
64
64
|
# Flag the issue if actions are not all allowed or no allowed_wildcards configured
|
|
65
65
|
message = config.config.get("message", "Statement applies to all resources (*)")
|
|
66
|
-
|
|
66
|
+
suggestion = config.config.get(
|
|
67
67
|
"suggestion", "Replace wildcard with specific resource ARNs"
|
|
68
68
|
)
|
|
69
69
|
example = config.config.get("example", "")
|
|
70
70
|
|
|
71
|
-
# Combine suggestion + example
|
|
72
|
-
suggestion = (
|
|
73
|
-
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
74
|
-
if example
|
|
75
|
-
else suggestion_text
|
|
76
|
-
)
|
|
77
|
-
|
|
78
71
|
issues.append(
|
|
79
72
|
ValidationIssue(
|
|
80
73
|
severity=self.get_severity(config),
|
|
@@ -83,6 +76,7 @@ class WildcardResourceCheck(PolicyCheck):
|
|
|
83
76
|
issue_type="overly_permissive",
|
|
84
77
|
message=message,
|
|
85
78
|
suggestion=suggestion,
|
|
79
|
+
example=example if example else None,
|
|
86
80
|
line_number=statement.line_number,
|
|
87
81
|
)
|
|
88
82
|
)
|
|
@@ -254,9 +254,9 @@ Examples:
|
|
|
254
254
|
policy_type=policy_type,
|
|
255
255
|
)
|
|
256
256
|
|
|
257
|
-
# Generate report
|
|
257
|
+
# Generate report (include parsing errors if any)
|
|
258
258
|
generator = ReportGenerator()
|
|
259
|
-
report = generator.generate_report(results)
|
|
259
|
+
report = generator.generate_report(results, parsing_errors=loader.parsing_errors)
|
|
260
260
|
|
|
261
261
|
# Output results
|
|
262
262
|
if args.format is None:
|
|
@@ -27,11 +27,13 @@ import os
|
|
|
27
27
|
import re
|
|
28
28
|
import sys
|
|
29
29
|
import time
|
|
30
|
+
from dataclasses import dataclass
|
|
30
31
|
from pathlib import Path
|
|
31
32
|
from typing import Any
|
|
32
33
|
|
|
33
34
|
import httpx
|
|
34
35
|
|
|
36
|
+
from iam_validator.core import constants
|
|
35
37
|
from iam_validator.core.config import AWS_SERVICE_REFERENCE_BASE_URL
|
|
36
38
|
from iam_validator.core.models import ServiceDetail, ServiceInfo
|
|
37
39
|
from iam_validator.utils.cache import LRUCache
|
|
@@ -39,6 +41,23 @@ from iam_validator.utils.cache import LRUCache
|
|
|
39
41
|
logger = logging.getLogger(__name__)
|
|
40
42
|
|
|
41
43
|
|
|
44
|
+
@dataclass
|
|
45
|
+
class ConditionKeyValidationResult:
|
|
46
|
+
"""Result of condition key validation.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
is_valid: True if the condition key is valid for the action
|
|
50
|
+
error_message: Short error message if invalid (shown prominently)
|
|
51
|
+
warning_message: Warning message if valid but not recommended
|
|
52
|
+
suggestion: Detailed suggestion with valid keys (shown in collapsible section)
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
is_valid: bool
|
|
56
|
+
error_message: str | None = None
|
|
57
|
+
warning_message: str | None = None
|
|
58
|
+
suggestion: str | None = None
|
|
59
|
+
|
|
60
|
+
|
|
42
61
|
class CompiledPatterns:
|
|
43
62
|
"""Pre-compiled regex patterns for validation.
|
|
44
63
|
|
|
@@ -199,10 +218,10 @@ class AWSServiceFetcher:
|
|
|
199
218
|
|
|
200
219
|
def __init__(
|
|
201
220
|
self,
|
|
202
|
-
timeout: float =
|
|
221
|
+
timeout: float = constants.DEFAULT_HTTP_TIMEOUT_SECONDS,
|
|
203
222
|
retries: int = 3,
|
|
204
223
|
enable_cache: bool = True,
|
|
205
|
-
cache_ttl: int =
|
|
224
|
+
cache_ttl: int = constants.DEFAULT_CACHE_TTL_SECONDS,
|
|
206
225
|
memory_cache_size: int = 256,
|
|
207
226
|
connection_pool_size: int = 50,
|
|
208
227
|
keepalive_connections: int = 20,
|
|
@@ -303,7 +322,7 @@ class AWSServiceFetcher:
|
|
|
303
322
|
limits=httpx.Limits(
|
|
304
323
|
max_keepalive_connections=self.keepalive_connections,
|
|
305
324
|
max_connections=self.connection_pool_size,
|
|
306
|
-
keepalive_expiry=
|
|
325
|
+
keepalive_expiry=constants.DEFAULT_HTTP_TIMEOUT_SECONDS, # Keep connections alive
|
|
307
326
|
),
|
|
308
327
|
http2=True, # Enable HTTP/2 for multiplexing
|
|
309
328
|
)
|
|
@@ -658,7 +677,7 @@ class AWSServiceFetcher:
|
|
|
658
677
|
return service_detail
|
|
659
678
|
except FileNotFoundError:
|
|
660
679
|
pass
|
|
661
|
-
raise ValueError(f"Service
|
|
680
|
+
raise ValueError(f"Service `{service_name}` not found in {self.aws_services_dir}")
|
|
662
681
|
|
|
663
682
|
# Fetch service list and find URL from API
|
|
664
683
|
services = await self.fetch_services()
|
|
@@ -676,7 +695,7 @@ class AWSServiceFetcher:
|
|
|
676
695
|
|
|
677
696
|
return service_detail
|
|
678
697
|
|
|
679
|
-
raise ValueError(f"Service
|
|
698
|
+
raise ValueError(f"Service `{service_name}` not found")
|
|
680
699
|
|
|
681
700
|
async def fetch_multiple_services(self, service_names: list[str]) -> dict[str, ServiceDetail]:
|
|
682
701
|
"""Fetch multiple services concurrently with optimized batching."""
|
|
@@ -823,7 +842,7 @@ class AWSServiceFetcher:
|
|
|
823
842
|
|
|
824
843
|
async def validate_condition_key(
|
|
825
844
|
self, action: str, condition_key: str, resources: list[str] | None = None
|
|
826
|
-
) ->
|
|
845
|
+
) -> ConditionKeyValidationResult:
|
|
827
846
|
"""
|
|
828
847
|
Validate condition key against action and optionally resource types.
|
|
829
848
|
|
|
@@ -833,10 +852,11 @@ class AWSServiceFetcher:
|
|
|
833
852
|
resources: Optional list of resource ARNs to validate against
|
|
834
853
|
|
|
835
854
|
Returns:
|
|
836
|
-
|
|
855
|
+
ConditionKeyValidationResult with:
|
|
837
856
|
- is_valid: True if key is valid (even with warning)
|
|
838
|
-
- error_message:
|
|
857
|
+
- error_message: Short error message if invalid (shown prominently)
|
|
839
858
|
- warning_message: Warning message if valid but not recommended
|
|
859
|
+
- suggestion: Detailed suggestion with valid keys (shown in collapsible section)
|
|
840
860
|
"""
|
|
841
861
|
try:
|
|
842
862
|
from iam_validator.core.config.aws_global_conditions import (
|
|
@@ -852,10 +872,9 @@ class AWSServiceFetcher:
|
|
|
852
872
|
if global_conditions.is_valid_global_key(condition_key):
|
|
853
873
|
is_global_key = True
|
|
854
874
|
else:
|
|
855
|
-
return (
|
|
856
|
-
False,
|
|
857
|
-
f"Invalid AWS global condition key:
|
|
858
|
-
None,
|
|
875
|
+
return ConditionKeyValidationResult(
|
|
876
|
+
is_valid=False,
|
|
877
|
+
error_message=f"Invalid AWS global condition key: `{condition_key}`.",
|
|
859
878
|
)
|
|
860
879
|
|
|
861
880
|
# Fetch service detail (cached)
|
|
@@ -863,7 +882,7 @@ class AWSServiceFetcher:
|
|
|
863
882
|
|
|
864
883
|
# Check service-specific condition keys
|
|
865
884
|
if condition_key in service_detail.condition_keys:
|
|
866
|
-
return True
|
|
885
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
867
886
|
|
|
868
887
|
# Check action-specific condition keys
|
|
869
888
|
if action_name in service_detail.actions:
|
|
@@ -872,7 +891,7 @@ class AWSServiceFetcher:
|
|
|
872
891
|
action_detail.action_condition_keys
|
|
873
892
|
and condition_key in action_detail.action_condition_keys
|
|
874
893
|
):
|
|
875
|
-
return True
|
|
894
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
876
895
|
|
|
877
896
|
# Check resource-specific condition keys
|
|
878
897
|
# Get resource types required by this action
|
|
@@ -886,7 +905,7 @@ class AWSServiceFetcher:
|
|
|
886
905
|
resource_type = service_detail.resources.get(resource_name)
|
|
887
906
|
if resource_type and resource_type.condition_keys:
|
|
888
907
|
if condition_key in resource_type.condition_keys:
|
|
889
|
-
return True
|
|
908
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
890
909
|
|
|
891
910
|
# If it's a global key but the action has specific condition keys defined,
|
|
892
911
|
# AWS allows it but the key may not be available in every request context
|
|
@@ -898,21 +917,95 @@ class AWSServiceFetcher:
|
|
|
898
917
|
f"Verify that '{condition_key}' is available for this specific action's request context. "
|
|
899
918
|
f"Consider using '*IfExists' operators (e.g., StringEqualsIfExists) if the key might be missing."
|
|
900
919
|
)
|
|
901
|
-
return True,
|
|
920
|
+
return ConditionKeyValidationResult(is_valid=True, warning_message=warning_msg)
|
|
902
921
|
|
|
903
922
|
# If it's a global key and action doesn't define specific keys, allow it
|
|
904
923
|
if is_global_key:
|
|
905
|
-
return True
|
|
924
|
+
return ConditionKeyValidationResult(is_valid=True)
|
|
925
|
+
|
|
926
|
+
# Short error message
|
|
927
|
+
error_msg = f"Condition key `{condition_key}` is not valid for action `{action}`"
|
|
928
|
+
|
|
929
|
+
# Collect valid condition keys for this action
|
|
930
|
+
valid_keys = set()
|
|
906
931
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
932
|
+
# Add service-level condition keys
|
|
933
|
+
if service_detail.condition_keys:
|
|
934
|
+
if isinstance(service_detail.condition_keys, dict):
|
|
935
|
+
valid_keys.update(service_detail.condition_keys.keys())
|
|
936
|
+
elif isinstance(service_detail.condition_keys, list):
|
|
937
|
+
valid_keys.update(service_detail.condition_keys)
|
|
938
|
+
|
|
939
|
+
# Add action-specific condition keys
|
|
940
|
+
if action_name in service_detail.actions:
|
|
941
|
+
action_detail = service_detail.actions[action_name]
|
|
942
|
+
if action_detail.action_condition_keys:
|
|
943
|
+
if isinstance(action_detail.action_condition_keys, dict):
|
|
944
|
+
valid_keys.update(action_detail.action_condition_keys.keys())
|
|
945
|
+
elif isinstance(action_detail.action_condition_keys, list):
|
|
946
|
+
valid_keys.update(action_detail.action_condition_keys)
|
|
947
|
+
|
|
948
|
+
# Add resource-specific condition keys
|
|
949
|
+
if action_detail.resources:
|
|
950
|
+
for res_req in action_detail.resources:
|
|
951
|
+
resource_name = res_req.get("Name", "")
|
|
952
|
+
if resource_name:
|
|
953
|
+
resource_type = service_detail.resources.get(resource_name)
|
|
954
|
+
if resource_type and resource_type.condition_keys:
|
|
955
|
+
if isinstance(resource_type.condition_keys, dict):
|
|
956
|
+
valid_keys.update(resource_type.condition_keys.keys())
|
|
957
|
+
elif isinstance(resource_type.condition_keys, list):
|
|
958
|
+
valid_keys.update(resource_type.condition_keys)
|
|
959
|
+
|
|
960
|
+
# Build detailed suggestion with valid keys (goes in collapsible section)
|
|
961
|
+
suggestion_parts = []
|
|
962
|
+
|
|
963
|
+
if valid_keys:
|
|
964
|
+
# Sort and limit to first 10 keys for readability
|
|
965
|
+
sorted_keys = sorted(valid_keys)
|
|
966
|
+
suggestion_parts.append("**Valid condition keys for this action:**")
|
|
967
|
+
if len(sorted_keys) <= 10:
|
|
968
|
+
for key in sorted_keys:
|
|
969
|
+
suggestion_parts.append(f"- `{key}`")
|
|
970
|
+
else:
|
|
971
|
+
for key in sorted_keys[:10]:
|
|
972
|
+
suggestion_parts.append(f"- `{key}`")
|
|
973
|
+
suggestion_parts.append(f"- ... and {len(sorted_keys) - 10} more")
|
|
974
|
+
|
|
975
|
+
suggestion_parts.append("")
|
|
976
|
+
suggestion_parts.append(
|
|
977
|
+
"**Global condition keys** (e.g., `aws:ResourceOrgID`, `aws:RequestedRegion`, `aws:SourceIp`, `aws:SourceVpce`) "
|
|
978
|
+
"can also be used with any AWS action"
|
|
979
|
+
)
|
|
980
|
+
else:
|
|
981
|
+
# No action-specific keys - mention global keys
|
|
982
|
+
suggestion_parts.append(
|
|
983
|
+
"This action does not have specific condition keys defined.\n\n"
|
|
984
|
+
"However, you can use **global condition keys** such as:\n"
|
|
985
|
+
"- `aws:RequestedRegion`\n"
|
|
986
|
+
"- `aws:SourceIp`\n"
|
|
987
|
+
"- `aws:SourceVpce`\n"
|
|
988
|
+
"- `aws:UserAgent`\n"
|
|
989
|
+
"- `aws:CurrentTime`\n"
|
|
990
|
+
"- `aws:SecureTransport`\n"
|
|
991
|
+
"- `aws:PrincipalArn`\n"
|
|
992
|
+
"- And many others"
|
|
993
|
+
)
|
|
994
|
+
|
|
995
|
+
suggestion = "\n".join(suggestion_parts)
|
|
996
|
+
|
|
997
|
+
return ConditionKeyValidationResult(
|
|
998
|
+
is_valid=False,
|
|
999
|
+
error_message=error_msg,
|
|
1000
|
+
suggestion=suggestion,
|
|
911
1001
|
)
|
|
912
1002
|
|
|
913
1003
|
except Exception as e:
|
|
914
1004
|
logger.error(f"Error validating condition key {condition_key} for {action}: {e}")
|
|
915
|
-
return
|
|
1005
|
+
return ConditionKeyValidationResult(
|
|
1006
|
+
is_valid=False,
|
|
1007
|
+
error_message=f"Failed to validate condition key: {str(e)}",
|
|
1008
|
+
)
|
|
916
1009
|
|
|
917
1010
|
async def clear_caches(self) -> None:
|
|
918
1011
|
"""Clear all caches (memory and disk)."""
|
|
@@ -5,6 +5,7 @@ Loads and parses configuration from YAML files, environment variables,
|
|
|
5
5
|
and command-line arguments.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import importlib
|
|
8
9
|
import importlib.util
|
|
9
10
|
import inspect
|
|
10
11
|
import logging
|
|
@@ -314,8 +315,6 @@ class ConfigLoader:
|
|
|
314
315
|
module_name, class_name = parts
|
|
315
316
|
|
|
316
317
|
# Import the module
|
|
317
|
-
import importlib
|
|
318
|
-
|
|
319
318
|
module = importlib.import_module(module_name)
|
|
320
319
|
check_class = getattr(module, class_name)
|
|
321
320
|
|
|
@@ -16,6 +16,7 @@ Benefits of code-first approach:
|
|
|
16
16
|
- 5-10x faster than YAML parsing
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
+
from iam_validator.core import constants
|
|
19
20
|
from iam_validator.core.config.category_suggestions import get_category_suggestions
|
|
20
21
|
from iam_validator.core.config.condition_requirements import CONDITION_REQUIREMENTS
|
|
21
22
|
from iam_validator.core.config.principal_requirements import (
|
|
@@ -70,11 +71,11 @@ DEFAULT_CONFIG = {
|
|
|
70
71
|
# Cache AWS service definitions locally (persists between runs)
|
|
71
72
|
"cache_enabled": True,
|
|
72
73
|
# Cache TTL in hours (default: 168 = 7 days)
|
|
73
|
-
"cache_ttl_hours":
|
|
74
|
+
"cache_ttl_hours": constants.DEFAULT_CACHE_TTL_HOURS,
|
|
74
75
|
# Severity levels that cause validation to fail
|
|
75
76
|
# IAM Validity: error, warning, info
|
|
76
77
|
# Security: critical, high, medium, low
|
|
77
|
-
"fail_on_severity":
|
|
78
|
+
"fail_on_severity": list(constants.HIGH_SEVERITY_LEVELS),
|
|
78
79
|
},
|
|
79
80
|
# ========================================================================
|
|
80
81
|
# AWS IAM Validation Checks (17 checks total)
|
|
@@ -188,7 +189,7 @@ DEFAULT_CONFIG = {
|
|
|
188
189
|
"enabled": True,
|
|
189
190
|
"severity": "error", # IAM validity error
|
|
190
191
|
"description": "Validates ARN format for resources",
|
|
191
|
-
"arn_pattern":
|
|
192
|
+
"arn_pattern": constants.DEFAULT_ARN_VALIDATION_PATTERN,
|
|
192
193
|
},
|
|
193
194
|
# ========================================================================
|
|
194
195
|
# 9. PRINCIPAL VALIDATION
|
|
@@ -389,6 +390,18 @@ DEFAULT_CONFIG = {
|
|
|
389
390
|
# Services that are allowed to use wildcards (default: logs, cloudwatch, xray)
|
|
390
391
|
# See: iam_validator/core/config/wildcards.py
|
|
391
392
|
"allowed_services": list(DEFAULT_SERVICE_WILDCARDS),
|
|
393
|
+
"message": "Service wildcard '{action}' grants all permissions for the {service} service",
|
|
394
|
+
"suggestion": (
|
|
395
|
+
"Replace '{action}' with specific actions needed for your use case to follow least-privilege principle.\n"
|
|
396
|
+
"Find valid {service} actions: https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
|
|
397
|
+
),
|
|
398
|
+
"example": (
|
|
399
|
+
"Replace:\n"
|
|
400
|
+
' "Action": ["{action}"]\n'
|
|
401
|
+
"\n"
|
|
402
|
+
"With specific actions:\n"
|
|
403
|
+
' "Action": ["{service}:Describe*", "{service}:List*"]\n'
|
|
404
|
+
),
|
|
392
405
|
},
|
|
393
406
|
# ========================================================================
|
|
394
407
|
# 16. SENSITIVE ACTION
|
|
@@ -482,10 +495,6 @@ DEFAULT_CONFIG = {
|
|
|
482
495
|
# - prevent_public_ip: Prevents 0.0.0.0/0 IP ranges
|
|
483
496
|
#
|
|
484
497
|
# See: iam_validator/core/config/condition_requirements.py
|
|
485
|
-
# Python API:
|
|
486
|
-
# from iam_validator.core.config import CONDITION_REQUIREMENTS
|
|
487
|
-
# import copy
|
|
488
|
-
# requirements = copy.deepcopy(CONDITION_REQUIREMENTS)
|
|
489
498
|
"action_condition_enforcement": {
|
|
490
499
|
"enabled": True,
|
|
491
500
|
"severity": "high", # Default severity (can be overridden per-requirement)
|
iam_validator/core/constants.py
CHANGED
|
@@ -62,6 +62,21 @@ DEFAULT_CONFIG_FILENAMES = [
|
|
|
62
62
|
".iam-validator.yml",
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
+
# ============================================================================
|
|
66
|
+
# Severity Levels
|
|
67
|
+
# ============================================================================
|
|
68
|
+
# Severity level groupings for filtering and categorization
|
|
69
|
+
# Used across formatters and report generation
|
|
70
|
+
|
|
71
|
+
# High severity issues that typically fail validation
|
|
72
|
+
HIGH_SEVERITY_LEVELS = ("error", "critical", "high")
|
|
73
|
+
|
|
74
|
+
# Medium severity issues (warnings)
|
|
75
|
+
MEDIUM_SEVERITY_LEVELS = ("warning", "medium")
|
|
76
|
+
|
|
77
|
+
# Low severity issues (informational)
|
|
78
|
+
LOW_SEVERITY_LEVELS = ("info", "low")
|
|
79
|
+
|
|
65
80
|
# ============================================================================
|
|
66
81
|
# GitHub Integration
|
|
67
82
|
# ============================================================================
|
|
@@ -72,3 +87,45 @@ BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
|
72
87
|
# HTML comment markers for identifying bot-generated content (for cleanup/updates)
|
|
73
88
|
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
74
89
|
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
90
|
+
|
|
91
|
+
# GitHub comment size limits
|
|
92
|
+
# GitHub's actual limit is 65536 characters, but we use a smaller limit for safety
|
|
93
|
+
GITHUB_MAX_COMMENT_LENGTH = 65000 # Maximum single comment length
|
|
94
|
+
GITHUB_COMMENT_SPLIT_LIMIT = 60000 # Target size when splitting into multiple parts
|
|
95
|
+
|
|
96
|
+
# Comment size estimation parameters (used for multi-part comment splitting)
|
|
97
|
+
COMMENT_BASE_OVERHEAD_CHARS = 2000 # Base overhead for headers/footers
|
|
98
|
+
COMMENT_CHARS_PER_ISSUE_ESTIMATE = 500 # Average characters per issue
|
|
99
|
+
COMMENT_CONTINUATION_OVERHEAD_CHARS = 200 # Overhead for continuation markers
|
|
100
|
+
FORMATTING_SAFETY_BUFFER = 100 # Safety buffer for formatting calculations
|
|
101
|
+
|
|
102
|
+
# ============================================================================
|
|
103
|
+
# Console Display Settings
|
|
104
|
+
# ============================================================================
|
|
105
|
+
|
|
106
|
+
# Panel width for formatted console output
|
|
107
|
+
CONSOLE_PANEL_WIDTH = 100
|
|
108
|
+
|
|
109
|
+
# Rich console color styles
|
|
110
|
+
CONSOLE_HEADER_COLOR = "bright_blue"
|
|
111
|
+
|
|
112
|
+
# ============================================================================
|
|
113
|
+
# Cache and Timeout Settings
|
|
114
|
+
# ============================================================================
|
|
115
|
+
|
|
116
|
+
# Cache TTL (Time To Live) - 7 days
|
|
117
|
+
DEFAULT_CACHE_TTL_HOURS = 168 # 7 days in hours
|
|
118
|
+
DEFAULT_CACHE_TTL_SECONDS = 604800 # 7 days in seconds (168 * 3600)
|
|
119
|
+
|
|
120
|
+
# HTTP request timeout in seconds
|
|
121
|
+
DEFAULT_HTTP_TIMEOUT_SECONDS = 30.0
|
|
122
|
+
|
|
123
|
+
# Time conversion constants
|
|
124
|
+
SECONDS_PER_HOUR = 3600
|
|
125
|
+
|
|
126
|
+
# ============================================================================
|
|
127
|
+
# AWS Documentation URLs
|
|
128
|
+
# ============================================================================
|
|
129
|
+
|
|
130
|
+
# AWS Service Authorization Reference (for finding valid actions, resources, and condition keys)
|
|
131
|
+
AWS_SERVICE_AUTH_REF_URL = "https://docs.aws.amazon.com/service-authorization/latest/reference/reference_policies_actions-resources-contextkeys.html"
|