iam-policy-validator 1.10.2__py3-none-any.whl → 1.11.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. iam_policy_validator-1.11.0.dist-info/METADATA +782 -0
  2. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/RECORD +26 -22
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/action_condition_enforcement.py +27 -14
  5. iam_validator/checks/sensitive_action.py +123 -11
  6. iam_validator/checks/utils/policy_level_checks.py +47 -10
  7. iam_validator/checks/wildcard_resource.py +29 -7
  8. iam_validator/commands/__init__.py +6 -0
  9. iam_validator/commands/completion.py +420 -0
  10. iam_validator/commands/query.py +485 -0
  11. iam_validator/commands/validate.py +21 -26
  12. iam_validator/core/config/category_suggestions.py +77 -0
  13. iam_validator/core/config/condition_requirements.py +105 -54
  14. iam_validator/core/config/defaults.py +110 -6
  15. iam_validator/core/config/wildcards.py +3 -0
  16. iam_validator/core/diff_parser.py +321 -0
  17. iam_validator/core/formatters/enhanced.py +34 -27
  18. iam_validator/core/models.py +2 -0
  19. iam_validator/core/pr_commenter.py +179 -51
  20. iam_validator/core/report.py +19 -17
  21. iam_validator/integrations/github_integration.py +250 -1
  22. iam_validator/sdk/__init__.py +33 -0
  23. iam_validator/sdk/query_utils.py +454 -0
  24. iam_policy_validator-1.10.2.dist-info/METADATA +0 -549
  25. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/WHEEL +0 -0
  26. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.dist-info}/entry_points.txt +0 -0
  27. {iam_policy_validator-1.10.2.dist-info → iam_policy_validator-1.11.0.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=Fz08JonmD1dXoibF83ZYkSo1e0IWJO9aJ4iIAfq64mI,374
3
+ iam_validator/__version__.py,sha256=QDfo5_PdJfgisRdaZ8ltIgqPu_z6ugxj636N7IepezA,374
4
4
  iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
5
- iam_validator/checks/action_condition_enforcement.py,sha256=0dCH_xX-Xc0uLxtNeRjrpNjWYbdWQRzO1XNcLTSn6sI,51698
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,24 +14,26 @@ 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=tKvZYjZvpRqRyS-JE1R8BaT3ecahKgghSsIZ9kwxahs,9799
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
21
21
  iam_validator/checks/trust_policy_validation.py,sha256=a8Sm2xu3gFOHLd7rXDl-ibqiLEmg5c-dyWv1lK2i6HA,17816
22
22
  iam_validator/checks/wildcard_action.py,sha256=CyURgURDt2fQT2468LK813RupQ3WWvpmvLVLjUZf9QQ,1960
23
- iam_validator/checks/wildcard_resource.py,sha256=AidyyKMQL3PxLI6Zd-iFiiI6BnvSle4ATLwDXUmV3jQ,5404
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=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
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=M-5bo8w0TCWydK0cXgJyPD2fmk8bpQs-3b26YbgLzlc,565
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=oPUZkY0U3x-DU3bNB0UmdpXxxKibTBB8_TDPcQLWc10,15175
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/validate.py,sha256=cvrgYagYm7W29MYsitZsLcttIIqVKQMRm-bCGY7N3fU,24355
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=yQ5iBTffdAzx88h8RyVCCmBg6kkD2zg5_lb-qLdjy3w,13386
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=NTKoSmjvspYX2rbl3Xn8d611XkTNSfYlGUY0zBHBP4g,16801
49
- iam_validator/core/report.py,sha256=kzSeWnT1LqWZVA5pqKKz-maVowXVj0djdoShfRhhpz4,35899
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=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
62
- iam_validator/core/config/condition_requirements.py,sha256=qauIP73HFnOw1dchUeFpg1x7Y7QWkILo3GfxV_dxdQo,7696
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=qpFP534dgCQ-vjCdhkK7ZslDoTm9Ftgy20qmYZsSYUI,28637
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=H_v6hb-rZ0UUz4cul9lxkVI39e6knaK4Y-MbWz2Ebpw,3228
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=TVtkcTIow8NGoLhG45-5ms-_PTxyxMcAHxf_uPMyKAc,18155
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=EnrolMq3uZbKWPxUMhYnqcKAfic6Fb8qJzieDruKqsc,26485
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=5I-PCrEbORm1cmNkN9J8-61u9XLHftQ3xuBi_JGePKc,5306
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.10.2.dist-info/METADATA,sha256=t5vzMawKVACC8uFWNJznjMBvw4xX9T7z1EduP8HbAQA,19070
93
- iam_policy_validator-1.10.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
- iam_policy_validator-1.10.2.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
95
- iam_policy_validator-1.10.2.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
96
- iam_policy_validator-1.10.2.dist-info/RECORD,,
96
+ iam_policy_validator-1.11.0.dist-info/METADATA,sha256=OL4Pb02Cgtv4XL1AXoupb1KYc5fUMXUNfiqquaFyn24,34456
97
+ iam_policy_validator-1.11.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
98
+ iam_policy_validator-1.11.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
99
+ iam_policy_validator-1.11.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
100
+ iam_policy_validator-1.11.0.dist-info/RECORD,,
@@ -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.10.2"
6
+ __version__ = "1.11.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("."))
@@ -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 both old (policy_level_requirements) and new (action_condition_requirements) keys
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
- "action_condition_requirements",
190
- config.config.get("policy_level_requirements", []),
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
- if isinstance(actions_config, list) and actions_config:
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 if wildcard overlaps with patterns
705
- if not matched and "*" in stmt_action:
706
- # For wildcards, also check pattern overlap directly
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
- if re.match(f"^{wildcard_pattern}$", statement_action):
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
- if re.match(f"^{stmt_wildcard_pattern}$", required_action):
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
- if re.match(pattern, full_granted_action):
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
- if re.match(pattern, stmt_prefix):
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
- if re.match(pattern, statement_action):
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 `{matching_actions_formatted}` require at least ONE of these conditions: "
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
- return (
73
- category_suggestions[category]["suggestion"],
74
- category_suggestions[category]["example"],
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 fallback for uncategorized actions
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 = get_severity_func(check_config, "sensitive_action_check", "error")
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
- sid_str = f"'{sid}'" if sid else f"#{stmt_idx}"
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
- action_list = "', '".join(matched_actions)
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=f"Policy-level privilege escalation detected: grants all of ['{action_list}'] across multiple statements",
135
- suggestion=f"These actions combined allow privilege escalation. Consider:\n"
136
- f" 1. Splitting into separate policies for different users/roles\n"
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
@@ -39,22 +39,44 @@ class WildcardResourceCheck(PolicyCheck):
39
39
  # to all matching AWS actions using the AWS API, then checking if the policy's
40
40
  # actions are in that expanded list. This ensures only validated AWS actions
41
41
  # are allowed with Resource: "*".
42
+ allowed_wildcards_config = config.config.get("allowed_wildcards", [])
42
43
  allowed_wildcards_expanded = await self._get_expanded_allowed_wildcards(config, fetcher)
43
44
 
44
45
  # Check if ALL actions (excluding full wildcard "*") are in the expanded list
45
46
  non_wildcard_actions = [a for a in actions if a != "*"]
46
47
 
47
- if allowed_wildcards_expanded and non_wildcard_actions:
48
- # Check if all actions are in the expanded allowed list (exact match)
49
- all_actions_allowed = all(
50
- action in allowed_wildcards_expanded for action in non_wildcard_actions
48
+ if (allowed_wildcards_config or allowed_wildcards_expanded) and non_wildcard_actions:
49
+ # Strategy 1: Check literal pattern match (fast path)
50
+ # If policy action matches config pattern literally, allow it
51
+ # Example: Policy has "iam:Get*", config has "iam:Get*" -> match
52
+ all_actions_allowed_literal = all(
53
+ action in allowed_wildcards_config for action in non_wildcard_actions
51
54
  )
52
55
 
53
- # If all actions are in the expanded list, skip the wildcard resource warning
54
- if all_actions_allowed:
55
- # All actions are safe, Resource: "*" is acceptable
56
+ if all_actions_allowed_literal:
57
+ # All actions match literally, Resource: "*" is acceptable
56
58
  return issues
57
59
 
60
+ # Strategy 2: Check expanded pattern match (comprehensive path)
61
+ # Expand both policy actions and config patterns, then compare
62
+ # Example: Policy has "iam:Get*" -> ["iam:GetUser", ...],
63
+ # config has "iam:Get*" -> ["iam:GetUser", ...] -> all match
64
+ if allowed_wildcards_expanded:
65
+ expanded_statement_actions = await expand_wildcard_actions(
66
+ non_wildcard_actions, fetcher
67
+ )
68
+
69
+ # Check if all expanded actions are in the expanded allowed list (exact match)
70
+ all_actions_allowed_expanded = all(
71
+ action in allowed_wildcards_expanded
72
+ for action in expanded_statement_actions
73
+ )
74
+
75
+ # If all actions are in the expanded list, skip the wildcard resource warning
76
+ if all_actions_allowed_expanded:
77
+ # All actions are safe, Resource: "*" is acceptable
78
+ return issues
79
+
58
80
  # Flag the issue if actions are not all allowed or no allowed_wildcards configured
59
81
  message = config.config.get(
60
82
  "message", 'Statement applies to all resources `"*"` (wildcard resource).'
@@ -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
  ]