iam-policy-validator 1.12.0__py3-none-any.whl → 1.13.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.12.0.dist-info → iam_policy_validator-1.13.0.dist-info}/METADATA +1 -1
- {iam_policy_validator-1.12.0.dist-info → iam_policy_validator-1.13.0.dist-info}/RECORD +11 -11
- {iam_policy_validator-1.12.0.dist-info → iam_policy_validator-1.13.0.dist-info}/WHEEL +1 -1
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +320 -203
- iam_validator/checks/sensitive_action.py +83 -3
- iam_validator/checks/utils/policy_level_checks.py +12 -2
- iam_validator/core/config/defaults.py +64 -2
- iam_validator/core/report.py +11 -0
- {iam_policy_validator-1.12.0.dist-info → iam_policy_validator-1.13.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.12.0.dist-info → iam_policy_validator-1.13.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.13.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://github.com/boogy/iam-policy-validator/tree/main/docs
|
|
@@ -1,8 +1,8 @@
|
|
|
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=
|
|
3
|
+
iam_validator/__version__.py,sha256=Gh6xEIE-FdIaY_5D-vLQn-lzIOGKaeGM8vbVmUci1V4,374
|
|
4
4
|
iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
|
|
5
|
-
iam_validator/checks/action_condition_enforcement.py,sha256=
|
|
5
|
+
iam_validator/checks/action_condition_enforcement.py,sha256=aXCB9hZ7OxucUyJjgW6Y6X1dWPeUAFJIBjz6G_lTWsI,60380
|
|
6
6
|
iam_validator/checks/action_resource_matching.py,sha256=WiGJmCIJfx5yituMjZxpKmk-99N6nK20ueN02ddy9oM,19296
|
|
7
7
|
iam_validator/checks/action_validation.py,sha256=QXfNamcstQIO41zNed1-bCmXYkXdV77owu8G2cZ09-A,2517
|
|
8
8
|
iam_validator/checks/condition_key_validation.py,sha256=QJjG82wxvjdG2m-YuEzAjKRRiWaaPkf_LChdUTvm9g4,3919
|
|
@@ -14,7 +14,7 @@ iam_validator/checks/policy_structure.py,sha256=9eR8EEcERKcc5n7D3_LmFIQyDNzVV5Me
|
|
|
14
14
|
iam_validator/checks/policy_type_validation.py,sha256=z4RiAvmPhtrf6Gj3z1Ln4dDFWnFclsokVL7x-YhkMiM,15986
|
|
15
15
|
iam_validator/checks/principal_validation.py,sha256=jusBVEA-sHHft3Kfq_YdvPUgX3cBnxKqC1zhth74kCU,27691
|
|
16
16
|
iam_validator/checks/resource_validation.py,sha256=G_Pfh3WZ6-C3KTk3XPpUKhOESwIO5ISgbsUXc-aK1SE,5988
|
|
17
|
-
iam_validator/checks/sensitive_action.py,sha256=
|
|
17
|
+
iam_validator/checks/sensitive_action.py,sha256=vLGgFMDpW_CmKnKiQcR5pyAZRaDQtqeAAgS3D4CSPDU,18911
|
|
18
18
|
iam_validator/checks/service_wildcard.py,sha256=ycggiozWm1Z4lkWsDlooMEvRJflzLxZkihQDPZ9G_zw,3949
|
|
19
19
|
iam_validator/checks/set_operator_validation.py,sha256=FyxZ7qWlp9-ABzZaRRkxRP_Hws7Re7qZgeQCCM9sJAM,7258
|
|
20
20
|
iam_validator/checks/sid_uniqueness.py,sha256=vfpk88b9G9OApxtrotABI2mPXvGd_C_X4gJKeqIURlk,5968
|
|
@@ -22,7 +22,7 @@ iam_validator/checks/trust_policy_validation.py,sha256=a8Sm2xu3gFOHLd7rXDl-ibqiL
|
|
|
22
22
|
iam_validator/checks/wildcard_action.py,sha256=CyURgURDt2fQT2468LK813RupQ3WWvpmvLVLjUZf9QQ,1960
|
|
23
23
|
iam_validator/checks/wildcard_resource.py,sha256=lRNZN7f3ZQrvnbGdVDCefUQF8lESIMoXVfhIgpln3mM,6679
|
|
24
24
|
iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
|
|
25
|
-
iam_validator/checks/utils/policy_level_checks.py,sha256=
|
|
25
|
+
iam_validator/checks/utils/policy_level_checks.py,sha256=pr-uLo-otB612YLZ-rd8W5Kl9ENaTHuzTNOUhQaULKc,7593
|
|
26
26
|
iam_validator/checks/utils/sensitive_action_matcher.py,sha256=qDXcJa_2sCJu9pBbjDlI7x5lPtLRc6jQCpKPMheCOJQ,11215
|
|
27
27
|
iam_validator/checks/utils/wildcard_expansion.py,sha256=3W13hlyWcP2wJ6w-BwM887VOnRzglK6Bk3eHMjUtOco,3131
|
|
28
28
|
iam_validator/commands/__init__.py,sha256=RBEz-Kgt3aRVn_9B1HRy_XgQMIKzlSSQs4Gtg2jQEv8,729
|
|
@@ -49,7 +49,7 @@ iam_validator/core/models.py,sha256=FhQ7fpX6T9AOvHsAPlBZL0NmPuPE_xghD3dp4cAGLZw,
|
|
|
49
49
|
iam_validator/core/policy_checks.py,sha256=FNVuS2GTffwCjjrlupVIazC172gSxKYAAT_ObV6Apbo,8803
|
|
50
50
|
iam_validator/core/policy_loader.py,sha256=2KJnXzGg3g9pDXWZHk3DO0xpZnZZ-wXWFEOdQ_naJ8s,17862
|
|
51
51
|
iam_validator/core/pr_commenter.py,sha256=vDN9meq861nVpno-GyhGX4wnBE1Z7clCIhv6pq9rZGs,22755
|
|
52
|
-
iam_validator/core/report.py,sha256=
|
|
52
|
+
iam_validator/core/report.py,sha256=mi4zjlWHRakO2cTek5NCpiP92kUfJe_j5tAhB5yS02Q,36528
|
|
53
53
|
iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
|
|
54
54
|
iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
|
|
55
55
|
iam_validator/core/aws_service/client.py,sha256=Zv7rIpEFdUCDXKGp3migPDkj8L5eZltgrGe64M2t2Ko,7336
|
|
@@ -64,7 +64,7 @@ iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rn
|
|
|
64
64
|
iam_validator/core/config/category_suggestions.py,sha256=fopaZ9kXDrsLgi_r0pERrLwgdPPJl5VIiKvXtQK9tj0,8583
|
|
65
65
|
iam_validator/core/config/condition_requirements.py,sha256=1CeQJfWV-Y2ImW0Mq9YdrgvH-hj9IXe0gVOm3B36Rc8,10655
|
|
66
66
|
iam_validator/core/config/config_loader.py,sha256=qKD8aR8YAswaFf68pnYJLFNwKznvcc6lNxSQWU3i6SY,17713
|
|
67
|
-
iam_validator/core/config/defaults.py,sha256=
|
|
67
|
+
iam_validator/core/config/defaults.py,sha256=AdbqyUlsw4YViOWXFBENN3scoVx98hF6_dUKmfyZHNU,37057
|
|
68
68
|
iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
|
|
69
69
|
iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
|
|
70
70
|
iam_validator/core/config/service_principals.py,sha256=8pys5H_yycVJ9KTyimAKFYBg83Aol2Iri53wiHjtnEM,3959
|
|
@@ -93,8 +93,8 @@ iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYu
|
|
|
93
93
|
iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
|
|
94
94
|
iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
|
|
95
95
|
iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
|
|
96
|
-
iam_policy_validator-1.
|
|
97
|
-
iam_policy_validator-1.
|
|
98
|
-
iam_policy_validator-1.
|
|
99
|
-
iam_policy_validator-1.
|
|
100
|
-
iam_policy_validator-1.
|
|
96
|
+
iam_policy_validator-1.13.0.dist-info/METADATA,sha256=DHdEaa1wjR4XRrkk0F_oTFI1v9AbeeQsheg2QS3ojKw,34456
|
|
97
|
+
iam_policy_validator-1.13.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
98
|
+
iam_policy_validator-1.13.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
|
|
99
|
+
iam_policy_validator-1.13.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
100
|
+
iam_policy_validator-1.13.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.13.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("."))
|
|
@@ -1,146 +1,21 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- none_of: Flags statements that contain forbidden actions
|
|
20
|
-
Example: actions: {none_of: ["iam:DeleteUser", "s3:DeleteBucket"]}
|
|
21
|
-
|
|
22
|
-
Common use cases:
|
|
23
|
-
- iam:PassRole must have iam:PassedToService condition
|
|
24
|
-
- Sensitive actions must have MFA conditions
|
|
25
|
-
- Actions must have source IP restrictions
|
|
26
|
-
- Resources must have required tags
|
|
27
|
-
- Combine multiple conditions (MFA + IP + Tags)
|
|
28
|
-
- Detect overly permissive statements (all_of)
|
|
29
|
-
- Ensure privilege escalation combinations are protected
|
|
30
|
-
|
|
31
|
-
Configuration in iam-validator.yaml:
|
|
32
|
-
|
|
33
|
-
checks:
|
|
34
|
-
action_condition_enforcement:
|
|
35
|
-
enabled: true
|
|
36
|
-
severity: high
|
|
37
|
-
description: "Enforce specific conditions for specific actions"
|
|
38
|
-
|
|
39
|
-
action_condition_requirements:
|
|
40
|
-
# BASIC: Simple action with required condition
|
|
41
|
-
- actions:
|
|
42
|
-
- "iam:PassRole"
|
|
43
|
-
required_conditions:
|
|
44
|
-
- condition_key: "iam:PassedToService"
|
|
45
|
-
description: "Specify which AWS services can use the passed role"
|
|
46
|
-
|
|
47
|
-
# MFA + IP restrictions
|
|
48
|
-
- actions:
|
|
49
|
-
- "iam:DeleteUser"
|
|
50
|
-
required_conditions:
|
|
51
|
-
all_of:
|
|
52
|
-
- condition_key: "aws:MultiFactorAuthPresent"
|
|
53
|
-
expected_value: true
|
|
54
|
-
- condition_key: "aws:SourceIp"
|
|
55
|
-
|
|
56
|
-
# EC2 with TAGS + MFA + Region
|
|
57
|
-
- actions:
|
|
58
|
-
- "ec2:RunInstances"
|
|
59
|
-
required_conditions:
|
|
60
|
-
all_of:
|
|
61
|
-
- condition_key: "aws:MultiFactorAuthPresent"
|
|
62
|
-
expected_value: true
|
|
63
|
-
- condition_key: "aws:RequestTag/Environment"
|
|
64
|
-
operator: "StringEquals"
|
|
65
|
-
expected_value: ["Production", "Staging", "Development"]
|
|
66
|
-
- condition_key: "aws:RequestTag/Owner"
|
|
67
|
-
- condition_key: "aws:RequestedRegion"
|
|
68
|
-
expected_value: ["us-east-1", "us-west-2"]
|
|
69
|
-
|
|
70
|
-
# Principal-to-resource tag matching
|
|
71
|
-
- actions:
|
|
72
|
-
- "ec2:RunInstances"
|
|
73
|
-
required_conditions:
|
|
74
|
-
- condition_key: "aws:ResourceTag/owner"
|
|
75
|
-
operator: "StringEquals"
|
|
76
|
-
expected_value: "${aws:PrincipalTag/owner}"
|
|
77
|
-
description: "Resource owner must match principal's owner tag"
|
|
78
|
-
|
|
79
|
-
# Complex: all_of + any_of for actions and conditions
|
|
80
|
-
- actions:
|
|
81
|
-
any_of:
|
|
82
|
-
- "cloudformation:CreateStack"
|
|
83
|
-
- "cloudformation:UpdateStack"
|
|
84
|
-
required_conditions:
|
|
85
|
-
all_of:
|
|
86
|
-
- condition_key: "aws:MultiFactorAuthPresent"
|
|
87
|
-
expected_value: true
|
|
88
|
-
- condition_key: "aws:RequestTag/Environment"
|
|
89
|
-
any_of:
|
|
90
|
-
- condition_key: "aws:SourceIp"
|
|
91
|
-
- condition_key: "aws:SourceVpce"
|
|
92
|
-
|
|
93
|
-
# none_of for conditions: Ensure certain conditions are NOT present
|
|
94
|
-
- actions:
|
|
95
|
-
- "s3:GetObject"
|
|
96
|
-
required_conditions:
|
|
97
|
-
none_of:
|
|
98
|
-
- condition_key: "aws:SecureTransport"
|
|
99
|
-
expected_value: false
|
|
100
|
-
description: "Ensure insecure transport is never allowed"
|
|
101
|
-
|
|
102
|
-
# any_of for actions: If ANY statement grants privilege escalation actions, require MFA
|
|
103
|
-
- actions:
|
|
104
|
-
any_of:
|
|
105
|
-
- "iam:CreateUser"
|
|
106
|
-
- "iam:AttachUserPolicy"
|
|
107
|
-
- "iam:PutUserPolicy"
|
|
108
|
-
required_conditions:
|
|
109
|
-
- condition_key: "aws:MultiFactorAuthPresent"
|
|
110
|
-
expected_value: true
|
|
111
|
-
description: "Privilege escalation actions require MFA"
|
|
112
|
-
severity: "critical"
|
|
113
|
-
|
|
114
|
-
# all_of for actions: Flag statements that contain BOTH dangerous actions (overly permissive)
|
|
115
|
-
- actions:
|
|
116
|
-
all_of:
|
|
117
|
-
- "iam:CreateAccessKey"
|
|
118
|
-
- "iam:UpdateAccessKey"
|
|
119
|
-
severity: "critical"
|
|
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"
|
|
128
|
-
|
|
129
|
-
# Per-requirement ignore_patterns: Skip specific requirements for certain files/actions
|
|
130
|
-
- actions:
|
|
131
|
-
- "iam:CreateRole"
|
|
132
|
-
- "iam:PutRolePolicy"
|
|
133
|
-
- "iam:AttachRolePolicy"
|
|
134
|
-
required_conditions:
|
|
135
|
-
- condition_key: "iam:PermissionsBoundary"
|
|
136
|
-
description: "Require permissions boundary for IAM operations"
|
|
137
|
-
ignore_patterns:
|
|
138
|
-
# Ignore this requirement for iam-openid modules (they enforce boundary by default)
|
|
139
|
-
- filepath_regex: ".*modules//?iam-openid.*"
|
|
140
|
-
|
|
141
|
-
Note: ignore_patterns can be specified at TWO levels:
|
|
142
|
-
1. Check-level (applies to ALL requirements): Useful for broad exclusions
|
|
143
|
-
2. Requirement-level (applies to ONE requirement): Useful for fine-grained control
|
|
1
|
+
"""Action-Specific Condition Enforcement Check.
|
|
2
|
+
|
|
3
|
+
Ensures specific actions have required IAM conditions (MFA, IP, tags, etc.).
|
|
4
|
+
|
|
5
|
+
Action Matching Modes:
|
|
6
|
+
- Simple list: actions: ["iam:PassRole"]
|
|
7
|
+
- any_of: Require conditions if ANY action matches
|
|
8
|
+
- all_of: Require conditions if ALL actions present (overly permissive detection)
|
|
9
|
+
- none_of: Flag forbidden actions
|
|
10
|
+
|
|
11
|
+
Merge Strategies (merge_strategy setting):
|
|
12
|
+
- append (default): User + defaults both apply
|
|
13
|
+
- user_only: Disable ALL defaults, use only user requirements
|
|
14
|
+
- per_action_override: User replaces defaults for matching actions
|
|
15
|
+
- replace_all: User replaces all if provided
|
|
16
|
+
- defaults_only: Ignore user, use only defaults
|
|
17
|
+
|
|
18
|
+
For full documentation, see: docs/condition-requirements.md
|
|
144
19
|
"""
|
|
145
20
|
|
|
146
21
|
import re
|
|
@@ -201,18 +76,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
201
76
|
del kwargs # Not used in current implementation
|
|
202
77
|
issues = []
|
|
203
78
|
|
|
204
|
-
# Get action condition requirements
|
|
205
|
-
|
|
206
|
-
# - "requirements" (current/preferred)
|
|
207
|
-
# - "action_condition_requirements" (legacy)
|
|
208
|
-
# - "policy_level_requirements" (legacy)
|
|
209
|
-
requirements = config.config.get(
|
|
210
|
-
"requirements",
|
|
211
|
-
config.config.get(
|
|
212
|
-
"action_condition_requirements",
|
|
213
|
-
config.config.get("policy_level_requirements", []),
|
|
214
|
-
),
|
|
215
|
-
)
|
|
79
|
+
# Get action condition requirements using configurable merge strategy
|
|
80
|
+
requirements = self._get_merged_requirements(config, policy_file)
|
|
216
81
|
|
|
217
82
|
if not requirements:
|
|
218
83
|
return issues
|
|
@@ -246,6 +111,207 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
246
111
|
|
|
247
112
|
return issues
|
|
248
113
|
|
|
114
|
+
def _get_merged_requirements(
|
|
115
|
+
self,
|
|
116
|
+
config: CheckConfig,
|
|
117
|
+
policy_file: str,
|
|
118
|
+
) -> list[dict[str, Any]]:
|
|
119
|
+
"""
|
|
120
|
+
Get merged requirements based on configured merge strategy.
|
|
121
|
+
|
|
122
|
+
Supports multiple merge strategies to control how user requirements
|
|
123
|
+
interact with default requirements:
|
|
124
|
+
- "per_action_override": User requirements replace defaults for matching actions (default)
|
|
125
|
+
- "append": User requirements added to defaults (both apply)
|
|
126
|
+
- "replace_all": User requirements completely replace ALL defaults
|
|
127
|
+
- "defaults_only": Ignore user requirements, use only defaults
|
|
128
|
+
- "user_only": Ignore defaults, use only user requirements
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
config: Check configuration containing requirements and merge strategy
|
|
132
|
+
policy_file: Path to the policy file being checked (for ignore_patterns)
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Merged list of requirements based on strategy
|
|
136
|
+
"""
|
|
137
|
+
# Get default and user requirements
|
|
138
|
+
default_requirements = config.config.get(
|
|
139
|
+
"requirements",
|
|
140
|
+
config.config.get("policy_level_requirements", []),
|
|
141
|
+
)
|
|
142
|
+
user_requirements = config.config.get("action_condition_requirements")
|
|
143
|
+
|
|
144
|
+
# Get merge strategy (default: append - both defaults and user requirements apply)
|
|
145
|
+
merge_strategy = config.config.get("merge_strategy", "append")
|
|
146
|
+
|
|
147
|
+
# For user_only, replace_all, and per_action_override:
|
|
148
|
+
# Filter user requirements by ignore_patterns BEFORE merging
|
|
149
|
+
# For append and defaults_only: ignore_patterns on user requirements still apply
|
|
150
|
+
if user_requirements:
|
|
151
|
+
active_user_requirements = self._filter_requirements_by_filepath(
|
|
152
|
+
user_requirements, policy_file
|
|
153
|
+
)
|
|
154
|
+
else:
|
|
155
|
+
active_user_requirements = []
|
|
156
|
+
|
|
157
|
+
# Apply merge strategy
|
|
158
|
+
if merge_strategy == "user_only":
|
|
159
|
+
# Use ONLY user requirements - no defaults at all
|
|
160
|
+
# If a user requirement is filtered by ignore_patterns, it's simply not checked
|
|
161
|
+
return active_user_requirements
|
|
162
|
+
|
|
163
|
+
elif merge_strategy == "defaults_only":
|
|
164
|
+
# Use ONLY defaults - ignore all user requirements
|
|
165
|
+
return default_requirements
|
|
166
|
+
|
|
167
|
+
elif merge_strategy == "replace_all":
|
|
168
|
+
# User requirements completely replace ALL defaults (if user provided any)
|
|
169
|
+
# If no user requirements provided, fall back to defaults
|
|
170
|
+
if user_requirements: # Check original, not filtered
|
|
171
|
+
return active_user_requirements
|
|
172
|
+
return default_requirements
|
|
173
|
+
|
|
174
|
+
elif merge_strategy == "per_action_override":
|
|
175
|
+
# User requirements replace defaults for MATCHING actions only
|
|
176
|
+
# Non-matching defaults are kept
|
|
177
|
+
# Note: We use the ORIGINAL user_requirements to determine which actions
|
|
178
|
+
# are "user-defined" (even if filtered out by ignore_patterns)
|
|
179
|
+
return self._merge_per_action_override(
|
|
180
|
+
default_requirements, user_requirements or [], active_user_requirements
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
else: # "append" (default)
|
|
184
|
+
# Both defaults AND user requirements apply
|
|
185
|
+
# User requirements are added on top of defaults
|
|
186
|
+
return default_requirements + active_user_requirements
|
|
187
|
+
|
|
188
|
+
def _filter_requirements_by_filepath(
|
|
189
|
+
self,
|
|
190
|
+
requirements: list[dict[str, Any]],
|
|
191
|
+
policy_file: str,
|
|
192
|
+
) -> list[dict[str, Any]]:
|
|
193
|
+
"""
|
|
194
|
+
Filter out requirements that should be ignored for this file.
|
|
195
|
+
|
|
196
|
+
This handles ignore_patterns at the requirement level BEFORE merging,
|
|
197
|
+
allowing defaults to apply when user requirements are ignored.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
requirements: List of requirements to filter
|
|
201
|
+
policy_file: Path to the policy file being checked
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
Filtered list of requirements (excluding ignored ones)
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
User defines: iam:CreateRole with ignore_patterns: [".*test/.*"]
|
|
208
|
+
When checking test/policy.json:
|
|
209
|
+
- User requirement is filtered out
|
|
210
|
+
- Default iam:CreateRole requirement can apply instead
|
|
211
|
+
"""
|
|
212
|
+
active_reqs = []
|
|
213
|
+
|
|
214
|
+
for req in requirements:
|
|
215
|
+
ignore_patterns = req.get("ignore_patterns", [])
|
|
216
|
+
|
|
217
|
+
if not ignore_patterns:
|
|
218
|
+
# No ignore patterns - include this requirement
|
|
219
|
+
active_reqs.append(req)
|
|
220
|
+
continue
|
|
221
|
+
|
|
222
|
+
# Check if any ignore pattern matches this file
|
|
223
|
+
should_ignore = self._should_ignore_filepath(policy_file, ignore_patterns)
|
|
224
|
+
|
|
225
|
+
if not should_ignore:
|
|
226
|
+
active_reqs.append(req)
|
|
227
|
+
|
|
228
|
+
return active_reqs
|
|
229
|
+
|
|
230
|
+
def _should_ignore_filepath(
|
|
231
|
+
self,
|
|
232
|
+
filepath: str,
|
|
233
|
+
ignore_patterns: list[dict[str, Any]],
|
|
234
|
+
) -> bool:
|
|
235
|
+
"""
|
|
236
|
+
Check if filepath matches any of the ignore patterns.
|
|
237
|
+
|
|
238
|
+
Only checks filepath-based patterns (filepath, filepath_regex).
|
|
239
|
+
This is used for filtering requirements before merging.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
filepath: Path to the policy file
|
|
243
|
+
ignore_patterns: List of ignore pattern dictionaries
|
|
244
|
+
|
|
245
|
+
Returns:
|
|
246
|
+
True if filepath matches any pattern
|
|
247
|
+
"""
|
|
248
|
+
for pattern in ignore_patterns:
|
|
249
|
+
# Only check filepath-based patterns
|
|
250
|
+
if "filepath" in pattern or "filepath_regex" in pattern:
|
|
251
|
+
regex_pattern = pattern.get("filepath") or pattern.get("filepath_regex")
|
|
252
|
+
if regex_pattern:
|
|
253
|
+
compiled = compile_and_cache(regex_pattern)
|
|
254
|
+
if compiled and compiled.search(filepath):
|
|
255
|
+
return True
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
def _merge_per_action_override(
|
|
259
|
+
self,
|
|
260
|
+
default_requirements: list[dict[str, Any]],
|
|
261
|
+
all_user_requirements: list[dict[str, Any]],
|
|
262
|
+
active_user_requirements: list[dict[str, Any]],
|
|
263
|
+
) -> list[dict[str, Any]]:
|
|
264
|
+
"""
|
|
265
|
+
Merge user requirements with defaults on a per-action basis.
|
|
266
|
+
|
|
267
|
+
User requirements override defaults for matching actions.
|
|
268
|
+
Defaults are kept for actions not specified by user.
|
|
269
|
+
|
|
270
|
+
Key behavior with ignore_patterns:
|
|
271
|
+
- If user defines a requirement for action X with ignore_patterns
|
|
272
|
+
- And the current file matches the ignore_patterns
|
|
273
|
+
- Then: The user requirement is SKIPPED (not applied)
|
|
274
|
+
- AND: The default for action X is ALSO skipped (user "owns" this action)
|
|
275
|
+
- Result: No check for action X on this file
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
default_requirements: Default requirements from system config
|
|
279
|
+
all_user_requirements: ALL user requirements (before ignore_patterns filtering)
|
|
280
|
+
active_user_requirements: User requirements after ignore_patterns filtering
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
Merged list of requirements
|
|
284
|
+
"""
|
|
285
|
+
# Build a set of actions that user has customized (from ALL user requirements)
|
|
286
|
+
# This determines which defaults to exclude
|
|
287
|
+
user_actions = set()
|
|
288
|
+
for req in all_user_requirements:
|
|
289
|
+
actions = req.get("actions", [])
|
|
290
|
+
# Handle both single action and list of actions
|
|
291
|
+
if isinstance(actions, str):
|
|
292
|
+
user_actions.add(actions)
|
|
293
|
+
else:
|
|
294
|
+
user_actions.update(actions)
|
|
295
|
+
|
|
296
|
+
# Start with ACTIVE user requirements (filtered by ignore_patterns)
|
|
297
|
+
merged = list(active_user_requirements)
|
|
298
|
+
|
|
299
|
+
# Add defaults that don't conflict with user requirements
|
|
300
|
+
for default_req in default_requirements:
|
|
301
|
+
default_actions = default_req.get("actions", [])
|
|
302
|
+
# Handle both single action and list of actions
|
|
303
|
+
if isinstance(default_actions, str):
|
|
304
|
+
default_actions = [default_actions]
|
|
305
|
+
|
|
306
|
+
# Check if any of the default actions are customized by user
|
|
307
|
+
has_overlap = any(action in user_actions for action in default_actions)
|
|
308
|
+
|
|
309
|
+
if not has_overlap:
|
|
310
|
+
# No overlap - keep this default requirement
|
|
311
|
+
merged.append(default_req)
|
|
312
|
+
|
|
313
|
+
return merged
|
|
314
|
+
|
|
249
315
|
def _filter_requirement_issues(
|
|
250
316
|
self,
|
|
251
317
|
issues: list[ValidationIssue],
|
|
@@ -1016,33 +1082,52 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
1016
1082
|
)
|
|
1017
1083
|
|
|
1018
1084
|
if not any_present:
|
|
1019
|
-
#
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1085
|
+
# Check if requirement has custom message/suggestion/example
|
|
1086
|
+
custom_message = requirement.get("message") if requirement else None
|
|
1087
|
+
custom_suggestion = requirement.get("suggestion") if requirement else None
|
|
1088
|
+
custom_example = requirement.get("example") if requirement else None
|
|
1089
|
+
|
|
1090
|
+
if custom_message:
|
|
1091
|
+
# Use fully custom message/suggestion/example from requirement
|
|
1092
|
+
message = custom_message
|
|
1093
|
+
suggestion = custom_suggestion or ""
|
|
1094
|
+
example = custom_example or ""
|
|
1095
|
+
else:
|
|
1096
|
+
# Generate default message and build suggestion from conditions
|
|
1097
|
+
condition_keys = []
|
|
1098
|
+
for cond in any_of:
|
|
1099
|
+
if "all_of" in cond:
|
|
1100
|
+
# Nested all_of - collect all condition keys
|
|
1101
|
+
nested_keys = [
|
|
1102
|
+
c.get("condition_key", "unknown") for c in cond["all_of"]
|
|
1103
|
+
]
|
|
1104
|
+
condition_keys.append(
|
|
1105
|
+
f"({' + '.join(f'`{k}`' for k in nested_keys)})"
|
|
1106
|
+
)
|
|
1107
|
+
else:
|
|
1108
|
+
# Simple condition
|
|
1109
|
+
condition_keys.append(f"`{cond.get('condition_key', 'unknown')}`")
|
|
1110
|
+
condition_keys_formatted = " OR ".join(condition_keys)
|
|
1111
|
+
matching_actions_formatted = ", ".join(f"`{a}`" for a in matching_actions)
|
|
1112
|
+
|
|
1113
|
+
message = (
|
|
1114
|
+
f"Actions {matching_actions_formatted} require at least ONE of these conditions: "
|
|
1115
|
+
f"{condition_keys_formatted}"
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
# Build suggestion and examples from conditions
|
|
1119
|
+
suggestion, example = self._build_any_of_suggestion(any_of)
|
|
1120
|
+
|
|
1034
1121
|
issues.append(
|
|
1035
1122
|
ValidationIssue(
|
|
1036
1123
|
severity=self.get_severity(config),
|
|
1037
1124
|
statement_sid=statement.sid,
|
|
1038
1125
|
statement_index=statement_idx,
|
|
1039
1126
|
issue_type="missing_required_condition_any_of",
|
|
1040
|
-
message=
|
|
1041
|
-
f"Actions {matching_actions_formatted} require at least ONE of these conditions: "
|
|
1042
|
-
f"{condition_keys_formatted}"
|
|
1043
|
-
),
|
|
1127
|
+
message=message,
|
|
1044
1128
|
action=", ".join(matching_actions),
|
|
1045
|
-
suggestion=
|
|
1129
|
+
suggestion=suggestion,
|
|
1130
|
+
example=example if example else None,
|
|
1046
1131
|
line_number=statement.line_number,
|
|
1047
1132
|
)
|
|
1048
1133
|
)
|
|
@@ -1241,12 +1326,24 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
1241
1326
|
|
|
1242
1327
|
return suggestion, example_code
|
|
1243
1328
|
|
|
1244
|
-
def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
|
|
1245
|
-
"""Build suggestion for any_of conditions.
|
|
1329
|
+
def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> tuple[str, str]:
|
|
1330
|
+
"""Build suggestion and combined examples for any_of conditions.
|
|
1331
|
+
|
|
1332
|
+
Always uses clean formatting without "Option X" prefixes.
|
|
1333
|
+
Uses either:
|
|
1334
|
+
- 'message' field if provided (custom message)
|
|
1335
|
+
- 'description' field if provided (displays as: `condition_key` - description)
|
|
1336
|
+
- Just 'condition_key' if neither message nor description provided
|
|
1337
|
+
|
|
1338
|
+
Returns:
|
|
1339
|
+
Tuple of (suggestion_text, combined_examples)
|
|
1340
|
+
"""
|
|
1246
1341
|
suggestions = []
|
|
1342
|
+
examples = []
|
|
1343
|
+
|
|
1247
1344
|
suggestions.append("Add at least ONE of these conditions:")
|
|
1248
1345
|
|
|
1249
|
-
for
|
|
1346
|
+
for cond in any_of_conditions:
|
|
1250
1347
|
# Handle nested all_of blocks
|
|
1251
1348
|
if "all_of" in cond:
|
|
1252
1349
|
# Nested all_of - show all required conditions together
|
|
@@ -1254,35 +1351,55 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
1254
1351
|
condition_keys = [c.get("condition_key", "unknown") for c in all_of_list]
|
|
1255
1352
|
condition_keys_formatted = " + ".join(f"`{k}`" for k in condition_keys)
|
|
1256
1353
|
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1354
|
+
# Check for custom message first
|
|
1355
|
+
custom_message = cond.get("message")
|
|
1356
|
+
if custom_message:
|
|
1357
|
+
suggestions.append(f"\n- {custom_message}")
|
|
1358
|
+
else:
|
|
1359
|
+
# Use description from first condition or combine them
|
|
1360
|
+
descriptions = [
|
|
1361
|
+
c.get("description", "") for c in all_of_list if c.get("description")
|
|
1362
|
+
]
|
|
1363
|
+
if descriptions:
|
|
1364
|
+
suggestions.append(f"\n- {condition_keys_formatted} - {descriptions[0]}")
|
|
1365
|
+
else:
|
|
1366
|
+
suggestions.append(f"\n- {condition_keys_formatted} (both required)")
|
|
1367
|
+
|
|
1368
|
+
# Collect example from first condition that has one
|
|
1267
1369
|
for c in all_of_list:
|
|
1268
1370
|
if c.get("example"):
|
|
1269
|
-
|
|
1371
|
+
examples.append(c["example"])
|
|
1270
1372
|
break
|
|
1271
1373
|
else:
|
|
1272
|
-
# Simple condition
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1374
|
+
# Simple condition - check for message, description, or just condition_key
|
|
1375
|
+
custom_message = cond.get("message")
|
|
1376
|
+
if custom_message:
|
|
1377
|
+
# Use custom message directly
|
|
1378
|
+
suggestions.append(f"\n- {custom_message}")
|
|
1379
|
+
else:
|
|
1380
|
+
# Use description if available
|
|
1381
|
+
condition_key = cond.get("condition_key", "unknown")
|
|
1382
|
+
description = cond.get("description", "")
|
|
1383
|
+
expected_value = cond.get("expected_value")
|
|
1384
|
+
|
|
1385
|
+
if description:
|
|
1386
|
+
# Format: - `condition_key` - description
|
|
1387
|
+
suggestions.append(f"\n- `{condition_key}` - {description}")
|
|
1388
|
+
else:
|
|
1389
|
+
# Format: - `condition_key` (with expected value if present)
|
|
1390
|
+
suggestion_line = f"\n- `{condition_key}`"
|
|
1391
|
+
if expected_value is not None:
|
|
1392
|
+
suggestion_line += f" (value: `{expected_value}`)"
|
|
1393
|
+
suggestions.append(suggestion_line)
|
|
1394
|
+
|
|
1395
|
+
# Collect example if present (no prefix)
|
|
1396
|
+
if cond.get("example"):
|
|
1397
|
+
examples.append(cond["example"])
|
|
1398
|
+
|
|
1399
|
+
suggestion_text = "".join(suggestions)
|
|
1400
|
+
combined_examples = "\n\n".join(examples) if examples else ""
|
|
1401
|
+
|
|
1402
|
+
return suggestion_text, combined_examples
|
|
1286
1403
|
|
|
1287
1404
|
def _create_none_of_issue(
|
|
1288
1405
|
self,
|
|
@@ -271,6 +271,50 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
271
271
|
|
|
272
272
|
return issues
|
|
273
273
|
|
|
274
|
+
def _apply_merge_strategy(
|
|
275
|
+
self,
|
|
276
|
+
merge_strategy: str,
|
|
277
|
+
user_config: list[dict] | None,
|
|
278
|
+
default_config: list[dict] | None,
|
|
279
|
+
) -> list[dict] | None:
|
|
280
|
+
"""
|
|
281
|
+
Apply merge strategy to combine user and default sensitive action patterns.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
merge_strategy: One of "per_action_override", "user_only", "append",
|
|
285
|
+
"replace_all", or "defaults_only"
|
|
286
|
+
user_config: User-provided sensitive action patterns (or None)
|
|
287
|
+
default_config: Default sensitive action patterns (or None)
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
Merged list of patterns based on strategy, or None if no patterns
|
|
291
|
+
"""
|
|
292
|
+
if merge_strategy == "user_only":
|
|
293
|
+
# Use ONLY user patterns, completely ignore defaults
|
|
294
|
+
return user_config
|
|
295
|
+
|
|
296
|
+
elif merge_strategy == "defaults_only":
|
|
297
|
+
# Use ONLY defaults, ignore user patterns
|
|
298
|
+
return default_config
|
|
299
|
+
|
|
300
|
+
elif merge_strategy == "append":
|
|
301
|
+
# Combine both (defaults first, then user)
|
|
302
|
+
result = []
|
|
303
|
+
if default_config:
|
|
304
|
+
result.extend(default_config)
|
|
305
|
+
if user_config:
|
|
306
|
+
result.extend(user_config)
|
|
307
|
+
return result if result else None
|
|
308
|
+
|
|
309
|
+
elif merge_strategy == "replace_all":
|
|
310
|
+
# User replaces all if provided, otherwise use defaults
|
|
311
|
+
return user_config if user_config else default_config
|
|
312
|
+
|
|
313
|
+
else: # "per_action_override" (default)
|
|
314
|
+
# If user provides patterns, use them; otherwise use defaults
|
|
315
|
+
# This is the legacy behavior
|
|
316
|
+
return user_config if user_config else default_config
|
|
317
|
+
|
|
274
318
|
async def execute_policy(
|
|
275
319
|
self,
|
|
276
320
|
policy: "IAMPolicy",
|
|
@@ -319,9 +363,45 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
319
363
|
statement_map[action] = []
|
|
320
364
|
statement_map[action].append((idx, statement.sid))
|
|
321
365
|
|
|
322
|
-
# Get configuration for sensitive actions
|
|
323
|
-
|
|
324
|
-
|
|
366
|
+
# Get configuration for sensitive actions with merge_strategy support
|
|
367
|
+
# merge_strategy options:
|
|
368
|
+
# - "append": Add user patterns ON TOP OF defaults (both apply) - DEFAULT
|
|
369
|
+
# - "user_only": Use ONLY user patterns, disable ALL default privilege escalation patterns
|
|
370
|
+
# - "defaults_only": Ignore user patterns, use only defaults
|
|
371
|
+
# - "replace_all": User patterns completely replace ALL defaults (if provided)
|
|
372
|
+
# - "per_action_override": User patterns replace defaults for matching action combos
|
|
373
|
+
merge_strategy = config.config.get("merge_strategy", "append")
|
|
374
|
+
|
|
375
|
+
# Determine which sensitive_actions patterns to use based on merge_strategy
|
|
376
|
+
# Note: The config.config already contains deep-merged values from defaults + user config
|
|
377
|
+
# For lists like sensitive_actions, user config REPLACES defaults (not merges)
|
|
378
|
+
# So if user provided sensitive_actions, it's already the only value in config.config
|
|
379
|
+
sensitive_actions_config: list[dict] | None = None
|
|
380
|
+
sensitive_patterns_config: list[dict] | None = None
|
|
381
|
+
|
|
382
|
+
if merge_strategy == "user_only":
|
|
383
|
+
# user_only: Disable ALL default patterns
|
|
384
|
+
# If user set merge_strategy: "user_only", they want NO defaults
|
|
385
|
+
# They must explicitly provide sensitive_actions if they want any checks
|
|
386
|
+
# Since we can't distinguish user-provided from defaults after merge,
|
|
387
|
+
# we assume user_only means "no patterns" unless user explicitly provided them
|
|
388
|
+
# (which would have replaced defaults anyway)
|
|
389
|
+
sensitive_actions_config = None
|
|
390
|
+
sensitive_patterns_config = None
|
|
391
|
+
|
|
392
|
+
elif merge_strategy == "defaults_only":
|
|
393
|
+
# Use only defaults - but since config is merged, we use what's there
|
|
394
|
+
# (user would need to NOT provide sensitive_actions to get defaults)
|
|
395
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
396
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
397
|
+
|
|
398
|
+
else:
|
|
399
|
+
# append, replace_all, per_action_override all use the merged config
|
|
400
|
+
# The deep_merge already handled the merging:
|
|
401
|
+
# - If user provided sensitive_actions, it replaced defaults
|
|
402
|
+
# - If user didn't provide it, defaults are in config
|
|
403
|
+
sensitive_actions_config = config.config.get("sensitive_actions")
|
|
404
|
+
sensitive_patterns_config = config.config.get("sensitive_action_patterns")
|
|
325
405
|
|
|
326
406
|
# Check for privilege escalation patterns using all_of logic
|
|
327
407
|
# We need to check both exact actions and patterns
|
|
@@ -98,15 +98,21 @@ def _check_all_of_pattern(
|
|
|
98
98
|
Returns:
|
|
99
99
|
ValidationIssue if privilege escalation detected, None otherwise
|
|
100
100
|
"""
|
|
101
|
+
# Filter out actions that match ignore_patterns BEFORE checking for privilege escalation
|
|
102
|
+
# This allows users to exclude specific actions from privilege escalation detection
|
|
103
|
+
# by adding them to ignore_patterns in sensitive_action config
|
|
104
|
+
filtered_actions = check_config.filter_actions(frozenset(all_actions))
|
|
105
|
+
all_actions_filtered = list(filtered_actions)
|
|
106
|
+
|
|
101
107
|
matched_actions = []
|
|
102
108
|
|
|
103
109
|
if check_type == "actions":
|
|
104
110
|
# Exact matching
|
|
105
|
-
matched_actions = [a for a in
|
|
111
|
+
matched_actions = [a for a in all_actions_filtered if a in required_actions]
|
|
106
112
|
else:
|
|
107
113
|
# Pattern matching - for each pattern, find actions that match
|
|
108
114
|
for pattern in required_actions:
|
|
109
|
-
for action in
|
|
115
|
+
for action in all_actions_filtered:
|
|
110
116
|
try:
|
|
111
117
|
if re.match(pattern, action):
|
|
112
118
|
matched_actions.append(action)
|
|
@@ -167,6 +173,9 @@ def _check_all_of_pattern(
|
|
|
167
173
|
f"Actions found in:\n - {stmt_details}",
|
|
168
174
|
)
|
|
169
175
|
|
|
176
|
+
# Use custom example if provided in item_config
|
|
177
|
+
example = item_config.get("example")
|
|
178
|
+
|
|
170
179
|
return ValidationIssue(
|
|
171
180
|
severity=severity,
|
|
172
181
|
statement_sid=None, # Policy-level issue
|
|
@@ -174,6 +183,7 @@ def _check_all_of_pattern(
|
|
|
174
183
|
issue_type="privilege_escalation",
|
|
175
184
|
message=message,
|
|
176
185
|
suggestion=suggestion,
|
|
186
|
+
example=example,
|
|
177
187
|
line_number=1, # Policy-level issues point to line 1 (top of policy)
|
|
178
188
|
)
|
|
179
189
|
|
|
@@ -519,7 +519,7 @@ DEFAULT_CONFIG = {
|
|
|
519
519
|
# Useful when you have specific condition enforcement for certain actions
|
|
520
520
|
# Example: Ignore iam:PassRole since it's checked by action_condition_enforcement
|
|
521
521
|
"ignore_patterns": [
|
|
522
|
-
{"
|
|
522
|
+
{"action": "^iam:PassRole$"},
|
|
523
523
|
],
|
|
524
524
|
# Cross-statement privilege escalation patterns (policy-wide detection)
|
|
525
525
|
# These patterns detect dangerous action combinations across ANY statements in the policy
|
|
@@ -537,7 +537,19 @@ DEFAULT_CONFIG = {
|
|
|
537
537
|
"3. Escalate to full account access\n\n"
|
|
538
538
|
"Mitigation options:\n"
|
|
539
539
|
"• Remove both of these permissions\n"
|
|
540
|
-
"• Add strict IAM conditions (
|
|
540
|
+
"• Add strict IAM conditions (IP restrictions, tags, force a specific policy with `iam:PolicyARN` condition)\n"
|
|
541
|
+
),
|
|
542
|
+
"example": (
|
|
543
|
+
"{\n"
|
|
544
|
+
' "Condition": {\n'
|
|
545
|
+
' "StringEquals": {\n'
|
|
546
|
+
' "iam:PolicyARN": "arn:aws:iam::*:policy/ReadOnlyAccess"\n'
|
|
547
|
+
" },\n"
|
|
548
|
+
' "IpAddress": {\n'
|
|
549
|
+
' "aws:SourceIp": ["10.0.0.0/8"]\n'
|
|
550
|
+
" }\n"
|
|
551
|
+
" }\n"
|
|
552
|
+
"}\n"
|
|
541
553
|
),
|
|
542
554
|
},
|
|
543
555
|
# Role privilege escalation: Create role + attach admin policy
|
|
@@ -551,6 +563,23 @@ DEFAULT_CONFIG = {
|
|
|
551
563
|
"• Remove both of these permissions\n"
|
|
552
564
|
"• Add strict IAM conditions with a Permissions Boundary and ABAC Tagging, force a specific policy with `iam:PolicyARN` condition\n"
|
|
553
565
|
),
|
|
566
|
+
"example": (
|
|
567
|
+
"{\n"
|
|
568
|
+
' "Condition": {\n'
|
|
569
|
+
' "StringEquals": {\n'
|
|
570
|
+
' "iam:PermissionsBoundary": "arn:aws:iam::*:policy/MaxPermissions"\n'
|
|
571
|
+
" }\n"
|
|
572
|
+
" }\n"
|
|
573
|
+
"}\n"
|
|
574
|
+
"OR\n"
|
|
575
|
+
"{\n"
|
|
576
|
+
' "Condition": {\n'
|
|
577
|
+
' "StringEquals": {\n'
|
|
578
|
+
' "iam:PolicyARN": "arn:aws:iam::*:policy/MaxPermissions"\n'
|
|
579
|
+
" }\n"
|
|
580
|
+
" }\n"
|
|
581
|
+
"}\n"
|
|
582
|
+
),
|
|
554
583
|
},
|
|
555
584
|
# Lambda backdoor: Create/update function + invoke
|
|
556
585
|
{
|
|
@@ -567,6 +596,18 @@ DEFAULT_CONFIG = {
|
|
|
567
596
|
"• Require MFA for Lambda function creation\n"
|
|
568
597
|
"• Use separate policies for creation vs invocation"
|
|
569
598
|
),
|
|
599
|
+
"example": (
|
|
600
|
+
"{\n"
|
|
601
|
+
' "Condition": {\n'
|
|
602
|
+
' "StringEquals": {\n'
|
|
603
|
+
' "aws:PrincipalTag/team": "${aws:ResourceTag/team}"\n'
|
|
604
|
+
" },\n"
|
|
605
|
+
' "SourceIp": {\n'
|
|
606
|
+
' "aws:SourceIp": ["10.0.0.0/8"]\n'
|
|
607
|
+
" }\n"
|
|
608
|
+
" }\n"
|
|
609
|
+
"}\n"
|
|
610
|
+
),
|
|
570
611
|
},
|
|
571
612
|
# Lambda code modification backdoor
|
|
572
613
|
{
|
|
@@ -581,6 +622,15 @@ DEFAULT_CONFIG = {
|
|
|
581
622
|
"• Use separate policies for code updates vs invocation\n"
|
|
582
623
|
"• Implement code signing for Lambda functions"
|
|
583
624
|
),
|
|
625
|
+
"example": (
|
|
626
|
+
"{\n"
|
|
627
|
+
' "Condition": {\n'
|
|
628
|
+
' "StringEquals": {\n'
|
|
629
|
+
' "aws:ResourceAccount": "${aws:PrincipalAccount}"\n'
|
|
630
|
+
" }\n"
|
|
631
|
+
" }\n"
|
|
632
|
+
"}\n"
|
|
633
|
+
),
|
|
584
634
|
},
|
|
585
635
|
# EC2 instance privilege escalation
|
|
586
636
|
{
|
|
@@ -595,6 +645,18 @@ DEFAULT_CONFIG = {
|
|
|
595
645
|
"• Limit PassRole to specific low-privilege roles\n"
|
|
596
646
|
"• Require tagging and ABAC controls"
|
|
597
647
|
),
|
|
648
|
+
"example": (
|
|
649
|
+
"{\n"
|
|
650
|
+
' "Condition": {\n'
|
|
651
|
+
' "StringEquals": {\n'
|
|
652
|
+
' "iam:PassedToService": "ec2.amazonaws.com"\n'
|
|
653
|
+
" },\n"
|
|
654
|
+
' "ArnLike": {\n'
|
|
655
|
+
' "iam:AssociatedResourceArn": "arn:aws:ec2:*:*:instance/*"\n'
|
|
656
|
+
" }\n"
|
|
657
|
+
" }\n"
|
|
658
|
+
"}\n"
|
|
659
|
+
),
|
|
598
660
|
},
|
|
599
661
|
],
|
|
600
662
|
},
|
iam_validator/core/report.py
CHANGED
|
@@ -882,6 +882,17 @@ class ReportGenerator:
|
|
|
882
882
|
parts.append(f"> 💡 **Suggestion:** {issue.suggestion}")
|
|
883
883
|
parts.append("")
|
|
884
884
|
|
|
885
|
+
# Handle separate example field (if not already in suggestion)
|
|
886
|
+
if issue.example and "\nExample:\n" not in (issue.suggestion or ""):
|
|
887
|
+
parts.append("<details>")
|
|
888
|
+
parts.append("<summary>📖 View Example</summary>")
|
|
889
|
+
parts.append("")
|
|
890
|
+
parts.append("```json")
|
|
891
|
+
parts.append(issue.example)
|
|
892
|
+
parts.append("```")
|
|
893
|
+
parts.append("</details>")
|
|
894
|
+
parts.append("")
|
|
895
|
+
|
|
885
896
|
return "\n".join(parts)
|
|
886
897
|
|
|
887
898
|
def save_json_report(self, report: ValidationReport, file_path: str) -> None:
|
{iam_policy_validator-1.12.0.dist-info → iam_policy_validator-1.13.0.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{iam_policy_validator-1.12.0.dist-info → iam_policy_validator-1.13.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|