iam-policy-validator 1.15.6__py3-none-any.whl → 1.16.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.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/METADATA +1 -1
- {iam_policy_validator-1.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/RECORD +13 -12
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/condition_key_validation.py +62 -25
- iam_validator/checks/condition_type_mismatch.py +20 -0
- iam_validator/checks/ifexists_condition_check.py +210 -0
- iam_validator/checks/set_operator_validation.py +20 -6
- iam_validator/core/check_registry.py +1 -0
- iam_validator/core/condition_validators.py +77 -3
- {iam_policy_validator-1.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: iam-policy-validator
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.16.0
|
|
4
4
|
Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
|
|
5
5
|
Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
|
|
6
6
|
Project-URL: Documentation, https://boogy.github.io/iam-policy-validator
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
iam_validator/__init__.py,sha256=xHdUASOxFHwEXfT_GSr_KrkLlnxZ-pAAr1wW1PwAGko,693
|
|
2
2
|
iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
|
|
3
|
-
iam_validator/__version__.py,sha256=
|
|
4
|
-
iam_validator/checks/__init__.py,sha256=
|
|
3
|
+
iam_validator/__version__.py,sha256=SNgWrnwHrJ7-BbIPlpeXBPHcfB0-l1X47rnMf2CYA6I,374
|
|
4
|
+
iam_validator/checks/__init__.py,sha256=ybmPohOLXWUpPULY1b_DY0ZfazA4YzB9xRJGjUvsK3c,2217
|
|
5
5
|
iam_validator/checks/action_condition_enforcement.py,sha256=2-XUMbof9tQ7SHZNmAHMkR1DgbOIzY2eFWlp9S9dwLk,60625
|
|
6
6
|
iam_validator/checks/action_resource_matching.py,sha256=qND0hfDgNoxFEdLWwrxOPVDfdj3k50nzedT2qF7nK7o,19428
|
|
7
7
|
iam_validator/checks/action_validation.py,sha256=gy9_mujYbojBboLk0WwuJYJjggrY2sRwHAFvJnoLTYE,4823
|
|
8
|
-
iam_validator/checks/condition_key_validation.py,sha256=
|
|
9
|
-
iam_validator/checks/condition_type_mismatch.py,sha256=
|
|
8
|
+
iam_validator/checks/condition_key_validation.py,sha256=X0fVQ_6ZZgyUmaT87m0aMusk-gb98rnh3UFeVhfKXH8,5714
|
|
9
|
+
iam_validator/checks/condition_type_mismatch.py,sha256=KmlFHOHenmFAPacJjxq4jd-H8kibvkw_Wh_aoNPUNuI,11635
|
|
10
10
|
iam_validator/checks/full_wildcard.py,sha256=0TkkHtV0MZ6nZtJRtGdn3wwOMM96TRyGO7l7mmdHNUo,2325
|
|
11
|
+
iam_validator/checks/ifexists_condition_check.py,sha256=9Q32bA0sy2sQxMZQee4l_DMLzQ5zjyJWjOb7Fm60K-E,11166
|
|
11
12
|
iam_validator/checks/mfa_condition_check.py,sha256=EYOzESzsZVlxW9Pp6hSqPZkH0UT1h21SoZibBX7k_OU,7705
|
|
12
13
|
iam_validator/checks/not_action_not_resource.py,sha256=18wB1NKaCj4X7o8xcTLmtFjsXFJLXTukzp1FhZzvMa8,9779
|
|
13
14
|
iam_validator/checks/policy_size.py,sha256=eJd36Nj4gqWLIkQ5imhHR1hGtQ6T-iJsC22Wd1VSUf0,4681
|
|
@@ -17,7 +18,7 @@ iam_validator/checks/principal_validation.py,sha256=oXSooVQIgUfbTRulgpiLACSJ2LZX
|
|
|
17
18
|
iam_validator/checks/resource_validation.py,sha256=k7qHIwX7IDf4MCWIvl9G17aINzTuZLOHDHRWrujbCaM,7787
|
|
18
19
|
iam_validator/checks/sensitive_action.py,sha256=LHicl6dd5E1JV19cLKvFAMGzLzYr5h_Y7QvS8s57kvA,18952
|
|
19
20
|
iam_validator/checks/service_wildcard.py,sha256=1epcET5oDclAmzxhtmQfKBg3Q5Rl4VXBMoZouxCJLpM,4114
|
|
20
|
-
iam_validator/checks/set_operator_validation.py,sha256=
|
|
21
|
+
iam_validator/checks/set_operator_validation.py,sha256=lV6y4PZ_qaRePfIezEoSf21oHAAPLsEdJD3aD8f6T6o,8285
|
|
21
22
|
iam_validator/checks/sid_uniqueness.py,sha256=MPQwALBjcvbY4Q55mpxDrGMqmVCMb13YSg621YbQYF8,6048
|
|
22
23
|
iam_validator/checks/trust_policy_validation.py,sha256=r3fM2hU7W0yCFS6hbd4ZSlD8y2tm0zk99mUVlIt0LsI,17956
|
|
23
24
|
iam_validator/checks/wildcard_action.py,sha256=VhWezb1JXmFnwV9WKgVu-48ythScGfZasg8fFd6tAG4,2001
|
|
@@ -41,10 +42,10 @@ iam_validator/core/__init__.py,sha256=hYXkSbxplKzhM6dqrVzV4M3k7GKLsZbgExypxKq74g
|
|
|
41
42
|
iam_validator/core/access_analyzer.py,sha256=mtMaY-FnKjKEVITky_9ywZe1FaCAm61ElRv5Z_ZeC7E,24562
|
|
42
43
|
iam_validator/core/access_analyzer_report.py,sha256=UMm2RNGj2rAKav1zsCw_htQZZRwRC0jjayd2zvKma1A,24896
|
|
43
44
|
iam_validator/core/aws_fetcher.py,sha256=op93QvtGmeLF9dHobl2IuoPDeunn33pBLb8h7XjtmoQ,920
|
|
44
|
-
iam_validator/core/check_registry.py,sha256=
|
|
45
|
+
iam_validator/core/check_registry.py,sha256=iSFly0rloQ71NW-2dbmTx1gaAXwsmKwkZ_3GNdnwTfA,28655
|
|
45
46
|
iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
|
|
46
47
|
iam_validator/core/codeowners.py,sha256=dfRjYTpcTVmc-h95i4EoPXCXlcblD8yryeJBaTKQfjM,7530
|
|
47
|
-
iam_validator/core/condition_validators.py,sha256=
|
|
48
|
+
iam_validator/core/condition_validators.py,sha256=E7TJ46LJ1NDCDG061mtEiJJZF23_E3GdnhBdIjK1Gt0,30631
|
|
48
49
|
iam_validator/core/constants.py,sha256=O80dQ6AtgsCJPempRtNlKaSIVE61rg6YDPV5vgaSnAY,7771
|
|
49
50
|
iam_validator/core/diff_parser.py,sha256=5Jxa6WvQZtG5grblZeUH2OQ2R46tFLK-h8tvkHOSfLk,12110
|
|
50
51
|
iam_validator/core/finding_fingerprint.py,sha256=NJIlu8NhdenWbLS7ww8LyWFasJgpKWN63-DprrNW7Zs,4353
|
|
@@ -112,8 +113,8 @@ iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYu
|
|
|
112
113
|
iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
|
|
113
114
|
iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
|
|
114
115
|
iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
|
|
115
|
-
iam_policy_validator-1.
|
|
116
|
-
iam_policy_validator-1.
|
|
117
|
-
iam_policy_validator-1.
|
|
118
|
-
iam_policy_validator-1.
|
|
119
|
-
iam_policy_validator-1.
|
|
116
|
+
iam_policy_validator-1.16.0.dist-info/METADATA,sha256=AgKsjkJqDBzL6zzDJeetMwSQIowHGPAYgCHHhJqVA9s,34939
|
|
117
|
+
iam_policy_validator-1.16.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
118
|
+
iam_policy_validator-1.16.0.dist-info/entry_points.txt,sha256=VXAcx1evo9fuxX0Gtj3J2HnzWcBHSXugiZwBtQ1BXE0,162
|
|
119
|
+
iam_policy_validator-1.16.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
120
|
+
iam_policy_validator-1.16.0.dist-info/RECORD,,
|
iam_validator/__version__.py
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This file is the single source of truth for the package version.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
__version__ = "1.
|
|
6
|
+
__version__ = "1.16.0"
|
|
7
7
|
# Parse version, handling pre-release suffixes like -rc, -alpha, -beta
|
|
8
8
|
_version_base = __version__.split("-", maxsplit=1)[0] # Remove pre-release suffix if present
|
|
9
9
|
__version_info__ = tuple(int(part) for part in _version_base.split("."))
|
iam_validator/checks/__init__.py
CHANGED
|
@@ -10,6 +10,7 @@ from iam_validator.checks.action_validation import ActionValidationCheck
|
|
|
10
10
|
from iam_validator.checks.condition_key_validation import ConditionKeyValidationCheck
|
|
11
11
|
from iam_validator.checks.condition_type_mismatch import ConditionTypeMismatchCheck
|
|
12
12
|
from iam_validator.checks.full_wildcard import FullWildcardCheck
|
|
13
|
+
from iam_validator.checks.ifexists_condition_check import IfExistsConditionCheck
|
|
13
14
|
from iam_validator.checks.mfa_condition_check import MFAConditionCheck
|
|
14
15
|
from iam_validator.checks.not_action_not_resource import NotActionNotResourceCheck
|
|
15
16
|
from iam_validator.checks.policy_size import PolicySizeCheck
|
|
@@ -31,6 +32,7 @@ __all__ = [
|
|
|
31
32
|
"ConditionKeyValidationCheck",
|
|
32
33
|
"ConditionTypeMismatchCheck",
|
|
33
34
|
"FullWildcardCheck",
|
|
35
|
+
"IfExistsConditionCheck",
|
|
34
36
|
"MFAConditionCheck",
|
|
35
37
|
"NotActionNotResourceCheck",
|
|
36
38
|
"PolicySizeCheck",
|
|
@@ -4,6 +4,7 @@ from typing import ClassVar
|
|
|
4
4
|
|
|
5
5
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
6
6
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
7
|
+
from iam_validator.core.condition_validators import has_if_exists_suffix
|
|
7
8
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
8
9
|
|
|
9
10
|
|
|
@@ -37,8 +38,14 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
37
38
|
resources = statement.get_resources()
|
|
38
39
|
|
|
39
40
|
# Extract all condition keys from all condition operators
|
|
40
|
-
for
|
|
41
|
+
for operator, conditions in statement.condition.items():
|
|
42
|
+
operator_has_ifexists = has_if_exists_suffix(operator)
|
|
43
|
+
|
|
41
44
|
for condition_key in conditions.keys():
|
|
45
|
+
key_valid_for_any_action = False
|
|
46
|
+
key_invalid_actions: list[tuple] = []
|
|
47
|
+
first_warning_result = None
|
|
48
|
+
|
|
42
49
|
# Validate this condition key against each action in the statement
|
|
43
50
|
for action in actions:
|
|
44
51
|
# Skip wildcard actions
|
|
@@ -48,41 +55,71 @@ class ConditionKeyValidationCheck(PolicyCheck):
|
|
|
48
55
|
# Validate against action and resource types
|
|
49
56
|
result = await fetcher.validate_condition_key(action, condition_key, resources)
|
|
50
57
|
|
|
51
|
-
if
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
)
|
|
66
|
-
)
|
|
67
|
-
# Only report once per condition key (not per action)
|
|
68
|
-
break
|
|
69
|
-
elif result.warning_message and warn_on_global_keys:
|
|
70
|
-
# Add warning for global condition keys with action-specific keys
|
|
71
|
-
# Only if warn_on_global_condition_keys is enabled
|
|
58
|
+
if result.is_valid:
|
|
59
|
+
key_valid_for_any_action = True
|
|
60
|
+
if result.warning_message and first_warning_result is None:
|
|
61
|
+
first_warning_result = (action, result)
|
|
62
|
+
else:
|
|
63
|
+
key_invalid_actions.append((action, result))
|
|
64
|
+
|
|
65
|
+
# If IfExists is used and the key is valid for at least one action,
|
|
66
|
+
# suppress errors for actions that don't support it.
|
|
67
|
+
# Warnings (global key usage) are still reported.
|
|
68
|
+
if operator_has_ifexists and key_valid_for_any_action:
|
|
69
|
+
if first_warning_result and warn_on_global_keys:
|
|
70
|
+
action, result = first_warning_result
|
|
71
|
+
warning_msg = result.warning_message or ""
|
|
72
72
|
issues.append(
|
|
73
73
|
ValidationIssue(
|
|
74
74
|
severity="warning",
|
|
75
75
|
statement_sid=statement_sid,
|
|
76
76
|
statement_index=statement_idx,
|
|
77
77
|
issue_type="global_condition_key_with_action_specific",
|
|
78
|
-
message=
|
|
78
|
+
message=warning_msg,
|
|
79
79
|
action=action,
|
|
80
80
|
condition_key=condition_key,
|
|
81
81
|
line_number=line_number,
|
|
82
82
|
field_name="condition",
|
|
83
83
|
)
|
|
84
84
|
)
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
continue # Skip error reporting
|
|
86
|
+
|
|
87
|
+
# Report errors (first invalid action)
|
|
88
|
+
if key_invalid_actions:
|
|
89
|
+
action, result = key_invalid_actions[0]
|
|
90
|
+
issues.append(
|
|
91
|
+
ValidationIssue(
|
|
92
|
+
severity=self.get_severity(config),
|
|
93
|
+
statement_sid=statement_sid,
|
|
94
|
+
statement_index=statement_idx,
|
|
95
|
+
issue_type="invalid_condition_key",
|
|
96
|
+
message=result.error_message
|
|
97
|
+
or f"Invalid condition key: `{condition_key}`",
|
|
98
|
+
action=action,
|
|
99
|
+
condition_key=condition_key,
|
|
100
|
+
line_number=line_number,
|
|
101
|
+
suggestion=result.suggestion,
|
|
102
|
+
field_name="condition",
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
# Report warnings for valid keys (no IfExists suppression path)
|
|
108
|
+
if first_warning_result and warn_on_global_keys:
|
|
109
|
+
action, result = first_warning_result
|
|
110
|
+
warning_msg = result.warning_message or ""
|
|
111
|
+
issues.append(
|
|
112
|
+
ValidationIssue(
|
|
113
|
+
severity="warning",
|
|
114
|
+
statement_sid=statement_sid,
|
|
115
|
+
statement_index=statement_idx,
|
|
116
|
+
issue_type="global_condition_key_with_action_specific",
|
|
117
|
+
message=warning_msg,
|
|
118
|
+
action=action,
|
|
119
|
+
condition_key=condition_key,
|
|
120
|
+
line_number=line_number,
|
|
121
|
+
field_name="condition",
|
|
122
|
+
)
|
|
123
|
+
)
|
|
87
124
|
|
|
88
125
|
return issues
|
|
@@ -8,6 +8,7 @@ from typing import ClassVar
|
|
|
8
8
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
9
9
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
10
10
|
from iam_validator.core.condition_validators import (
|
|
11
|
+
has_if_exists_suffix,
|
|
11
12
|
normalize_operator,
|
|
12
13
|
translate_type,
|
|
13
14
|
validate_value_for_type,
|
|
@@ -71,6 +72,25 @@ class ConditionTypeMismatchCheck(PolicyCheck):
|
|
|
71
72
|
# Unknown operator - this will be caught by another check
|
|
72
73
|
continue
|
|
73
74
|
|
|
75
|
+
# Detect NullIfExists as invalid syntax (must be BEFORE skip_operators guard)
|
|
76
|
+
if base_operator == "Null" and has_if_exists_suffix(operator):
|
|
77
|
+
issues.append(
|
|
78
|
+
ValidationIssue(
|
|
79
|
+
severity=self.get_severity(config),
|
|
80
|
+
message=(
|
|
81
|
+
f"Invalid operator `{operator}`. The `Null` condition operator "
|
|
82
|
+
f"does not support the `IfExists` suffix. The `Null` operator "
|
|
83
|
+
f"already checks for key existence \u2014 use `Null` directly."
|
|
84
|
+
),
|
|
85
|
+
statement_sid=statement_sid,
|
|
86
|
+
statement_index=statement_idx,
|
|
87
|
+
issue_type="invalid_operator",
|
|
88
|
+
line_number=line_number,
|
|
89
|
+
field_name="condition",
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
continue
|
|
93
|
+
|
|
74
94
|
if base_operator in skip_operators:
|
|
75
95
|
continue
|
|
76
96
|
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""IfExists Condition Usage Check.
|
|
2
|
+
|
|
3
|
+
Validates proper usage of the IfExists suffix on IAM condition operators.
|
|
4
|
+
|
|
5
|
+
Detects:
|
|
6
|
+
- IfExists on security-sensitive keys in Allow statements (may bypass controls)
|
|
7
|
+
- Non-negated IfExists in Deny statements (weakens the Deny)
|
|
8
|
+
- IfExists on always-present keys (redundant)
|
|
9
|
+
- Suggests IfExists for negated operators in Deny without it
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
15
|
+
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
16
|
+
from iam_validator.core.condition_validators import (
|
|
17
|
+
ALWAYS_PRESENT_CONDITION_KEYS,
|
|
18
|
+
SECURITY_SENSITIVE_CONDITION_KEYS,
|
|
19
|
+
has_if_exists_suffix,
|
|
20
|
+
is_negated_operator,
|
|
21
|
+
normalize_operator,
|
|
22
|
+
)
|
|
23
|
+
from iam_validator.core.models import Statement, ValidationIssue
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class IfExistsConditionCheck(PolicyCheck):
|
|
27
|
+
"""Check for improper or risky usage of IfExists condition operators."""
|
|
28
|
+
|
|
29
|
+
check_id: ClassVar[str] = "ifexists_condition_usage"
|
|
30
|
+
description: ClassVar[str] = "Validates proper usage of IfExists suffix on condition operators"
|
|
31
|
+
default_severity: ClassVar[str] = "warning"
|
|
32
|
+
|
|
33
|
+
async def execute(
|
|
34
|
+
self,
|
|
35
|
+
statement: Statement,
|
|
36
|
+
statement_idx: int,
|
|
37
|
+
fetcher: AWSServiceFetcher,
|
|
38
|
+
config: CheckConfig,
|
|
39
|
+
) -> list[ValidationIssue]:
|
|
40
|
+
"""Execute IfExists condition usage checks."""
|
|
41
|
+
issues: list[ValidationIssue] = []
|
|
42
|
+
|
|
43
|
+
if not statement.condition:
|
|
44
|
+
return issues
|
|
45
|
+
|
|
46
|
+
statement_sid = statement.sid
|
|
47
|
+
line_number = statement.line_number
|
|
48
|
+
effect = statement.effect
|
|
49
|
+
|
|
50
|
+
# Config options
|
|
51
|
+
warn_security_sensitive_allow = config.config.get("warn_security_sensitive_allow", True)
|
|
52
|
+
suggest_deny_ifexists = config.config.get("suggest_deny_ifexists", False)
|
|
53
|
+
warn_always_present_keys = config.config.get("warn_always_present_keys", True)
|
|
54
|
+
additional_security_keys = config.config.get("additional_security_sensitive_keys", [])
|
|
55
|
+
|
|
56
|
+
# Build the full set of security-sensitive keys
|
|
57
|
+
security_keys = SECURITY_SENSITIVE_CONDITION_KEYS | frozenset(additional_security_keys)
|
|
58
|
+
|
|
59
|
+
# Collect Null-checked keys for suppression
|
|
60
|
+
null_checked_keys: set[str] = set()
|
|
61
|
+
for op, conds in statement.condition.items():
|
|
62
|
+
base_op, _, _ = normalize_operator(op)
|
|
63
|
+
if base_op == "Null":
|
|
64
|
+
for key, value in conds.items():
|
|
65
|
+
# Only suppress if Null checks for key presence (value = "false")
|
|
66
|
+
values = value if isinstance(value, list) else [value]
|
|
67
|
+
if any(str(v).lower() == "false" for v in values):
|
|
68
|
+
null_checked_keys.add(key)
|
|
69
|
+
|
|
70
|
+
for operator, conditions in statement.condition.items():
|
|
71
|
+
has_ifexists = has_if_exists_suffix(operator)
|
|
72
|
+
is_negated = is_negated_operator(operator)
|
|
73
|
+
base_op, _, _ = normalize_operator(operator)
|
|
74
|
+
|
|
75
|
+
for condition_key in conditions:
|
|
76
|
+
# Normalize condition key for case-insensitive comparison
|
|
77
|
+
key_lower = condition_key.lower()
|
|
78
|
+
|
|
79
|
+
# Skip MFA key - handled by mfa_condition_antipattern check
|
|
80
|
+
if key_lower == "aws:multifactorauthpresent":
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if has_ifexists:
|
|
84
|
+
# IfExists on always-present keys is redundant
|
|
85
|
+
if warn_always_present_keys:
|
|
86
|
+
always_present_match = any(
|
|
87
|
+
k.lower() == key_lower for k in ALWAYS_PRESENT_CONDITION_KEYS
|
|
88
|
+
)
|
|
89
|
+
if always_present_match:
|
|
90
|
+
# Reconstruct the operator without IfExists for the message
|
|
91
|
+
raw_base = operator
|
|
92
|
+
if ":" in operator:
|
|
93
|
+
parts = operator.split(":", 1)
|
|
94
|
+
if parts[0] in ("ForAllValues", "ForAnyValue"):
|
|
95
|
+
raw_base = parts[1]
|
|
96
|
+
base_without_ifexists = (
|
|
97
|
+
raw_base[:-8] if raw_base.endswith("IfExists") else raw_base
|
|
98
|
+
)
|
|
99
|
+
if ":" in operator:
|
|
100
|
+
parts = operator.split(":", 1)
|
|
101
|
+
if parts[0] in ("ForAllValues", "ForAnyValue"):
|
|
102
|
+
base_without_ifexists = f"{parts[0]}:{base_without_ifexists}"
|
|
103
|
+
|
|
104
|
+
issues.append(
|
|
105
|
+
ValidationIssue(
|
|
106
|
+
severity="info",
|
|
107
|
+
message=(
|
|
108
|
+
f"Redundant `IfExists`: The condition key "
|
|
109
|
+
f"`{condition_key}` is always present in the "
|
|
110
|
+
f"request context. Using `{operator}` has the "
|
|
111
|
+
f"same effect as `{base_without_ifexists}` for "
|
|
112
|
+
f"this key."
|
|
113
|
+
),
|
|
114
|
+
statement_sid=statement_sid,
|
|
115
|
+
statement_index=statement_idx,
|
|
116
|
+
issue_type="ifexists_on_always_present_key",
|
|
117
|
+
condition_key=condition_key,
|
|
118
|
+
line_number=line_number,
|
|
119
|
+
field_name="condition",
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Check if key is security-sensitive (case-insensitive)
|
|
124
|
+
is_security_key = any(k.lower() == key_lower for k in security_keys)
|
|
125
|
+
|
|
126
|
+
if effect == "Allow" and warn_security_sensitive_allow:
|
|
127
|
+
# IfExists on security-sensitive keys in Allow may bypass controls
|
|
128
|
+
if is_security_key:
|
|
129
|
+
# Check for complementary Null check
|
|
130
|
+
if condition_key not in null_checked_keys:
|
|
131
|
+
issues.append(
|
|
132
|
+
ValidationIssue(
|
|
133
|
+
severity=self.get_severity(config),
|
|
134
|
+
message=(
|
|
135
|
+
f"Security control may be bypassed: "
|
|
136
|
+
f"`{operator}` with `{condition_key}` in "
|
|
137
|
+
f"an Allow statement means the restriction "
|
|
138
|
+
f"is not enforced when the key is missing "
|
|
139
|
+
f"from the request context. Not all API "
|
|
140
|
+
f"calls include `{condition_key}` (e.g., "
|
|
141
|
+
f"calls made by AWS services on your "
|
|
142
|
+
f"behalf). Consider using the operator "
|
|
143
|
+
f"without `IfExists` or adding a `Null` "
|
|
144
|
+
f"condition check."
|
|
145
|
+
),
|
|
146
|
+
statement_sid=statement_sid,
|
|
147
|
+
statement_index=statement_idx,
|
|
148
|
+
issue_type="ifexists_weakens_security_condition",
|
|
149
|
+
condition_key=condition_key,
|
|
150
|
+
line_number=line_number,
|
|
151
|
+
field_name="condition",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
elif effect == "Deny":
|
|
156
|
+
# Non-negated IfExists in Deny weakens the restriction
|
|
157
|
+
if not is_negated and is_security_key:
|
|
158
|
+
if condition_key not in null_checked_keys:
|
|
159
|
+
issues.append(
|
|
160
|
+
ValidationIssue(
|
|
161
|
+
severity=self.get_severity(config),
|
|
162
|
+
message=(
|
|
163
|
+
f"Weakened Deny: `{operator}` with "
|
|
164
|
+
f"`{condition_key}` in a Deny statement "
|
|
165
|
+
f"means the Deny does not apply when "
|
|
166
|
+
f"`{condition_key}` is missing from the "
|
|
167
|
+
f"request context. Consider removing "
|
|
168
|
+
f"`IfExists` or adding a `Null` condition "
|
|
169
|
+
f"check."
|
|
170
|
+
),
|
|
171
|
+
statement_sid=statement_sid,
|
|
172
|
+
statement_index=statement_idx,
|
|
173
|
+
issue_type="ifexists_weakens_deny",
|
|
174
|
+
condition_key=condition_key,
|
|
175
|
+
line_number=line_number,
|
|
176
|
+
field_name="condition",
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
elif not has_ifexists and effect == "Deny" and suggest_deny_ifexists:
|
|
181
|
+
# Suggest IfExists for negated operator in Deny without it
|
|
182
|
+
if is_negated:
|
|
183
|
+
# Only suggest for keys that may be absent
|
|
184
|
+
is_always_present = any(
|
|
185
|
+
k.lower() == key_lower for k in ALWAYS_PRESENT_CONDITION_KEYS
|
|
186
|
+
)
|
|
187
|
+
is_security_key = any(k.lower() == key_lower for k in security_keys)
|
|
188
|
+
if not is_always_present and is_security_key:
|
|
189
|
+
issues.append(
|
|
190
|
+
ValidationIssue(
|
|
191
|
+
severity="info",
|
|
192
|
+
message=(
|
|
193
|
+
f"Consider using `{base_op}IfExists` instead "
|
|
194
|
+
f"of `{base_op}` in this Deny statement. "
|
|
195
|
+
f"Without `IfExists`, the Deny does not apply "
|
|
196
|
+
f"when `{condition_key}` is missing from the "
|
|
197
|
+
f"request context. With `{base_op}IfExists`, "
|
|
198
|
+
f"the Deny still applies even when the key is "
|
|
199
|
+
f"absent."
|
|
200
|
+
),
|
|
201
|
+
statement_sid=statement_sid,
|
|
202
|
+
statement_index=statement_idx,
|
|
203
|
+
issue_type="ifexists_deny_suggestion",
|
|
204
|
+
condition_key=condition_key,
|
|
205
|
+
line_number=line_number,
|
|
206
|
+
field_name="condition",
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return issues
|
|
@@ -11,6 +11,7 @@ from typing import ClassVar
|
|
|
11
11
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
12
12
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
13
13
|
from iam_validator.core.condition_validators import (
|
|
14
|
+
has_if_exists_suffix,
|
|
14
15
|
is_multivalued_context_key,
|
|
15
16
|
normalize_operator,
|
|
16
17
|
)
|
|
@@ -81,6 +82,7 @@ class SetOperatorValidationCheck(PolicyCheck):
|
|
|
81
82
|
# Second pass: Validate set operator usage
|
|
82
83
|
for operator, conditions in statement.condition.items():
|
|
83
84
|
base_operator, _operator_type, set_prefix = normalize_operator(operator)
|
|
85
|
+
has_ifexists = has_if_exists_suffix(operator)
|
|
84
86
|
|
|
85
87
|
if not set_prefix:
|
|
86
88
|
continue
|
|
@@ -110,15 +112,27 @@ class SetOperatorValidationCheck(PolicyCheck):
|
|
|
110
112
|
# Issue 2: ForAllValues with Allow effect without Null check (security risk)
|
|
111
113
|
if set_prefix == "ForAllValues" and effect == "Allow":
|
|
112
114
|
if condition_key not in null_checked_keys:
|
|
115
|
+
if has_ifexists:
|
|
116
|
+
message = (
|
|
117
|
+
f"Compounded security risk: `{operator}` with `Allow` effect "
|
|
118
|
+
f"on `{condition_key}` is doubly permissive. `ForAllValues` "
|
|
119
|
+
f"passes when the value set is empty, and `IfExists` passes "
|
|
120
|
+
f"when the key is missing entirely. Both mechanisms "
|
|
121
|
+
f"independently allow access without matching any values. "
|
|
122
|
+
f'Add: `"Null": {{"{condition_key}": "false"}}` and consider '
|
|
123
|
+
f"removing `IfExists`."
|
|
124
|
+
)
|
|
125
|
+
else:
|
|
126
|
+
message = (
|
|
127
|
+
f"Security risk: `ForAllValues` with `Allow` effect on `{condition_key}` "
|
|
128
|
+
f"should include a `Null` condition check. Without it, requests with missing "
|
|
129
|
+
f'`{condition_key}` will be granted access. Add: `"Null": {{"{condition_key}": "false"}}`. '
|
|
130
|
+
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
131
|
+
)
|
|
113
132
|
issues.append(
|
|
114
133
|
ValidationIssue(
|
|
115
134
|
severity="warning",
|
|
116
|
-
message=
|
|
117
|
-
f"Security risk: `ForAllValues` with `Allow` effect on `{condition_key}` "
|
|
118
|
-
f"should include a `Null` condition check. Without it, requests with missing "
|
|
119
|
-
f'`{condition_key}` will be granted access. Add: `"Null": {{"{condition_key}": "false"}}`. '
|
|
120
|
-
f"See: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-single-vs-multi-valued-context-keys.html"
|
|
121
|
-
),
|
|
135
|
+
message=message,
|
|
122
136
|
statement_sid=statement_sid,
|
|
123
137
|
statement_index=statement_idx,
|
|
124
138
|
issue_type="forallvalues_allow_without_null_check",
|
|
@@ -706,6 +706,7 @@ def create_default_registry(
|
|
|
706
706
|
# 3. TYPE VALIDATION (Condition operator type checking)
|
|
707
707
|
registry.register(checks.ConditionTypeMismatchCheck()) # Operator-value type compatibility
|
|
708
708
|
registry.register(checks.SetOperatorValidationCheck()) # ForAllValues/ForAnyValue usage
|
|
709
|
+
registry.register(checks.IfExistsConditionCheck()) # IfExists usage validation
|
|
709
710
|
|
|
710
711
|
# 4. RESOURCE MATCHING (Action-resource relationship validation)
|
|
711
712
|
registry.register(
|
|
@@ -3,7 +3,8 @@ Condition Key and Value Validators.
|
|
|
3
3
|
|
|
4
4
|
This module provides validators for IAM policy condition operators, keys, and values.
|
|
5
5
|
Based on AWS IAM Policy Elements Reference:
|
|
6
|
-
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
6
|
+
- https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition_operators.html
|
|
7
|
+
- https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
|
|
7
8
|
|
|
8
9
|
Supports:
|
|
9
10
|
- All standard IAM condition operators (String, Numeric, Date, Bool, Binary, IP, ARN)
|
|
@@ -69,6 +70,49 @@ CONDITION_OPERATORS = {
|
|
|
69
70
|
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_multi-value-conditions.html
|
|
70
71
|
SET_OPERATOR_PREFIXES = ["ForAllValues", "ForAnyValue"]
|
|
71
72
|
|
|
73
|
+
# Condition keys that are sometimes absent from the request context AND are
|
|
74
|
+
# security-sensitive. Using IfExists with these keys in Allow statements can
|
|
75
|
+
# bypass security controls when the key is missing.
|
|
76
|
+
# Note: aws:MultiFactorAuthPresent is excluded — handled by mfa_condition_antipattern check.
|
|
77
|
+
# Note: Always-present keys are in ALWAYS_PRESENT_CONDITION_KEYS.
|
|
78
|
+
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
|
|
79
|
+
SECURITY_SENSITIVE_CONDITION_KEYS = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"aws:SourceIp", # Absent when requester uses VPC endpoint
|
|
82
|
+
"aws:VpcSourceIp", # Related to VPC context
|
|
83
|
+
"aws:SourceVpc", # Only present with VPC endpoint
|
|
84
|
+
"aws:SourceVpce", # Only present with VPC endpoint
|
|
85
|
+
"aws:SourceArn", # Only for direct service principal calls
|
|
86
|
+
"aws:SourceAccount", # Only for direct service principal calls
|
|
87
|
+
"aws:SourceOrgID", # Only for service principal calls + org membership
|
|
88
|
+
"aws:SourceOrgPaths", # Only for service principal calls + org membership
|
|
89
|
+
"aws:PrincipalOrgID", # Only if principal is a member of an organization
|
|
90
|
+
"aws:PrincipalOrgPaths", # Only if principal is a member of an organization
|
|
91
|
+
"aws:PrincipalArn", # All signed requests only; absent for anonymous requests
|
|
92
|
+
"aws:ResourceAccount", # Always included for most actions (has documented exceptions)
|
|
93
|
+
"aws:ResourceOrgID", # Only if resource account is in an organization
|
|
94
|
+
"aws:ResourceOrgPaths", # Only if resource account is in an organization
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Condition keys that are always present in every AWS request context.
|
|
99
|
+
# Using IfExists with these keys is redundant since the key is never absent.
|
|
100
|
+
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html
|
|
101
|
+
ALWAYS_PRESENT_CONDITION_KEYS = frozenset(
|
|
102
|
+
{
|
|
103
|
+
# "always included in the request context" per AWS docs
|
|
104
|
+
"aws:CurrentTime",
|
|
105
|
+
"aws:EpochTime",
|
|
106
|
+
"aws:SecureTransport",
|
|
107
|
+
"aws:ViaAWSService",
|
|
108
|
+
"aws:RequestedRegion",
|
|
109
|
+
# "included in the request context for all requests, including anonymous requests"
|
|
110
|
+
"aws:PrincipalAccount",
|
|
111
|
+
"aws:PrincipalType",
|
|
112
|
+
"aws:userid",
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
|
|
72
116
|
|
|
73
117
|
def normalize_operator(operator: str) -> tuple[str, str | None, str | None]:
|
|
74
118
|
"""
|
|
@@ -122,6 +166,37 @@ def normalize_operator(operator: str) -> tuple[str, str | None, str | None]:
|
|
|
122
166
|
return operator, None, set_prefix
|
|
123
167
|
|
|
124
168
|
|
|
169
|
+
def has_if_exists_suffix(operator: str) -> bool:
|
|
170
|
+
"""
|
|
171
|
+
Check if a condition operator has the IfExists suffix.
|
|
172
|
+
|
|
173
|
+
Handles set operator prefixes (ForAllValues/ForAnyValue).
|
|
174
|
+
Case-sensitive for the IfExists suffix, matching AWS IAM behavior.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
operator: Raw operator string (e.g., "StringEqualsIfExists", "ForAllValues:StringLikeIfExists")
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
True if the operator has the IfExists suffix
|
|
181
|
+
|
|
182
|
+
Examples:
|
|
183
|
+
>>> has_if_exists_suffix("StringEqualsIfExists")
|
|
184
|
+
True
|
|
185
|
+
>>> has_if_exists_suffix("StringEquals")
|
|
186
|
+
False
|
|
187
|
+
>>> has_if_exists_suffix("ForAllValues:StringLikeIfExists")
|
|
188
|
+
True
|
|
189
|
+
>>> has_if_exists_suffix("BoolIfExists")
|
|
190
|
+
True
|
|
191
|
+
"""
|
|
192
|
+
cleaned = operator
|
|
193
|
+
if ":" in operator:
|
|
194
|
+
parts = operator.split(":", 1)
|
|
195
|
+
if parts[0] in SET_OPERATOR_PREFIXES:
|
|
196
|
+
cleaned = parts[1]
|
|
197
|
+
return cleaned.endswith("IfExists")
|
|
198
|
+
|
|
199
|
+
|
|
125
200
|
def translate_type(doc_type: str) -> str:
|
|
126
201
|
"""
|
|
127
202
|
Translate documentation type names to normalized types.
|
|
@@ -264,8 +339,7 @@ def _validate_date_value(value_str: str) -> tuple[bool, str | None]:
|
|
|
264
339
|
if epoch > 32503680000: # Year 3000 in seconds
|
|
265
340
|
return (
|
|
266
341
|
False,
|
|
267
|
-
f"UNIX epoch timestamp appears unreasonably large: {value_str}. "
|
|
268
|
-
"Expected seconds since 1970-01-01.",
|
|
342
|
+
f"UNIX epoch timestamp appears unreasonably large: {value_str}. Expected seconds since 1970-01-01.",
|
|
269
343
|
)
|
|
270
344
|
return True, None
|
|
271
345
|
except (ValueError, OverflowError):
|
|
File without changes
|
{iam_policy_validator-1.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.15.6.dist-info → iam_policy_validator-1.16.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|