iam-policy-validator 1.10.3__py3-none-any.whl → 1.11.1__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.11.1.dist-info/METADATA +782 -0
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/RECORD +25 -21
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +27 -14
- iam_validator/checks/sensitive_action.py +123 -11
- iam_validator/checks/utils/policy_level_checks.py +47 -10
- iam_validator/commands/__init__.py +6 -0
- iam_validator/commands/completion.py +467 -0
- iam_validator/commands/query.py +485 -0
- iam_validator/commands/validate.py +21 -26
- iam_validator/core/config/category_suggestions.py +77 -0
- iam_validator/core/config/condition_requirements.py +105 -54
- iam_validator/core/config/defaults.py +82 -6
- iam_validator/core/config/wildcards.py +3 -0
- iam_validator/core/diff_parser.py +321 -0
- iam_validator/core/formatters/enhanced.py +34 -27
- iam_validator/core/models.py +2 -0
- iam_validator/core/pr_commenter.py +179 -51
- iam_validator/core/report.py +19 -17
- iam_validator/integrations/github_integration.py +250 -1
- iam_validator/sdk/__init__.py +33 -0
- iam_validator/sdk/query_utils.py +454 -0
- iam_policy_validator-1.10.3.dist-info/METADATA +0 -549
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.10.3.dist-info → iam_policy_validator-1.11.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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=VA38cDITf2Rm6FSdOiIMZHEocdI2rmYOPTSCCAIrJ-M,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=6LJfO7DCgf10rtPjaZ5P4fmb5hfxJUBS5w1CrOiCu5Q,52442
|
|
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=ckyb2n47hKuyr1smC4_Q0dkcdngfhaMueyesQcNE_6k,14912
|
|
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,16 +22,18 @@ 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=hZjexXgxuELf-wrO-JwVK8VzP8oRHK3sk0PyaG7QVfI,7070
|
|
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
|
-
iam_validator/commands/__init__.py,sha256=
|
|
28
|
+
iam_validator/commands/__init__.py,sha256=RBEz-Kgt3aRVn_9B1HRy_XgQMIKzlSSQs4Gtg2jQEv8,729
|
|
29
29
|
iam_validator/commands/analyze.py,sha256=rvLBJ5_A3HB530xtixhaIsC19QON68olEQnn8TievgI,20784
|
|
30
30
|
iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
|
|
31
31
|
iam_validator/commands/cache.py,sha256=llfyQzPE5Azd5YcW0ohYcYjF_OCyiQ1GoJQ982t71lQ,14294
|
|
32
|
+
iam_validator/commands/completion.py,sha256=1TfkVjeltsf2jAZy5RqaxKVIxpbL_bnZc6k8uvrk6Xg,18915
|
|
32
33
|
iam_validator/commands/download_services.py,sha256=KKz3ybMLT8DQUf9aFZ0tilJ-o1b6PE8Pf1pC4K6cT8I,9175
|
|
33
34
|
iam_validator/commands/post_to_pr.py,sha256=CvUXs2xvO-UhluxdfNM6F0TCWD8hDBEOiYw60fm1Dms,2363
|
|
34
|
-
iam_validator/commands/
|
|
35
|
+
iam_validator/commands/query.py,sha256=ft8ptWfsNUK4Wprq_A11txdV_chBgqkoAo7SQfzEwK0,17079
|
|
36
|
+
iam_validator/commands/validate.py,sha256=lsiHXjeY1JVKelP5CpIpwMHx1cldmEMLPqCzD-XHvL4,24214
|
|
35
37
|
iam_validator/core/__init__.py,sha256=hYXkSbxplKzhM6dqrVzV4M3k7GKLsZbgExypxKq74gs,376
|
|
36
38
|
iam_validator/core/access_analyzer.py,sha256=mtMaY-FnKjKEVITky_9ywZe1FaCAm61ElRv5Z_ZeC7E,24562
|
|
37
39
|
iam_validator/core/access_analyzer_report.py,sha256=UMm2RNGj2rAKav1zsCw_htQZZRwRC0jjayd2zvKma1A,24896
|
|
@@ -40,13 +42,14 @@ iam_validator/core/check_registry.py,sha256=oRCdWoCGQ8VZERVYd821u9r5NdKQ9FMC54e6
|
|
|
40
42
|
iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
|
|
41
43
|
iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
|
|
42
44
|
iam_validator/core/constants.py,sha256=cVBPgbXr4ALltH_NTSKsgBi6wmndLnOyUWhyBx0ZwrM,6113
|
|
45
|
+
iam_validator/core/diff_parser.py,sha256=yjIplHywYWLr2lrGecwYraynmMerTpIjxFHy2a4itsM,11688
|
|
43
46
|
iam_validator/core/ignore_patterns.py,sha256=pZqDJBtkbck-85QK5eFPM5ZOPEKs3McRh3avqiCT5z0,10398
|
|
44
47
|
iam_validator/core/label_manager.py,sha256=48CRASWg98wyjfVF_1pUzj6dm9itzmG7SeIWf0TSUfc,7502
|
|
45
|
-
iam_validator/core/models.py,sha256=
|
|
48
|
+
iam_validator/core/models.py,sha256=FhQ7fpX6T9AOvHsAPlBZL0NmPuPE_xghD3dp4cAGLZw,13534
|
|
46
49
|
iam_validator/core/policy_checks.py,sha256=FNVuS2GTffwCjjrlupVIazC172gSxKYAAT_ObV6Apbo,8803
|
|
47
50
|
iam_validator/core/policy_loader.py,sha256=2KJnXzGg3g9pDXWZHk3DO0xpZnZZ-wXWFEOdQ_naJ8s,17862
|
|
48
|
-
iam_validator/core/pr_commenter.py,sha256=
|
|
49
|
-
iam_validator/core/report.py,sha256=
|
|
51
|
+
iam_validator/core/pr_commenter.py,sha256=vDN9meq861nVpno-GyhGX4wnBE1Z7clCIhv6pq9rZGs,22755
|
|
52
|
+
iam_validator/core/report.py,sha256=BkhBFZHBKuF5WiUqXuNgEpFxcDCnbVRjzIy9qfezxdk,36071
|
|
50
53
|
iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
|
|
51
54
|
iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
|
|
52
55
|
iam_validator/core/aws_service/client.py,sha256=Zv7rIpEFdUCDXKGp3migPDkj8L5eZltgrGe64M2t2Ko,7336
|
|
@@ -58,39 +61,40 @@ iam_validator/core/aws_service/validators.py,sha256=L9XRJdGmR-vZ1r0bj5SCznULyKEY
|
|
|
58
61
|
iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
|
|
59
62
|
iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
|
|
60
63
|
iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
|
|
61
|
-
iam_validator/core/config/category_suggestions.py,sha256=
|
|
62
|
-
iam_validator/core/config/condition_requirements.py,sha256=
|
|
64
|
+
iam_validator/core/config/category_suggestions.py,sha256=fopaZ9kXDrsLgi_r0pERrLwgdPPJl5VIiKvXtQK9tj0,8583
|
|
65
|
+
iam_validator/core/config/condition_requirements.py,sha256=1CeQJfWV-Y2ImW0Mq9YdrgvH-hj9IXe0gVOm3B36Rc8,10655
|
|
63
66
|
iam_validator/core/config/config_loader.py,sha256=qKD8aR8YAswaFf68pnYJLFNwKznvcc6lNxSQWU3i6SY,17713
|
|
64
|
-
iam_validator/core/config/defaults.py,sha256=
|
|
67
|
+
iam_validator/core/config/defaults.py,sha256=HYCuVXVRMT-0E8R6g609STFBU_IeMG0fFMHUYXsArAU,34685
|
|
65
68
|
iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
|
|
66
69
|
iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
|
|
67
70
|
iam_validator/core/config/service_principals.py,sha256=8pys5H_yycVJ9KTyimAKFYBg83Aol2Iri53wiHjtnEM,3959
|
|
68
|
-
iam_validator/core/config/wildcards.py,sha256=
|
|
71
|
+
iam_validator/core/config/wildcards.py,sha256=PI7Fmr7oMOkqdn_XJ0ST0U5NOUG6k4qKUEoouKWAPvc,3288
|
|
69
72
|
iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
|
|
70
73
|
iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
|
|
71
74
|
iam_validator/core/formatters/console.py,sha256=FdTp7AzeILCWrUynSvSew8QJKGOMJaAA9_YiQJd-uco,2196
|
|
72
75
|
iam_validator/core/formatters/csv.py,sha256=pPqgvGh4KtD5Qm36xnMaDAavXYR6MlQhs4zbcrxT550,5941
|
|
73
|
-
iam_validator/core/formatters/enhanced.py,sha256=
|
|
76
|
+
iam_validator/core/formatters/enhanced.py,sha256=GD7RIAL1hLDAsypCKECwDMGslAx2AaMPbdoW6YZTAlQ,18555
|
|
74
77
|
iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
|
|
75
78
|
iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
|
|
76
79
|
iam_validator/core/formatters/markdown.py,sha256=dk4STeY-tOEZsVrlmolIEqZvWYP9JhRtygxxNA49DEE,2293
|
|
77
80
|
iam_validator/core/formatters/sarif.py,sha256=O3pn7whqFq5xxk-tuoqSb2k4Fk5ai_A2SKX_ph8GLV4,10469
|
|
78
81
|
iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
|
|
79
|
-
iam_validator/integrations/github_integration.py,sha256=
|
|
82
|
+
iam_validator/integrations/github_integration.py,sha256=hvAmoNLn4r9eglBw9KTnm4E-Or0weh_CM_tkuT2TFiI,36413
|
|
80
83
|
iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
|
|
81
|
-
iam_validator/sdk/__init__.py,sha256=
|
|
84
|
+
iam_validator/sdk/__init__.py,sha256=AZLnfdn3A9AWb0pMhsbu3GAOAzt6rV7Fi3E3d9_3ZdI,6388
|
|
82
85
|
iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urchEFDA,12778
|
|
83
86
|
iam_validator/sdk/context.py,sha256=FvAEyUa_s7tHWoSdgjSkzHf1CLlYpAEmLZANxs2IJ4A,6826
|
|
84
87
|
iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
|
|
85
88
|
iam_validator/sdk/helpers.py,sha256=sjfK0na_Fo7O8GhEVhl44rVHqOdw6nAKkBL4FVL-QdU,5697
|
|
86
89
|
iam_validator/sdk/policy_utils.py,sha256=bGdJ1X1aC72dVXXpAnAwyBpAiiX-qXvblpetY5BsjKU,13658
|
|
90
|
+
iam_validator/sdk/query_utils.py,sha256=kp1sORVnouRMt7kvzyZo1569l7j20jJGmHICR7O8Cqs,14455
|
|
87
91
|
iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
|
|
88
92
|
iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYuCs,1021
|
|
89
93
|
iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
|
|
90
94
|
iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
|
|
91
95
|
iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
|
|
92
|
-
iam_policy_validator-1.
|
|
93
|
-
iam_policy_validator-1.
|
|
94
|
-
iam_policy_validator-1.
|
|
95
|
-
iam_policy_validator-1.
|
|
96
|
-
iam_policy_validator-1.
|
|
96
|
+
iam_policy_validator-1.11.1.dist-info/METADATA,sha256=yEdzYv7iyjewXjI59L_Jy5la1Gl49V6rObTECdOGfgY,34456
|
|
97
|
+
iam_policy_validator-1.11.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
98
|
+
iam_policy_validator-1.11.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
|
|
99
|
+
iam_policy_validator-1.11.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
100
|
+
iam_policy_validator-1.11.1.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.11.1"
|
|
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("."))
|
|
@@ -133,6 +133,7 @@ from typing import TYPE_CHECKING, Any, ClassVar
|
|
|
133
133
|
from iam_validator.core.aws_service import AWSServiceFetcher
|
|
134
134
|
from iam_validator.core.check_registry import CheckConfig, PolicyCheck
|
|
135
135
|
from iam_validator.core.models import Statement, ValidationIssue
|
|
136
|
+
from iam_validator.utils.regex import compile_and_cache
|
|
136
137
|
|
|
137
138
|
if TYPE_CHECKING:
|
|
138
139
|
from iam_validator.core.models import IAMPolicy
|
|
@@ -184,10 +185,16 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
184
185
|
issues = []
|
|
185
186
|
|
|
186
187
|
# Get action condition requirements from config
|
|
187
|
-
# Support
|
|
188
|
+
# Support legacy keys for backward compatibility:
|
|
189
|
+
# - "requirements" (current/preferred)
|
|
190
|
+
# - "action_condition_requirements" (legacy)
|
|
191
|
+
# - "policy_level_requirements" (legacy)
|
|
188
192
|
requirements = config.config.get(
|
|
189
|
-
"
|
|
190
|
-
config.config.get(
|
|
193
|
+
"requirements",
|
|
194
|
+
config.config.get(
|
|
195
|
+
"action_condition_requirements",
|
|
196
|
+
config.config.get("policy_level_requirements", []),
|
|
197
|
+
),
|
|
191
198
|
)
|
|
192
199
|
|
|
193
200
|
if not requirements:
|
|
@@ -683,7 +690,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
683
690
|
matching_actions: list[str] = []
|
|
684
691
|
|
|
685
692
|
# Handle simple list format (backward compatibility)
|
|
686
|
-
|
|
693
|
+
# Also handle requirements with only action_patterns (when actions is empty list)
|
|
694
|
+
if isinstance(actions_config, list) and (actions_config or action_patterns):
|
|
687
695
|
# Simple list - check if any action matches
|
|
688
696
|
for stmt_action in statement_actions:
|
|
689
697
|
if stmt_action == "*":
|
|
@@ -701,9 +709,9 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
701
709
|
matched = True
|
|
702
710
|
break
|
|
703
711
|
|
|
704
|
-
# If not matched by actions, check
|
|
705
|
-
if not matched and
|
|
706
|
-
#
|
|
712
|
+
# If not matched by actions, check against action_patterns directly
|
|
713
|
+
if not matched and action_patterns:
|
|
714
|
+
# Check if statement action matches any of the patterns
|
|
707
715
|
matched = await self._action_matches(stmt_action, "", action_patterns, fetcher)
|
|
708
716
|
|
|
709
717
|
if matched and stmt_action not in matching_actions:
|
|
@@ -804,10 +812,11 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
804
812
|
|
|
805
813
|
# AWS wildcard match in required_action (e.g., "s3:*", "s3:Get*")
|
|
806
814
|
if "*" in required_action:
|
|
807
|
-
# Convert AWS wildcard to regex
|
|
815
|
+
# Convert AWS wildcard to regex and cache compilation
|
|
808
816
|
wildcard_pattern = required_action.replace("*", ".*").replace("?", ".")
|
|
809
817
|
try:
|
|
810
|
-
|
|
818
|
+
compiled_pattern = compile_and_cache(f"^{wildcard_pattern}$")
|
|
819
|
+
if compiled_pattern.match(statement_action):
|
|
811
820
|
return True
|
|
812
821
|
except re.error:
|
|
813
822
|
# Invalid regex pattern - skip this match attempt
|
|
@@ -824,7 +833,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
824
833
|
# Required action is specific (e.g., "iam:CreateUser")
|
|
825
834
|
# Check if statement wildcard would grant it
|
|
826
835
|
try:
|
|
827
|
-
|
|
836
|
+
compiled_pattern = compile_and_cache(f"^{stmt_wildcard_pattern}$")
|
|
837
|
+
if compiled_pattern.match(required_action):
|
|
828
838
|
return True
|
|
829
839
|
except re.error:
|
|
830
840
|
# Invalid regex pattern - skip this match attempt
|
|
@@ -856,7 +866,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
856
866
|
full_granted_action = f"{service_prefix}:{granted_action}"
|
|
857
867
|
for pattern in patterns:
|
|
858
868
|
try:
|
|
859
|
-
|
|
869
|
+
compiled_pattern = compile_and_cache(pattern)
|
|
870
|
+
if compiled_pattern.match(full_granted_action):
|
|
860
871
|
return True
|
|
861
872
|
except re.error:
|
|
862
873
|
continue
|
|
@@ -866,7 +877,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
866
877
|
stmt_prefix = statement_action.rstrip("*")
|
|
867
878
|
for pattern in patterns:
|
|
868
879
|
try:
|
|
869
|
-
|
|
880
|
+
compiled_pattern = compile_and_cache(pattern)
|
|
881
|
+
if compiled_pattern.match(stmt_prefix):
|
|
870
882
|
return True
|
|
871
883
|
except re.error:
|
|
872
884
|
continue
|
|
@@ -874,7 +886,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
874
886
|
# Regex pattern match (from action_patterns config)
|
|
875
887
|
for pattern in patterns:
|
|
876
888
|
try:
|
|
877
|
-
|
|
889
|
+
compiled_pattern = compile_and_cache(pattern)
|
|
890
|
+
if compiled_pattern.match(statement_action):
|
|
878
891
|
return True
|
|
879
892
|
except re.error:
|
|
880
893
|
continue
|
|
@@ -964,7 +977,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
964
977
|
statement_index=statement_idx,
|
|
965
978
|
issue_type="missing_required_condition_any_of",
|
|
966
979
|
message=(
|
|
967
|
-
f"Actions
|
|
980
|
+
f"Actions {matching_actions_formatted} require at least ONE of these conditions: "
|
|
968
981
|
f"{condition_keys_formatted}"
|
|
969
982
|
),
|
|
970
983
|
action=", ".join(matching_actions),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Sensitive action check - detects sensitive actions without IAM conditions."""
|
|
2
2
|
|
|
3
|
-
from typing import TYPE_CHECKING, ClassVar
|
|
3
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
4
4
|
|
|
5
5
|
from iam_validator.checks.utils.policy_level_checks import check_policy_level_actions
|
|
6
6
|
from iam_validator.checks.utils.sensitive_action_matcher import (
|
|
@@ -17,6 +17,71 @@ if TYPE_CHECKING:
|
|
|
17
17
|
from iam_validator.core.models import IAMPolicy
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
def get_suggestion_from_requirement(requirement: dict[str, Any]) -> tuple[str, str] | None:
|
|
21
|
+
"""
|
|
22
|
+
Extract suggestion and example from a condition requirement.
|
|
23
|
+
|
|
24
|
+
This is a public utility function that can be used by custom checks
|
|
25
|
+
to extract user-friendly suggestions from condition requirement structures.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
requirement: Condition requirement dictionary containing:
|
|
29
|
+
- suggestion_text: Human-readable guidance text
|
|
30
|
+
- required_conditions: Conditions structure (list or dict with any_of/all_of/none_of)
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Tuple of (suggestion_text, example) if available, None otherwise
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> from iam_validator.core.config.condition_requirements import IAM_PASS_ROLE_REQUIREMENT
|
|
37
|
+
>>> suggestion, example = get_suggestion_from_requirement(IAM_PASS_ROLE_REQUIREMENT)
|
|
38
|
+
>>> print(suggestion)
|
|
39
|
+
This action allows passing IAM roles to AWS services...
|
|
40
|
+
"""
|
|
41
|
+
# Check if requirement has suggestion_text
|
|
42
|
+
if "suggestion_text" not in requirement:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
suggestion_text = requirement["suggestion_text"]
|
|
46
|
+
|
|
47
|
+
# Extract example from required_conditions
|
|
48
|
+
example = ""
|
|
49
|
+
required_conditions = requirement.get("required_conditions", [])
|
|
50
|
+
|
|
51
|
+
# Handle different condition structures (list, dict with any_of/all_of/none_of)
|
|
52
|
+
if isinstance(required_conditions, list) and required_conditions:
|
|
53
|
+
# Get first condition's example
|
|
54
|
+
first_condition = required_conditions[0]
|
|
55
|
+
example = first_condition.get("example", "")
|
|
56
|
+
elif isinstance(required_conditions, dict):
|
|
57
|
+
# Handle any_of, all_of, none_of structures
|
|
58
|
+
for logic_key in ["any_of", "all_of", "none_of"]:
|
|
59
|
+
if logic_key in required_conditions:
|
|
60
|
+
conditions = required_conditions[logic_key]
|
|
61
|
+
if isinstance(conditions, list) and conditions:
|
|
62
|
+
# Get first option's example
|
|
63
|
+
first_option = conditions[0]
|
|
64
|
+
if isinstance(first_option, dict):
|
|
65
|
+
if "example" in first_option:
|
|
66
|
+
example = first_option["example"]
|
|
67
|
+
break
|
|
68
|
+
# Handle nested all_of/any_of/none_of structures
|
|
69
|
+
for nested_key in ["all_of", "any_of", "none_of"]:
|
|
70
|
+
if nested_key in first_option and isinstance(
|
|
71
|
+
first_option[nested_key], list
|
|
72
|
+
):
|
|
73
|
+
for nested in first_option[nested_key]:
|
|
74
|
+
if "example" in nested:
|
|
75
|
+
example = nested["example"]
|
|
76
|
+
break
|
|
77
|
+
if example:
|
|
78
|
+
break
|
|
79
|
+
if example:
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
return (suggestion_text, example)
|
|
83
|
+
|
|
84
|
+
|
|
20
85
|
class SensitiveActionCheck(PolicyCheck):
|
|
21
86
|
"""Checks for sensitive actions without IAM conditions to limit their use."""
|
|
22
87
|
|
|
@@ -48,11 +113,45 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
48
113
|
# Fall back to default severity
|
|
49
114
|
return self.get_severity(config)
|
|
50
115
|
|
|
116
|
+
def _get_actions_covered_by_condition_enforcement(self, config: CheckConfig) -> set[str]:
|
|
117
|
+
"""
|
|
118
|
+
Get set of actions that are covered by action_condition_enforcement requirements.
|
|
119
|
+
|
|
120
|
+
This prevents duplicate warnings when an action is already validated by
|
|
121
|
+
formal condition requirements.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
config: Check configuration with root_config access
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Set of action strings that are covered by condition requirements
|
|
128
|
+
"""
|
|
129
|
+
covered_actions: set[str] = set()
|
|
130
|
+
|
|
131
|
+
# Access action_condition_enforcement config from root_config
|
|
132
|
+
ace_config = config.root_config.get("action_condition_enforcement", {})
|
|
133
|
+
requirements = ace_config.get("requirements", [])
|
|
134
|
+
|
|
135
|
+
for requirement in requirements:
|
|
136
|
+
# Get actions from requirement
|
|
137
|
+
actions_config = requirement.get("actions", [])
|
|
138
|
+
if isinstance(actions_config, list):
|
|
139
|
+
covered_actions.update(actions_config)
|
|
140
|
+
|
|
141
|
+
return covered_actions
|
|
142
|
+
|
|
51
143
|
def _get_category_specific_suggestion(
|
|
52
144
|
self, action: str, config: CheckConfig
|
|
53
145
|
) -> tuple[str, str]:
|
|
54
146
|
"""
|
|
55
|
-
Get category-specific suggestion and example for an action.
|
|
147
|
+
Get category-specific suggestion and example for an action using two-tier lookup.
|
|
148
|
+
|
|
149
|
+
This method provides suggestions for the sensitive_action check, which flags
|
|
150
|
+
actions that have NO conditions. It does NOT validate specific conditions
|
|
151
|
+
(that's handled by the action_condition_enforcement check).
|
|
152
|
+
|
|
153
|
+
Tier 1: Check action_overrides in category suggestions for important actions
|
|
154
|
+
Tier 2: Fall back to category-level default suggestions
|
|
56
155
|
|
|
57
156
|
Args:
|
|
58
157
|
action: The AWS action to check
|
|
@@ -61,20 +160,23 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
61
160
|
Returns:
|
|
62
161
|
Tuple of (suggestion_text, example_text) tailored to the action's category
|
|
63
162
|
"""
|
|
163
|
+
# TIER 1: Check action-specific overrides in category suggestions
|
|
64
164
|
category = get_category_for_action(action)
|
|
65
|
-
|
|
66
|
-
# Get category suggestions from config (ABAC-focused by default)
|
|
67
|
-
# See: iam_validator/core/config/category_suggestions.py
|
|
68
165
|
category_suggestions = config.config.get("category_suggestions", {})
|
|
69
166
|
|
|
70
|
-
# Get category-specific content or fall back to generic ABAC guidance
|
|
71
167
|
if category and category in category_suggestions:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
)
|
|
168
|
+
category_data = category_suggestions[category]
|
|
169
|
+
|
|
170
|
+
# Check if there's an action-specific override
|
|
171
|
+
action_overrides = category_data.get("action_overrides", {})
|
|
172
|
+
if action in action_overrides:
|
|
173
|
+
override = action_overrides[action]
|
|
174
|
+
return (override["suggestion"], override["example"])
|
|
175
|
+
|
|
176
|
+
# TIER 2: Fall back to category-level defaults
|
|
177
|
+
return (category_data["suggestion"], category_data["example"])
|
|
76
178
|
|
|
77
|
-
# Generic ABAC
|
|
179
|
+
# Ultimate fallback: Generic ABAC guidance for uncategorized actions
|
|
78
180
|
return (
|
|
79
181
|
"Add IAM conditions to limit when this action can be used. Use ABAC for scalability:\n"
|
|
80
182
|
"• Match principal tags to resource tags (`aws:PrincipalTag/<tag-name>` = `aws:ResourceTag/<tag-name>`)\n"
|
|
@@ -114,6 +216,16 @@ class SensitiveActionCheck(PolicyCheck):
|
|
|
114
216
|
)
|
|
115
217
|
|
|
116
218
|
if is_sensitive and not has_conditions:
|
|
219
|
+
# Filter out actions already covered by action_condition_enforcement
|
|
220
|
+
# This prevents duplicate warnings with different messages
|
|
221
|
+
covered_actions = self._get_actions_covered_by_condition_enforcement(config)
|
|
222
|
+
matched_actions = [
|
|
223
|
+
action for action in matched_actions if action not in covered_actions
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
# If all matched actions are covered elsewhere, skip this check
|
|
227
|
+
if not matched_actions:
|
|
228
|
+
return issues
|
|
117
229
|
# Create appropriate message based on matched actions using configurable templates
|
|
118
230
|
if len(matched_actions) == 1:
|
|
119
231
|
message_template = config.config.get(
|
|
@@ -49,6 +49,7 @@ def check_policy_level_actions(
|
|
|
49
49
|
all_actions,
|
|
50
50
|
statement_map,
|
|
51
51
|
item["all_of"],
|
|
52
|
+
item, # Pass the entire item config (includes severity, message, suggestion)
|
|
52
53
|
check_config,
|
|
53
54
|
check_type,
|
|
54
55
|
get_severity_func,
|
|
@@ -62,6 +63,7 @@ def check_policy_level_actions(
|
|
|
62
63
|
all_actions,
|
|
63
64
|
statement_map,
|
|
64
65
|
config["all_of"],
|
|
66
|
+
config, # Pass the entire config dict (includes severity, message, suggestion)
|
|
65
67
|
check_config,
|
|
66
68
|
check_type,
|
|
67
69
|
get_severity_func,
|
|
@@ -76,6 +78,7 @@ def _check_all_of_pattern(
|
|
|
76
78
|
all_actions: list[str],
|
|
77
79
|
statement_map: dict[str, list[tuple[int, str | None]]],
|
|
78
80
|
required_actions: list[str],
|
|
81
|
+
item_config: dict,
|
|
79
82
|
check_config: CheckConfig,
|
|
80
83
|
check_type: str,
|
|
81
84
|
get_severity_func,
|
|
@@ -87,6 +90,7 @@ def _check_all_of_pattern(
|
|
|
87
90
|
all_actions: All actions across the entire policy
|
|
88
91
|
statement_map: Mapping of action -> [(statement_idx, sid), ...]
|
|
89
92
|
required_actions: List of required actions or patterns
|
|
93
|
+
item_config: Configuration for this specific pattern (includes severity, message, suggestion)
|
|
90
94
|
check_config: Full check configuration
|
|
91
95
|
check_type: Either "actions" (exact match) or "patterns" (regex match)
|
|
92
96
|
get_severity_func: Function to get severity for the check
|
|
@@ -113,31 +117,64 @@ def _check_all_of_pattern(
|
|
|
113
117
|
# Check if ALL required actions/patterns are present
|
|
114
118
|
if len(matched_actions) >= len(required_actions):
|
|
115
119
|
# Privilege escalation detected!
|
|
116
|
-
severity
|
|
120
|
+
# Use severity from item_config if available, otherwise use default from check
|
|
121
|
+
severity = item_config.get("severity") or get_severity_func(check_config)
|
|
117
122
|
|
|
118
123
|
# Collect which statements these actions appear in
|
|
119
124
|
statement_refs = []
|
|
125
|
+
action_to_statements = {} # Map action -> list of statement references
|
|
126
|
+
|
|
120
127
|
for action in matched_actions:
|
|
128
|
+
action_to_statements[action] = []
|
|
121
129
|
if action in statement_map:
|
|
122
130
|
for stmt_idx, sid in statement_map[action]:
|
|
123
|
-
|
|
131
|
+
# Use index notation instead of # to avoid GitHub PR link interpretation
|
|
132
|
+
sid_str = f"'{sid}'" if sid else f"[{stmt_idx}]"
|
|
124
133
|
statement_refs.append(f"Statement {sid_str}: {action}")
|
|
134
|
+
action_to_statements[action].append(f"Statement {sid_str}")
|
|
125
135
|
|
|
126
|
-
|
|
136
|
+
# Format actions with backticks and statement references
|
|
137
|
+
action_list = "`, `".join(matched_actions)
|
|
127
138
|
stmt_details = "\n - ".join(statement_refs)
|
|
128
139
|
|
|
140
|
+
# Build a compact statement summary for the message
|
|
141
|
+
action_stmt_summary = []
|
|
142
|
+
for action in matched_actions:
|
|
143
|
+
stmts = action_to_statements.get(action, [])
|
|
144
|
+
if stmts:
|
|
145
|
+
action_stmt_summary.append(f"`{action}` in {', '.join(stmts)}")
|
|
146
|
+
|
|
147
|
+
stmt_summary = "; ".join(action_stmt_summary)
|
|
148
|
+
|
|
149
|
+
# Use custom message if provided in item_config, otherwise use default
|
|
150
|
+
# Support {actions} and {statements} placeholders in custom messages
|
|
151
|
+
message_template = item_config.get(
|
|
152
|
+
"message",
|
|
153
|
+
f"Policy grants [`{action_list}`] across statements - enables privilege escalation. Found: {stmt_summary}",
|
|
154
|
+
)
|
|
155
|
+
# Replace placeholders if present in custom message
|
|
156
|
+
message = message_template.replace("{actions}", f"`{action_list}`").replace(
|
|
157
|
+
"{statements}", stmt_summary
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Use custom suggestion if provided in item_config, otherwise use default
|
|
161
|
+
suggestion = item_config.get(
|
|
162
|
+
"suggestion",
|
|
163
|
+
f"These actions combined allow privilege escalation. Consider:\n"
|
|
164
|
+
f" 1. Splitting into separate policies for different users/roles\n"
|
|
165
|
+
f" 2. Adding strict conditions to limit when these actions can be used together\n"
|
|
166
|
+
f" 3. Reviewing if all these permissions are truly necessary\n\n"
|
|
167
|
+
f"Actions found in:\n - {stmt_details}",
|
|
168
|
+
)
|
|
169
|
+
|
|
129
170
|
return ValidationIssue(
|
|
130
171
|
severity=severity,
|
|
131
172
|
statement_sid=None, # Policy-level issue
|
|
132
173
|
statement_index=-1, # -1 indicates policy-level issue
|
|
133
174
|
issue_type="privilege_escalation",
|
|
134
|
-
message=
|
|
135
|
-
suggestion=
|
|
136
|
-
|
|
137
|
-
f" 2. Adding strict conditions to limit when these actions can be used together\n"
|
|
138
|
-
f" 3. Reviewing if all these permissions are truly necessary\n\n"
|
|
139
|
-
f"Actions found in:\n - {stmt_details}",
|
|
140
|
-
line_number=None,
|
|
175
|
+
message=message,
|
|
176
|
+
suggestion=suggestion,
|
|
177
|
+
line_number=1, # Policy-level issues point to line 1 (top of policy)
|
|
141
178
|
)
|
|
142
179
|
|
|
143
180
|
return None
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
from .analyze import AnalyzeCommand
|
|
4
4
|
from .cache import CacheCommand
|
|
5
|
+
from .completion import CompletionCommand
|
|
5
6
|
from .download_services import DownloadServicesCommand
|
|
6
7
|
from .post_to_pr import PostToPRCommand
|
|
8
|
+
from .query import QueryCommand
|
|
7
9
|
from .validate import ValidateCommand
|
|
8
10
|
|
|
9
11
|
# All available commands
|
|
@@ -13,6 +15,8 @@ ALL_COMMANDS = [
|
|
|
13
15
|
AnalyzeCommand(),
|
|
14
16
|
CacheCommand(),
|
|
15
17
|
DownloadServicesCommand(),
|
|
18
|
+
QueryCommand(),
|
|
19
|
+
CompletionCommand(),
|
|
16
20
|
]
|
|
17
21
|
|
|
18
22
|
__all__ = [
|
|
@@ -21,5 +25,7 @@ __all__ = [
|
|
|
21
25
|
"AnalyzeCommand",
|
|
22
26
|
"CacheCommand",
|
|
23
27
|
"DownloadServicesCommand",
|
|
28
|
+
"QueryCommand",
|
|
29
|
+
"CompletionCommand",
|
|
24
30
|
"ALL_COMMANDS",
|
|
25
31
|
]
|