iam-policy-validator 1.7.1__py3-none-any.whl → 1.7.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/METADATA +1 -2
  2. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/RECORD +34 -33
  3. iam_validator/__version__.py +4 -2
  4. iam_validator/checks/action_condition_enforcement.py +20 -13
  5. iam_validator/checks/action_resource_matching.py +70 -36
  6. iam_validator/checks/condition_key_validation.py +7 -7
  7. iam_validator/checks/condition_type_mismatch.py +8 -6
  8. iam_validator/checks/full_wildcard.py +2 -8
  9. iam_validator/checks/mfa_condition_check.py +8 -8
  10. iam_validator/checks/principal_validation.py +24 -20
  11. iam_validator/checks/sensitive_action.py +3 -9
  12. iam_validator/checks/service_wildcard.py +2 -8
  13. iam_validator/checks/sid_uniqueness.py +1 -1
  14. iam_validator/checks/wildcard_action.py +2 -8
  15. iam_validator/checks/wildcard_resource.py +2 -8
  16. iam_validator/commands/validate.py +2 -2
  17. iam_validator/core/aws_fetcher.py +115 -22
  18. iam_validator/core/config/config_loader.py +1 -2
  19. iam_validator/core/config/defaults.py +16 -7
  20. iam_validator/core/constants.py +57 -0
  21. iam_validator/core/formatters/console.py +10 -1
  22. iam_validator/core/formatters/csv.py +2 -1
  23. iam_validator/core/formatters/enhanced.py +42 -8
  24. iam_validator/core/formatters/markdown.py +2 -1
  25. iam_validator/core/models.py +22 -7
  26. iam_validator/core/policy_checks.py +5 -4
  27. iam_validator/core/policy_loader.py +71 -14
  28. iam_validator/core/report.py +65 -24
  29. iam_validator/integrations/github_integration.py +4 -5
  30. iam_validator/utils/__init__.py +4 -0
  31. iam_validator/utils/terminal.py +22 -0
  32. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/WHEEL +0 -0
  33. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.dist-info}/entry_points.txt +0 -0
  34. {iam_policy_validator-1.7.1.dist-info → iam_policy_validator-1.7.2.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.7.1
3
+ Version: 1.7.2
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
@@ -426,4 +426,3 @@ MIT License - see [LICENSE](LICENSE) file for details.
426
426
  ## 🆘 Support
427
427
 
428
428
  - **Issues**: [GitHub Issues](https://github.com/boogy/iam-policy-validator/issues)
429
- - **Discussions**: [GitHub Discussions](https://github.com/boogy/iam-policy-validator/discussions)
@@ -1,24 +1,24 @@
1
1
  iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=hhtguX-fvQWNuUzOdKMFXPyDTbhXhMm7VQgXxQD2xCQ,206
3
+ iam_validator/__version__.py,sha256=0niAY6KgsXeeyFV5nTmvfvem16X3OTrO_DItEqsW74A,361
4
4
  iam_validator/checks/__init__.py,sha256=eDiDlVon0CwWGSBnZgM-arn1i5R5ZSG89pgR-ifETxE,1782
5
- iam_validator/checks/action_condition_enforcement.py,sha256=n-F7NEmQm76Hs-Aj5qxgXney3MpkzbWElZUu1Ig73pw,36723
6
- iam_validator/checks/action_resource_matching.py,sha256=X9dqWy1s_-h1rA81wZRLOxAVLmUHlGVPjxMo0WKIlwM,17433
5
+ iam_validator/checks/action_condition_enforcement.py,sha256=VhFEGbkcgkRwNRRuslvat5uib2tlH2Nr6sltbAQTs6I,36834
6
+ iam_validator/checks/action_resource_matching.py,sha256=sk67jcDF1WzW4tPgcRSdTj4UBe2stALdwHx5ViVA9dU,19207
7
7
  iam_validator/checks/action_validation.py,sha256=IpxtTsk58f2zEZ-xzAoyHw4QK8BCRV43OffP-8ydf9E,2578
8
- iam_validator/checks/condition_key_validation.py,sha256=E-doe2QjvKSkyjXZO9TBp0QS7M0Fv2oYYQQ9738QNxg,3918
9
- iam_validator/checks/condition_type_mismatch.py,sha256=qAbP6pP_vM1aBvIBRHji56XLH_5cQI4cDhpMQe19CHM,10588
10
- iam_validator/checks/full_wildcard.py,sha256=0_F6h4goWlc3DuZwo1F9YGw5hvpnkfZxYSDxhXXK50I,2449
11
- iam_validator/checks/mfa_condition_check.py,sha256=s7K2r9hxlJI1KWk8qXl-JOWE6jLIhpxooK26Pr7acKs,4915
8
+ iam_validator/checks/condition_key_validation.py,sha256=10XxTwIcr887CbgmN90jfRZabj5RHo08dGa8csM50Fo,3980
9
+ iam_validator/checks/condition_type_mismatch.py,sha256=JyiAOyUZShzXZI8dgycL4oqwRkpJYUPwoGX4zigsi5I,10613
10
+ iam_validator/checks/full_wildcard.py,sha256=ywD762BOV8WxFuTTARkaGMJn27f3ZZVuZUjKo8URnTc,2281
11
+ iam_validator/checks/mfa_condition_check.py,sha256=YCBX3tFTQRmVTAed_W-Tu1b6WqD2LBYyom53P7lBjh4,4935
12
12
  iam_validator/checks/policy_size.py,sha256=ibgmrErpkz6OfUAN6bFuHe1KHzpzzra9gHwNtVAkPWc,5729
13
13
  iam_validator/checks/policy_type_validation.py,sha256=9qmrA8CXwsVpCU4rT0RrqDXgVOzNamMEpdg3cXWAtBI,15213
14
- iam_validator/checks/principal_validation.py,sha256=Bm4pH6eiJLDa9ID7UyM63phgffh-P5DpPpSBUbYyVn8,29851
14
+ iam_validator/checks/principal_validation.py,sha256=gTv_TqJDspGEX3iJkHXrw3DyKMJeyE33uQakZ0PjNoo,29969
15
15
  iam_validator/checks/resource_validation.py,sha256=fGi9QuX-lIHDtLm8xB3VReFFhbZpQ2Yub-FKRafQCkw,5984
16
- iam_validator/checks/sensitive_action.py,sha256=mdl4g67HBioYTvAvar9CaTjxfaPvpYkNo9phL4E1c1w,9794
17
- iam_validator/checks/service_wildcard.py,sha256=CiQQoti06nqVgvH-HpBIjoW23tnTJqDU4S-ZnM1DwsA,4218
16
+ iam_validator/checks/sensitive_action.py,sha256=0vuhF1UkAH_vxhfHsC8xk68aJXHvI7c9KTLcJFNlnHM,9652
17
+ iam_validator/checks/service_wildcard.py,sha256=1ynXLG6_82SIH8aHP88qQojJf38ZH0agnSmHp0VkZ98,4010
18
18
  iam_validator/checks/set_operator_validation.py,sha256=1XjOdf-xk-m6m1bODuHsELZccriGqOJTDI-HCcuId80,7464
19
- iam_validator/checks/sid_uniqueness.py,sha256=1Ux9W1hPPhzgdCzfxwxvD-nSBRo1SyrxFWlnTXDcOys,6887
20
- iam_validator/checks/wildcard_action.py,sha256=XAVuk5L9dQqiWPgd3HJXGNmYr2bh2szJMVVcHSBXb_8,2140
21
- iam_validator/checks/wildcard_resource.py,sha256=IEpyoU4mA3t2kRxSwVavtROYIyF0Bq1xZJAL9P7XbVQ,5582
19
+ iam_validator/checks/sid_uniqueness.py,sha256=yWNHyy002aIHxJKtHeYpYds7bKgreL0BvQmRkI2UwvQ,6891
20
+ iam_validator/checks/wildcard_action.py,sha256=f1QZ68eHzQwCTeYY_9UiYaMxUaq7XYia6DaBjIspZ2A,1972
21
+ iam_validator/checks/wildcard_resource.py,sha256=GNpbk7WDExHG6Yqu4_gxeRCK6NUEL8TFjgbvaHgg7V0,5414
22
22
  iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
23
23
  iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
24
24
  iam_validator/checks/utils/sensitive_action_matcher.py,sha256=tcWK4nImpSVNia0FUsN2uLK9LM5EnzjRFtaPQLHZaLw,10667
@@ -29,42 +29,42 @@ iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gz
29
29
  iam_validator/commands/cache.py,sha256=p4ucRVuh42sbK3Lk0b610L3ofAR5TnUreF00fpO6VFg,14219
30
30
  iam_validator/commands/download_services.py,sha256=KKz3ybMLT8DQUf9aFZ0tilJ-o1b6PE8Pf1pC4K6cT8I,9175
31
31
  iam_validator/commands/post_to_pr.py,sha256=CvUXs2xvO-UhluxdfNM6F0TCWD8hDBEOiYw60fm1Dms,2363
32
- iam_validator/commands/validate.py,sha256=Eik-w613zCnX7hUHziBq4k5la3e3qJ0CO1__7aw-gBk,23554
32
+ iam_validator/commands/validate.py,sha256=2v91ogbEzKfjk2u6Y4NO0yvsCOwxi9jXoqD7acBbVTE,23624
33
33
  iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
34
34
  iam_validator/core/access_analyzer.py,sha256=8GgkR-vCkCtSxtXGywvQNBPYq-rvDLexUuLSyflq0V4,24520
35
35
  iam_validator/core/access_analyzer_report.py,sha256=O17gagknvkNMTTlq7BrLM68FjlCEm4LjIKD9oqxEbPg,24860
36
- iam_validator/core/aws_fetcher.py,sha256=cZFo5JMSoNLx1tpM6NzYr2cnq8Bvc2KQx2nJDmo69lc,36504
36
+ iam_validator/core/aws_fetcher.py,sha256=obTzxHD9pMsWo-SojSOeWyw2s2_St-LNgbmh5BGEM9c,41215
37
37
  iam_validator/core/check_registry.py,sha256=cMjtJROkZOLzXxl-mTdLYHdxyajNnOsaHGs-EeaSZ7k,21741
38
38
  iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
39
39
  iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
40
- iam_validator/core/constants.py,sha256=oblMWsjoroIhwjYgZdcyLxaATsGeR99zQwRg6h59Nlo,3145
41
- iam_validator/core/models.py,sha256=59yqvHoX3nCSJyQDmWCuEsQzNz9PNiF7um7A1wti-2w,12176
42
- iam_validator/core/policy_checks.py,sha256=Uz2yCsqRaoIja31F4ZM-39a1pHv51yZqKyWWkGUZKNY,26489
43
- iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
40
+ iam_validator/core/constants.py,sha256=H3eH0yddn5Dk-xZxJWtuvluRIpuXKYGiiteBSHPpJoI,5560
41
+ iam_validator/core/models.py,sha256=55BCSqqJiAN96SFwK3tiTy6fhu6YBL6avKo8VpCpy2A,12766
42
+ iam_validator/core/policy_checks.py,sha256=3UMLl8SQ4oJLTU1kwscvh7c7gpT5QtjITk_bJCJ_rzs,26616
43
+ iam_validator/core/policy_loader.py,sha256=HVEnaXhQwrb9WbXpu0tn8SJBvHNW9UgDO6w4zLjLsu0,16776
44
44
  iam_validator/core/pr_commenter.py,sha256=MU-t7SfdHUpSc6BDbh8_dNAbxDiG-bZBCry-jUXivAc,15066
45
- iam_validator/core/report.py,sha256=j6uWlFL6Xavl4BnpaQtQoxFOEgKEiuY0IYBq8I9DH5Q,34134
45
+ iam_validator/core/report.py,sha256=kzSeWnT1LqWZVA5pqKKz-maVowXVj0djdoShfRhhpz4,35899
46
46
  iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
47
47
  iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
48
48
  iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
49
49
  iam_validator/core/config/category_suggestions.py,sha256=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
50
50
  iam_validator/core/config/condition_requirements.py,sha256=1PuADTB9pLqh-kNUGC7kSU6LMLtXMSc003tvI7qKeAY,5170
51
- iam_validator/core/config/config_loader.py,sha256=7YkuPnroR-Up5CUTQOXIyS_b732WrzNn8o1EH9O6lyI,17730
52
- iam_validator/core/config/defaults.py,sha256=w5ievxkqki3zYr7NaREoWtVx5rTfxBpZlgoNdovcILs,27112
51
+ iam_validator/core/config/config_loader.py,sha256=qKD8aR8YAswaFf68pnYJLFNwKznvcc6lNxSQWU3i6SY,17713
52
+ iam_validator/core/config/defaults.py,sha256=mCOr_YgiRQp6fThtxrcjMtm-LPdZQbd6AS16gLzV17c,27589
53
53
  iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
54
54
  iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
55
55
  iam_validator/core/config/service_principals.py,sha256=gQSROsxUWBD6P2F9qP320UZV4lHGlsyvHSkMyy0njrU,2685
56
56
  iam_validator/core/config/wildcards.py,sha256=H_v6hb-rZ0UUz4cul9lxkVI39e6knaK4Y-MbWz2Ebpw,3228
57
57
  iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
58
58
  iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
59
- iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
60
- iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
61
- iam_validator/core/formatters/enhanced.py,sha256=S0UgYKFOgILfOqwnBC8-WFab3F1CiEko33g0nbaswtk,17085
59
+ iam_validator/core/formatters/console.py,sha256=FdTp7AzeILCWrUynSvSew8QJKGOMJaAA9_YiQJd-uco,2196
60
+ iam_validator/core/formatters/csv.py,sha256=pPqgvGh4KtD5Qm36xnMaDAavXYR6MlQhs4zbcrxT550,5941
61
+ iam_validator/core/formatters/enhanced.py,sha256=TVtkcTIow8NGoLhG45-5ms-_PTxyxMcAHxf_uPMyKAc,18155
62
62
  iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
63
63
  iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
64
- iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
64
+ iam_validator/core/formatters/markdown.py,sha256=dk4STeY-tOEZsVrlmolIEqZvWYP9JhRtygxxNA49DEE,2293
65
65
  iam_validator/core/formatters/sarif.py,sha256=O3pn7whqFq5xxk-tuoqSb2k4Fk5ai_A2SKX_ph8GLV4,10469
66
66
  iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
67
- iam_validator/integrations/github_integration.py,sha256=QoPkaxdRDQTzmHN4cKEXoGcn8BRv37JW4IvD2W5jEtc,26474
67
+ iam_validator/integrations/github_integration.py,sha256=EnrolMq3uZbKWPxUMhYnqcKAfic6Fb8qJzieDruKqsc,26485
68
68
  iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
69
69
  iam_validator/sdk/__init__.py,sha256=fRDSXAclGmCU3KDft4StL8JUcpAsdzwIRf8mVj461q0,5306
70
70
  iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urchEFDA,12778
@@ -73,11 +73,12 @@ iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP
73
73
  iam_validator/sdk/helpers.py,sha256=OVBg4xrW95LT74wXCg1LQkba9kw5RfFqeCLuTqhgL-A,5697
74
74
  iam_validator/sdk/policy_utils.py,sha256=CZS1OGSdiWsd2lsCwg0BDcUNWa61tUwgvn-P5rKqeN8,12987
75
75
  iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
76
- iam_validator/utils/__init__.py,sha256=V8u-SSdnL4a7NwF-yg9x0JRl5epKAXEs2f5RiwK2qPo,856
76
+ iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYuCs,1021
77
77
  iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
78
78
  iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
79
- iam_policy_validator-1.7.1.dist-info/METADATA,sha256=WCjnDcJ38j-LRUz2EwdT2b2lX2IOTkW3xT1SLtCxiWY,15343
80
- iam_policy_validator-1.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
81
- iam_policy_validator-1.7.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
82
- iam_policy_validator-1.7.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
83
- iam_policy_validator-1.7.1.dist-info/RECORD,,
79
+ iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
80
+ iam_policy_validator-1.7.2.dist-info/METADATA,sha256=fwySi0xxZPeiRTXyYfmp8YZPNyphy8HylBzhSXkNNG0,15244
81
+ iam_policy_validator-1.7.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
82
+ iam_policy_validator-1.7.2.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
83
+ iam_policy_validator-1.7.2.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
84
+ iam_policy_validator-1.7.2.dist-info/RECORD,,
@@ -3,5 +3,7 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.7.1"
7
- __version_info__ = tuple(int(part) for part in __version__.split("."))
6
+ __version__ = "1.7.2"
7
+ # Parse version, handling pre-release suffixes like -rc, -alpha, -beta
8
+ _version_base = __version__.split("-")[0] # Remove pre-release suffix if present
9
+ __version_info__ = tuple(int(part) for part in _version_base.split("."))
@@ -349,7 +349,10 @@ class ActionConditionEnforcementCheck(PolicyCheck):
349
349
  return issues
350
350
 
351
351
  async def _check_action_match(
352
- self, statement_actions: list[str], requirement: dict[str, Any], fetcher: AWSServiceFetcher
352
+ self,
353
+ statement_actions: list[str],
354
+ requirement: dict[str, Any],
355
+ fetcher: AWSServiceFetcher,
353
356
  ) -> tuple[bool, list[str]]:
354
357
  """
355
358
  Check if statement actions match the requirement.
@@ -766,6 +769,10 @@ class ActionConditionEnforcementCheck(PolicyCheck):
766
769
  or self.get_severity(config) # Global check severity
767
770
  )
768
771
 
772
+ suggestion_text, example_code = self._build_suggestion(
773
+ condition_key, description, example, expected_value, operator
774
+ )
775
+
769
776
  return ValidationIssue(
770
777
  severity=severity,
771
778
  statement_sid=statement.sid,
@@ -774,9 +781,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
774
781
  message=f"{message_prefix} Action(s) {matching_actions} require condition '{condition_key}'",
775
782
  action=", ".join(matching_actions),
776
783
  condition_key=condition_key,
777
- suggestion=self._build_suggestion(
778
- condition_key, description, example, expected_value, operator
779
- ),
784
+ suggestion=suggestion_text,
785
+ example=example_code,
780
786
  line_number=statement.line_number,
781
787
  )
782
788
 
@@ -787,19 +793,20 @@ class ActionConditionEnforcementCheck(PolicyCheck):
787
793
  example: str,
788
794
  expected_value: Any = None,
789
795
  operator: str = "StringEquals",
790
- ) -> str:
791
- """Build a helpful suggestion for adding the missing condition."""
792
- parts = []
796
+ ) -> tuple[str, str]:
797
+ """Build suggestion and example for adding the missing condition.
793
798
 
794
- if description:
795
- parts.append(description)
799
+ Returns:
800
+ Tuple of (suggestion_text, example_code)
801
+ """
802
+ suggestion = description if description else f"Add condition: {condition_key}"
796
803
 
797
804
  # Build example based on condition key type
798
805
  if example:
799
- parts.append(f"Example:\n```json\n{example}\n```")
806
+ example_code = example
800
807
  else:
801
808
  # Auto-generate example
802
- example_lines = ['Add to "Condition" block:', f' "{operator}": {{']
809
+ example_lines = [f' "{operator}": {{']
803
810
 
804
811
  if isinstance(expected_value, list):
805
812
  value_str = (
@@ -826,9 +833,9 @@ class ActionConditionEnforcementCheck(PolicyCheck):
826
833
  example_lines.append(f' "{condition_key}": {value_str}')
827
834
  example_lines.append(" }")
828
835
 
829
- parts.append("\n".join(example_lines))
836
+ example_code = "\n".join(example_lines)
830
837
 
831
- return ". ".join(parts) if parts else f"Add condition: {condition_key}"
838
+ return suggestion, example_code
832
839
 
833
840
  def _build_any_of_suggestion(self, any_of_conditions: list[dict[str, Any]]) -> str:
834
841
  """Build suggestion for any_of conditions."""
@@ -21,6 +21,8 @@ Example:
21
21
  This check will report: s3:GetObject requires arn:aws:s3:::mybucket/*
22
22
  """
23
23
 
24
+ import re
25
+
24
26
  from iam_validator.core.aws_fetcher import AWSServiceFetcher
25
27
  from iam_validator.core.check_registry import CheckConfig, PolicyCheck
26
28
  from iam_validator.core.models import Statement, ValidationIssue
@@ -231,18 +233,23 @@ class ActionResourceMatchingCheck(PolicyCheck):
231
233
  if reason:
232
234
  message = reason
233
235
  elif all_required_formats and len(all_required_formats) > 1:
234
- types = ", ".join(f["type"] for f in all_required_formats)
236
+ types = ", ".join(f"`{f['type']}`" for f in all_required_formats)
235
237
  message = (
236
- f"No resources match for action '{action}'. This action requires one of: {types}"
238
+ f"No resources match for action `{action}`. This action requires one of: {types}"
237
239
  )
238
240
  else:
239
241
  message = (
240
- f"No resources match for action '{action}'. "
241
- f"This action requires resource type: {required_type}"
242
+ f"No resources match for action `{action}`. "
243
+ f"This action requires resource type: `{required_type}`"
242
244
  )
243
245
 
244
246
  # Build suggestion with examples
245
- suggestion = self._get_suggestion(action, required_format, provided_resources)
247
+ suggestion = self._get_suggestion(
248
+ action=action,
249
+ required_format=required_format,
250
+ provided_resources=provided_resources,
251
+ all_required_formats=all_required_formats,
252
+ )
246
253
 
247
254
  return ValidationIssue(
248
255
  severity=self.get_severity(config),
@@ -265,6 +272,7 @@ class ActionResourceMatchingCheck(PolicyCheck):
265
272
  action: str,
266
273
  required_format: str,
267
274
  provided_resources: list[str],
275
+ all_required_formats: list[dict] | None = None,
268
276
  ) -> str:
269
277
  """
270
278
  Generate helpful suggestion for fixing the mismatch.
@@ -281,44 +289,75 @@ class ActionResourceMatchingCheck(PolicyCheck):
281
289
  # Special case: Wildcard resource
282
290
  if required_format == "*":
283
291
  return (
284
- f'Action {action} can only use Resource: "*" (wildcard).\n'
292
+ f'Action `{action}` can only use Resource: "*" (wildcard).\n'
285
293
  f" This action does not support resource-level permissions.\n"
286
294
  f' Example: "Resource": "*"'
287
295
  )
288
296
 
289
- # Extract resource type from ARN pattern
290
- # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
291
- # Examples:
292
- # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
293
- # arn:${Partition}:iam::${Account}:user/${UserName} -> user
294
- resource_type = self._extract_resource_type_from_pattern(required_format)
295
-
296
- # Build service-specific suggestion
297
+ # Build service-specific suggestion with proper markdown formatting
297
298
  suggestion_parts = []
298
299
 
299
- # Add action description
300
- suggestion_parts.append(f"Action {action} requires {resource_type} resource type.")
300
+ # If multiple resource types are valid, show all of them
301
+ if all_required_formats and len(all_required_formats) > 1:
302
+ resource_types = [fmt["type"] for fmt in all_required_formats]
303
+ suggestion_parts.append(
304
+ f"Action `{action}` requires one of these resource types: {', '.join(f'`{t}`' for t in resource_types)}"
305
+ )
306
+ suggestion_parts.append("")
301
307
 
302
- # Add expected format
303
- suggestion_parts.append(f" Expected format: {required_format}")
308
+ # Show format and example for each resource type
309
+ for fmt in all_required_formats:
310
+ resource_type = fmt["type"]
311
+ arn_format = fmt["format"]
304
312
 
305
- # Add practical example based on the pattern
306
- example = self._generate_example_arn(required_format)
307
- if example:
308
- suggestion_parts.append(f" Example: {example}")
313
+ suggestion_parts.append(
314
+ f"**Option {all_required_formats.index(fmt) + 1}: `{resource_type}` resource**"
315
+ )
316
+ suggestion_parts.append("```")
317
+ suggestion_parts.append(arn_format)
318
+ suggestion_parts.append("```")
309
319
 
310
- # Add helpful context for common patterns
311
- context = self._get_resource_context(action_name, resource_type, required_format)
312
- if context:
313
- suggestion_parts.append(f" {context}")
320
+ # Add practical example
321
+ example = self._generate_example_arn(arn_format)
322
+ if example:
323
+ suggestion_parts.append(f"Example: `{example}`")
314
324
 
315
- suggestion = "\n".join(suggestion_parts)
325
+ suggestion_parts.append("")
326
+ else:
327
+ # Single resource type - show detailed info
328
+ # Extract resource type from ARN pattern
329
+ # Pattern format: arn:${Partition}:service:${Region}:${Account}:resourceType/...
330
+ # Examples:
331
+ # arn:${Partition}:s3:::${BucketName}/${ObjectName} -> object
332
+ # arn:${Partition}:iam::${Account}:user/${UserName} -> user
333
+ resource_type = self._extract_resource_type_from_pattern(required_format)
334
+
335
+ # Add action description
336
+ suggestion_parts.append(f"Action `{action}` requires `{resource_type}` resource type.")
337
+ suggestion_parts.append("")
338
+
339
+ # Add expected format in code block
340
+ suggestion_parts.append("**Expected format:**")
341
+ suggestion_parts.append(f"```\n{required_format}\n```")
342
+
343
+ # Add practical example based on the pattern
344
+ example = self._generate_example_arn(required_format)
345
+ if example:
346
+ suggestion_parts.append("**Example:**")
347
+ suggestion_parts.append(f"```\n{example}\n```")
348
+
349
+ # Add helpful context for common patterns
350
+ context = self._get_resource_context(action_name, resource_type, required_format)
351
+ if context:
352
+ suggestion_parts.append(f"**Note:** {context}")
316
353
 
317
354
  # Add current resources to help user understand the mismatch
318
355
  if provided_resources and len(provided_resources) <= 3:
319
- current = ", ".join(provided_resources)
320
- suggestion += f"\n Current resources: {current}"
356
+ suggestion_parts.append("**Current resources:**")
357
+ for resource in provided_resources:
358
+ suggestion_parts.append(f"- `{resource}`")
321
359
 
360
+ suggestion = "\n".join(suggestion_parts)
322
361
  return suggestion
323
362
 
324
363
  def _extract_resource_type_from_pattern(self, pattern: str) -> str:
@@ -340,17 +379,14 @@ class ActionResourceMatchingCheck(PolicyCheck):
340
379
 
341
380
  # Extract resource type (part before / or entire string)
342
381
  if "/" in resource_part:
343
- resource_type = resource_part.split("/")[0]
382
+ resource_type = resource_part.split("/", maxsplit=1)[0]
344
383
  elif ":" in resource_part:
345
- resource_type = resource_part.split(":")[0]
384
+ resource_type = resource_part.split(":", maxsplit=1)[0]
346
385
  else:
347
386
  resource_type = resource_part
348
387
 
349
388
  # Remove template variables like ${...}
350
- import re
351
-
352
389
  resource_type = re.sub(r"\$\{[^}]+\}", "", resource_type)
353
-
354
390
  return resource_type.strip() or "resource"
355
391
 
356
392
  def _generate_example_arn(self, pattern: str) -> str:
@@ -359,8 +395,6 @@ class ActionResourceMatchingCheck(PolicyCheck):
359
395
 
360
396
  Converts AWS template variables to realistic examples.
361
397
  """
362
- import re
363
-
364
398
  example = pattern
365
399
 
366
400
  # Common substitutions
@@ -52,26 +52,26 @@ class ConditionKeyValidationCheck(PolicyCheck):
52
52
  continue
53
53
 
54
54
  # Validate against action and resource types
55
- is_valid, error_msg, warning_msg = await fetcher.validate_condition_key(
56
- action, condition_key, resources
57
- )
55
+ result = await fetcher.validate_condition_key(action, condition_key, resources)
58
56
 
59
- if not is_valid:
57
+ if not result.is_valid:
60
58
  issues.append(
61
59
  ValidationIssue(
62
60
  severity=self.get_severity(config),
63
61
  statement_sid=statement_sid,
64
62
  statement_index=statement_idx,
65
63
  issue_type="invalid_condition_key",
66
- message=error_msg or f"Invalid condition key: {condition_key}",
64
+ message=result.error_message
65
+ or f"Invalid condition key: {condition_key}",
67
66
  action=action,
68
67
  condition_key=condition_key,
69
68
  line_number=line_number,
69
+ suggestion=result.suggestion,
70
70
  )
71
71
  )
72
72
  # Only report once per condition key (not per action)
73
73
  break
74
- elif warning_msg and warn_on_global_keys:
74
+ elif result.warning_message and warn_on_global_keys:
75
75
  # Add warning for global condition keys with action-specific keys
76
76
  # Only if warn_on_global_condition_keys is enabled
77
77
  issues.append(
@@ -80,7 +80,7 @@ class ConditionKeyValidationCheck(PolicyCheck):
80
80
  statement_sid=statement_sid,
81
81
  statement_index=statement_idx,
82
82
  issue_type="global_condition_key_with_action_specific",
83
- message=warning_msg,
83
+ message=result.warning_message,
84
84
  action=action,
85
85
  condition_key=condition_key,
86
86
  line_number=line_number,
@@ -107,8 +107,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
107
107
  ValidationIssue(
108
108
  severity="warning",
109
109
  message=(
110
- f"Type mismatch (usable but not recommended): Operator '{operator}' expects "
111
- f"{operator_type} values, but condition key '{condition_key}' is type {key_type}. "
110
+ f"Type mismatch (usable but not recommended): Operator `{operator}` expects "
111
+ f"{operator_type} values, but condition key `{condition_key}` is type {key_type}. "
112
112
  f"Consider using an ARN-specific operator like ArnEquals or ArnLike instead."
113
113
  ),
114
114
  statement_sid=statement_sid,
@@ -123,8 +123,8 @@ class ConditionTypeMismatchCheck(PolicyCheck):
123
123
  ValidationIssue(
124
124
  severity=self.get_severity(config),
125
125
  message=(
126
- f"Type mismatch: Operator '{operator}' expects {operator_type} values, "
127
- f"but condition key '{condition_key}' is type {key_type}."
126
+ f"Type mismatch: Operator `{operator}` expects {operator_type} values, "
127
+ f"but condition key `{condition_key}` is type {key_type}."
128
128
  ),
129
129
  statement_sid=statement_sid,
130
130
  statement_index=statement_idx,
@@ -141,7 +141,7 @@ class ConditionTypeMismatchCheck(PolicyCheck):
141
141
  ValidationIssue(
142
142
  severity=self.get_severity(config),
143
143
  message=(
144
- f"Invalid value format for condition key '{condition_key}': {error_msg}"
144
+ f"Invalid value format for condition key `{condition_key}`: {error_msg}"
145
145
  ),
146
146
  statement_sid=statement_sid,
147
147
  statement_index=statement_idx,
@@ -172,7 +172,9 @@ class ConditionTypeMismatchCheck(PolicyCheck):
172
172
  Returns:
173
173
  Type string or None if not found
174
174
  """
175
- from iam_validator.core.config.aws_global_conditions import get_global_conditions
175
+ from iam_validator.core.config.aws_global_conditions import (
176
+ get_global_conditions,
177
+ )
176
178
 
177
179
  # Check if it's a global condition key
178
180
  global_conditions = get_global_conditions()
@@ -43,19 +43,12 @@ class FullWildcardCheck(PolicyCheck):
43
43
  "message",
44
44
  "Statement allows all actions on all resources - CRITICAL SECURITY RISK",
45
45
  )
46
- suggestion_text = config.config.get(
46
+ suggestion = config.config.get(
47
47
  "suggestion",
48
48
  "This grants full administrative access. Replace both wildcards with specific actions and resources to follow least-privilege principle",
49
49
  )
50
50
  example = config.config.get("example", "")
51
51
 
52
- # Combine suggestion + example
53
- suggestion = (
54
- f"{suggestion_text}\nExample:\n```json\n{example}\n```"
55
- if example
56
- else suggestion_text
57
- )
58
-
59
52
  issues.append(
60
53
  ValidationIssue(
61
54
  severity=self.get_severity(config),
@@ -64,6 +57,7 @@ class FullWildcardCheck(PolicyCheck):
64
57
  issue_type="security_risk",
65
58
  message=message,
66
59
  suggestion=suggestion,
60
+ example=example if example else None,
67
61
  line_number=statement.line_number,
68
62
  )
69
63
  )
@@ -70,11 +70,11 @@ class MFAConditionCheck(PolicyCheck):
70
70
  ValidationIssue(
71
71
  severity=self.get_severity(config),
72
72
  message=(
73
- "Dangerous MFA condition pattern detected. "
74
- 'Using {"Bool": {"aws:MultiFactorAuthPresent": "false"}} does not enforce MFA '
75
- "because aws:MultiFactorAuthPresent may not exist in the request context. "
76
- 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement, '
77
- "or use BoolIfExists in a Deny statement."
73
+ "**Dangerous MFA condition pattern detected.** "
74
+ 'Using `{"Bool": {"aws:MultiFactorAuthPresent": "false"}}` does not enforce MFA '
75
+ "because `aws:MultiFactorAuthPresent` may not exist in the request context. "
76
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement, '
77
+ "or use `BoolIfExists` in a Deny statement."
78
78
  ),
79
79
  statement_sid=statement_sid,
80
80
  statement_index=statement_idx,
@@ -97,10 +97,10 @@ class MFAConditionCheck(PolicyCheck):
97
97
  ValidationIssue(
98
98
  severity=self.get_severity(config),
99
99
  message=(
100
- "Dangerous MFA condition pattern detected. "
101
- 'Using {"Null": {"aws:MultiFactorAuthPresent": "false"}} only checks if the key exists, '
100
+ "**Dangerous MFA condition pattern detected.** "
101
+ 'Using `{"Null": {"aws:MultiFactorAuthPresent": "false"}}` only checks if the key exists, '
102
102
  "not whether MFA was actually used. This does not enforce MFA. "
103
- 'Consider using {"Bool": {"aws:MultiFactorAuthPresent": "true"}} in an Allow statement instead.'
103
+ 'Consider using `{"Bool": {"aws:MultiFactorAuthPresent": "true"}}` in an Allow statement instead.'
104
104
  ),
105
105
  statement_sid=statement_sid,
106
106
  statement_index=statement_idx,