iam-policy-validator 1.0.4__py3-none-any.whl → 1.1.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.

Potentially problematic release.


This version of iam-policy-validator might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iam-policy-validator
3
- Version: 1.0.4
3
+ Version: 1.1.0
4
4
  Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
5
5
  Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
6
6
  Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
@@ -70,7 +70,8 @@ A high-performance GitHub Action and Python CLI tool that validates AWS IAM poli
70
70
  - **Comment Updates**: Update existing comments instead of creating duplicates
71
71
 
72
72
  ### Output Formats
73
- - **Console**: Rich terminal output with colors and tables
73
+ - **Console** (default): Clean terminal output with colors and tables
74
+ - **Enhanced**: Modern visual output with progress bars, tree structure, and rich visuals
74
75
  - **JSON**: Structured format for programmatic processing
75
76
  - **Markdown**: GitHub-flavored markdown for PR comments
76
77
  - **SARIF**: GitHub code scanning integration format
@@ -87,9 +88,11 @@ A high-performance GitHub Action and Python CLI tool that validates AWS IAM poli
87
88
 
88
89
  ### As a GitHub Action (Recommended) ⭐
89
90
 
90
- The easiest way to use IAM Policy Validator is as a GitHub Action in your workflows.
91
+ The IAM Policy Validator is available as **both** a standalone GitHub Action and a Python module. Choose the approach that best fits your needs:
91
92
 
92
- #### Basic Validation
93
+ #### **Option A: Standalone GitHub Action** (Recommended - Zero Setup)
94
+
95
+ Use the published action directly - it handles all setup automatically:
93
96
 
94
97
  Create `.github/workflows/iam-policy-validator.yml`:
95
98
 
@@ -121,7 +124,13 @@ jobs:
121
124
  fail-on-warnings: true
122
125
  ```
123
126
 
124
- #### With AWS Access Analyzer
127
+ **Benefits:**
128
+ - ✅ Zero setup - action handles Python, uv, and dependencies
129
+ - ✅ Automatic dependency caching
130
+ - ✅ Simple, declarative configuration
131
+ - ✅ Perfect for CI/CD workflows
132
+
133
+ #### With AWS Access Analyzer (Standalone Action)
125
134
 
126
135
  Use AWS's official policy validation service:
127
136
 
@@ -162,7 +171,61 @@ jobs:
162
171
  fail-on-warnings: true
163
172
  ```
164
173
 
165
- #### Custom Policy Checks
174
+ #### **Option B: As Python Module/CLI Tool**
175
+
176
+ For advanced use cases or when you need more control:
177
+
178
+ ```yaml
179
+ name: IAM Policy Validation (CLI)
180
+
181
+ on:
182
+ pull_request:
183
+ paths:
184
+ - 'policies/**/*.json'
185
+
186
+ jobs:
187
+ validate:
188
+ runs-on: ubuntu-latest
189
+ permissions:
190
+ contents: read
191
+ pull-requests: write
192
+
193
+ steps:
194
+ - name: Checkout code
195
+ uses: actions/checkout@v5
196
+
197
+ - name: Set up Python
198
+ uses: actions/setup-python@v5
199
+ with:
200
+ python-version: '3.13'
201
+
202
+ - name: Install uv
203
+ uses: astral-sh/setup-uv@v3
204
+
205
+ - name: Install dependencies
206
+ run: uv sync
207
+
208
+ - name: Validate IAM Policies
209
+ env:
210
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
211
+ GITHUB_REPOSITORY: ${{ github.repository }}
212
+ GITHUB_PR_NUMBER: ${{ github.event.pull_request.number }}
213
+ run: |
214
+ uv run iam-validator validate \
215
+ --path ./policies/ \
216
+ --github-comment \
217
+ --github-review \
218
+ --fail-on-warnings \
219
+ --log-level info
220
+ ```
221
+
222
+ **Use this when you need:**
223
+ - Advanced CLI options (e.g., `--log-level`, `--custom-checks-dir`, `--stream`)
224
+ - Full control over the Python environment
225
+ - Integration with existing Python workflows
226
+ - Multiple validation commands in sequence
227
+
228
+ #### Custom Policy Checks (Standalone Action)
166
229
 
167
230
  Enforce specific security requirements:
168
231
 
@@ -231,7 +294,22 @@ jobs:
231
294
  fail-on-warnings: true
232
295
  ```
233
296
 
234
- #### Multiple Paths
297
+ ---
298
+
299
+ ### Choosing the Right Approach
300
+
301
+ | Feature | Standalone Action | Python Module/CLI |
302
+ | --------------------- | ------------------------ | ------------------------------------------------------------------------ |
303
+ | Setup Required | None - fully automated | Manual (Python, uv, dependencies) |
304
+ | Configuration | YAML inputs | CLI arguments |
305
+ | Advanced Options | Limited to action inputs | Full CLI access (`--log-level`, `--custom-checks-dir`, `--stream`, etc.) |
306
+ | Custom Checks | Via config file only | Via config file or `--custom-checks-dir` |
307
+ | Best For | CI/CD, simple workflows | Development, advanced workflows, testing |
308
+ | Dependency Management | Automatic | Manual |
309
+
310
+ **Recommendation:** Use the **Standalone Action** for production CI/CD workflows, and the **Python Module/CLI** for development, testing, or when you need advanced features.
311
+
312
+ #### Multiple Paths (Standalone Action)
235
313
 
236
314
  Validate policies across multiple directories:
237
315
 
@@ -305,7 +383,7 @@ action_condition_enforcement_check:
305
383
  - condition_key: "iam:PassedToService"
306
384
  ```
307
385
 
308
- See [iam-validator.yaml](iam-validator.yaml) for a complete configuration example.
386
+ See [default-config.yaml](default-config.yaml) for a complete configuration example.
309
387
 
310
388
  ### GitHub Action Inputs
311
389
 
@@ -316,12 +394,12 @@ See [iam-validator.yaml](iam-validator.yaml) for a complete configuration exampl
316
394
  | `fail-on-warnings` | Fail validation if warnings are found | No | `false` |
317
395
  | `post-comment` | Post validation results as PR comment | No | `true` |
318
396
  | `create-review` | Create line-specific review comments on PR | No | `true` |
319
- | `format` | Output format (console, json, markdown, sarif, csv, html) | No | `console` |
397
+ | `format` | Output format (console, enhanced, json, markdown, sarif, csv, html) | No | `console` |
320
398
  | `output-file` | Path to save output file | No | "" |
321
399
  | `recursive` | Recursively search directories for policy files | No | `true` |
322
400
  | `use-access-analyzer` | Use AWS IAM Access Analyzer for validation | No | `false` |
323
401
  | `access-analyzer-region` | AWS region for Access Analyzer | No | `us-east-1` |
324
- | `policy-type` | Policy type (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY) | No | `RESOURCE_POLICY` |
402
+ | `policy-type` | Policy type (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY) | No | `IDENTITY_POLICY` |
325
403
  | `run-all-checks` | Run custom checks after Access Analyzer | No | `false` |
326
404
  | `check-access-not-granted` | Actions that should NOT be granted (space-separated) | No | "" |
327
405
  | `check-access-resources` | Resources to check with check-access-not-granted | No | "" |
@@ -2,18 +2,18 @@ iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
3
  iam_validator/__version__.py,sha256=YOIURWR5ocuvaQTQgwFi1XjHm_ifJDzicMOQSJZqmZc,206
4
4
  iam_validator/checks/__init__.py,sha256=q-_rIYGZJMjsiHK-R_3CbSUCBVGN5e137LPNDnMRZmw,841
5
- iam_validator/checks/action_condition_enforcement.py,sha256=3V8Wnz6BYnataKzuFMx8fHukVjzpIZaVfde9-RqZjPc,25357
5
+ iam_validator/checks/action_condition_enforcement.py,sha256=3M1Wj89Af6H-ywBTruZbJPzhCBBQVanVb5hwv-fkiDE,29721
6
6
  iam_validator/checks/action_validation.py,sha256=KbUw1SV-2nN-HtLlj3zrE6sdd0z8iAF0ubqz35Vwb7c,6921
7
7
  iam_validator/checks/condition_key_validation.py,sha256=bc4LQ8IRKyt0RquaQvQvVjmeJnuOUAFRL8xdduLPa_U,2661
8
8
  iam_validator/checks/policy_size.py,sha256=4cvZiWRJXGuvYo8PRcdD1Py_ZL8Xw0lOJfXTs6EX-_I,5753
9
9
  iam_validator/checks/resource_validation.py,sha256=AEIoiR6AKYLuVaA8ne3QE5qy6NCMDe98_2JAiwE9-JU,4261
10
- iam_validator/checks/security_best_practices.py,sha256=Sr3aiLbki8_M3U9qJv7u0fM__GjJRfZzWmJVgJ3ODSw,28185
10
+ iam_validator/checks/security_best_practices.py,sha256=OCAtbsO9HEK97DVPPnm-hJDQtf-ATlnwa1LLshweZDk,32045
11
11
  iam_validator/checks/sid_uniqueness.py,sha256=7S8RtVJgYPTKgr7gSEmxgT0oIGkSoXN6iu0ALHbcSfw,5015
12
12
  iam_validator/commands/__init__.py,sha256=zuhECuz-1Us5hBNAJtdMae8LaYn1eNeoYPBmQPMwI94,357
13
13
  iam_validator/commands/analyze.py,sha256=TWlDaZ8gVOdNv6__KQQfzeLVW36qLiL5IzlhGYfvq_g,16501
14
14
  iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
15
15
  iam_validator/commands/post_to_pr.py,sha256=hl_K-XlELYN-ArjMdgQqysvIE-26yf9XdrMl4ToDwG0,2148
16
- iam_validator/commands/validate.py,sha256=zdfay29HX1v4uz_LfzUUgC4-VUy8TTg5CGfZlAeEWXc,13779
16
+ iam_validator/commands/validate.py,sha256=R295cOTly8n7zL1jfvbh9RuCgiM5edBqbf6YMn_4G9A,14013
17
17
  iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
18
18
  iam_validator/core/access_analyzer.py,sha256=poeT1i74jXpKr1B3UmvqiTvCTbq82zffWgZHwiFUwoo,24337
19
19
  iam_validator/core/access_analyzer_report.py,sha256=iTIFKul6zQZd2qBg8V6zaDNPMKF8D_XDSX6pJwXFVYY,24791
@@ -21,25 +21,27 @@ iam_validator/core/aws_fetcher.py,sha256=fUCIMItIWmrbgsoVCz_9Oe5k3SjuXBlNBVwQ60I
21
21
  iam_validator/core/aws_global_conditions.py,sha256=ADVcMEWhgvDZWdBmRUQN3HB7a9OycbTLecXFAy3LPbo,5837
22
22
  iam_validator/core/check_registry.py,sha256=wXg4Yw5LJ-rAVLiPUIJOtw8Y49Q1PY00Zbu37LzyjHY,15477
23
23
  iam_validator/core/cli.py,sha256=5UHsHS8o7Fkag4d6MNaaqjCFSGu8evCIbtpa81591lE,3831
24
- iam_validator/core/config_loader.py,sha256=k4D5TT_D6B9N8BbIEg0nE3wUXu0naFLHOVVJsjYZzh4,14880
25
- iam_validator/core/models.py,sha256=SUEbxDUtkX1uvgMy6-LPzomyGu82PTpXdDXZ9RKqfTY,9655
24
+ iam_validator/core/config_loader.py,sha256=6Px_JEzk8WU6g8KXaSLme3x8qZXT9oppxUVXYyDkboY,16425
25
+ iam_validator/core/defaults.py,sha256=JzuDYNQGERDtX9S8E5grr4KZmooSephJW8KRplT34L8,10956
26
+ iam_validator/core/models.py,sha256=rWIZnD-I81Sg4asgOhnB10FWJC5mxQ2JO9bdS0sHb4Q,10772
26
27
  iam_validator/core/policy_checks.py,sha256=xK5CntsEKVDgN27uIdQ92jCL97t7eBqOk0SChWU9cgw,23872
27
28
  iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
28
29
  iam_validator/core/pr_commenter.py,sha256=TOhVXKTFcRHQ9EVuShXQcKXn9aNjB1mU6FnR2jvltmw,10581
29
- iam_validator/core/report.py,sha256=6k2A82EuhI72y-xCXbxRbykYcBvUP0z977pjUk9w1Cc,28977
30
- iam_validator/core/formatters/__init__.py,sha256=ggIKrI_Uu6tnKBLnUbBaXgaIXeL-JAGwUOrfSql6sow,774
30
+ iam_validator/core/report.py,sha256=aRM1mYlDjkPmQrUZtcq2akbviLPVTSa8q_mH7XyuuK4,32897
31
+ iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
31
32
  iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
32
- iam_validator/core/formatters/console.py,sha256=qkKWcxETRWDr62Zmkz0NXG4oS9hj2LwDX8uH4MoSX0s,750
33
- iam_validator/core/formatters/csv.py,sha256=hyop9gddZc1eOjUvd1YJjxbo8FNlLG7Rvh6UkSCEAhU,5565
34
- iam_validator/core/formatters/html.py,sha256=kW0BVTTX8PbiEbct8mXIyqTiO6jdsGjyK6Y3NNVeRaI,19317
33
+ iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
34
+ iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
35
+ iam_validator/core/formatters/enhanced.py,sha256=6hRiATRpH6Osqf_y6C58VN48L47AXYP3Dm9R90VMGs8,16432
36
+ iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
35
37
  iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
36
- iam_validator/core/formatters/markdown.py,sha256=FccevVlD_mC6wtrWsya2Uo-NEphyuWsZyFar9x_ZT2g,1859
38
+ iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
37
39
  iam_validator/core/formatters/sarif.py,sha256=tqp8g7RmUh0HRk-kKDaucx4sa-5I9ikgkSpy1MM8Vi4,7200
38
40
  iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
39
41
  iam_validator/integrations/github_integration.py,sha256=bKs94vNT4PmcmUPUeuY2WJFhCYpUY2SWiBP1vj-andA,25673
40
42
  iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
41
- iam_policy_validator-1.0.4.dist-info/METADATA,sha256=QQAQsAQCDiPen37apynaUzYjOUmiTpn8NYgrM6C7l0E,22070
42
- iam_policy_validator-1.0.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
43
- iam_policy_validator-1.0.4.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
44
- iam_policy_validator-1.0.4.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
45
- iam_policy_validator-1.0.4.dist-info/RECORD,,
43
+ iam_policy_validator-1.1.0.dist-info/METADATA,sha256=a_cegBs1dAS4y_ByXskcvgXY3CNsqC-7frERfdQR27k,25144
44
+ iam_policy_validator-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
45
+ iam_policy_validator-1.1.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
46
+ iam_policy_validator-1.1.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
47
+ iam_policy_validator-1.1.0.dist-info/RECORD,,
@@ -139,8 +139,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
139
139
  # Check each requirement rule
140
140
  for requirement in action_condition_requirements:
141
141
  # Check if this requirement applies to the statement's actions
142
- actions_match, matching_actions = self._check_action_match(
143
- statement_actions, requirement
142
+ actions_match, matching_actions = await self._check_action_match(
143
+ statement_actions, requirement, fetcher
144
144
  )
145
145
 
146
146
  if not actions_match:
@@ -186,8 +186,8 @@ class ActionConditionEnforcementCheck(PolicyCheck):
186
186
 
187
187
  return issues
188
188
 
189
- def _check_action_match(
190
- self, statement_actions: list[str], requirement: dict[str, Any]
189
+ async def _check_action_match(
190
+ self, statement_actions: list[str], requirement: dict[str, Any], fetcher: AWSServiceFetcher
191
191
  ) -> tuple[bool, list[str]]:
192
192
  """
193
193
  Check if statement actions match the requirement.
@@ -208,18 +208,25 @@ class ActionConditionEnforcementCheck(PolicyCheck):
208
208
  if stmt_action == "*":
209
209
  continue
210
210
 
211
- # Check exact matches
212
- if stmt_action in actions_config:
213
- matching_actions.append(stmt_action)
211
+ # Check if this statement action matches any of the required actions or patterns
212
+ # Use _action_matches which handles wildcards in both statement and config
213
+ matched = False
214
214
 
215
- # Check pattern matches
216
- for pattern in action_patterns:
217
- try:
218
- if re.match(pattern, stmt_action):
219
- matching_actions.append(stmt_action)
220
- break
221
- except re.error:
222
- continue
215
+ # Check against configured actions
216
+ for required_action in actions_config:
217
+ if await self._action_matches(
218
+ stmt_action, required_action, action_patterns, fetcher
219
+ ):
220
+ matched = True
221
+ break
222
+
223
+ # If not matched by actions, check if wildcard overlaps with patterns
224
+ if not matched and "*" in stmt_action:
225
+ # For wildcards, also check pattern overlap directly
226
+ matched = await self._action_matches(stmt_action, "", action_patterns, fetcher)
227
+
228
+ if matched and stmt_action not in matching_actions:
229
+ matching_actions.append(stmt_action)
223
230
 
224
231
  return len(matching_actions) > 0, matching_actions
225
232
 
@@ -231,20 +238,28 @@ class ActionConditionEnforcementCheck(PolicyCheck):
231
238
 
232
239
  # Check all_of: ALL specified actions must be in statement
233
240
  if all_of:
234
- all_present = all(
235
- any(
236
- self._action_matches(stmt_action, req_action, action_patterns)
237
- for stmt_action in statement_actions
238
- )
239
- for req_action in all_of
240
- )
241
+ all_present = True
242
+ for req_action in all_of:
243
+ found = False
244
+ for stmt_action in statement_actions:
245
+ if await self._action_matches(
246
+ stmt_action, req_action, action_patterns, fetcher
247
+ ):
248
+ found = True
249
+ break
250
+ if not found:
251
+ all_present = False
252
+ break
253
+
241
254
  if not all_present:
242
255
  return False, []
243
256
 
244
257
  # Collect matching actions
245
258
  for stmt_action in statement_actions:
246
259
  for req_action in all_of:
247
- if self._action_matches(stmt_action, req_action, action_patterns):
260
+ if await self._action_matches(
261
+ stmt_action, req_action, action_patterns, fetcher
262
+ ):
248
263
  if stmt_action not in matching_actions:
249
264
  matching_actions.append(stmt_action)
250
265
 
@@ -253,7 +268,9 @@ class ActionConditionEnforcementCheck(PolicyCheck):
253
268
  any_present = False
254
269
  for stmt_action in statement_actions:
255
270
  for req_action in any_of:
256
- if self._action_matches(stmt_action, req_action, action_patterns):
271
+ if await self._action_matches(
272
+ stmt_action, req_action, action_patterns, fetcher
273
+ ):
257
274
  any_present = True
258
275
  if stmt_action not in matching_actions:
259
276
  matching_actions.append(stmt_action)
@@ -266,7 +283,9 @@ class ActionConditionEnforcementCheck(PolicyCheck):
266
283
  forbidden_actions = []
267
284
  for stmt_action in statement_actions:
268
285
  for forbidden_action in none_of:
269
- if self._action_matches(stmt_action, forbidden_action, action_patterns):
286
+ if await self._action_matches(
287
+ stmt_action, forbidden_action, action_patterns, fetcher
288
+ ):
270
289
  forbidden_actions.append(stmt_action)
271
290
 
272
291
  # If forbidden actions are found, this is a match for flagging
@@ -277,15 +296,23 @@ class ActionConditionEnforcementCheck(PolicyCheck):
277
296
 
278
297
  return False, []
279
298
 
280
- def _action_matches(
281
- self, statement_action: str, required_action: str, patterns: list[str]
299
+ async def _action_matches(
300
+ self,
301
+ statement_action: str,
302
+ required_action: str,
303
+ patterns: list[str],
304
+ fetcher: AWSServiceFetcher,
282
305
  ) -> bool:
283
306
  """
284
307
  Check if a statement action matches a required action or pattern.
285
308
  Supports:
286
309
  - Exact matches: "s3:GetObject"
287
- - AWS wildcards: "s3:*", "s3:Get*"
310
+ - AWS wildcards in both statement and required actions: "s3:*", "s3:Get*", "iam:Creat*"
288
311
  - Regex patterns: "^s3:Get.*", "^iam:Delete.*"
312
+
313
+ This method handles bidirectional wildcard matching using real AWS actions from the fetcher:
314
+ - statement_action="iam:Create*" matches required_action="iam:CreateUser"
315
+ - statement_action="iam:C*" matches pattern="^iam:Create" (by checking actual AWS actions)
289
316
  """
290
317
  if statement_action == "*":
291
318
  return False
@@ -304,6 +331,63 @@ class ActionConditionEnforcementCheck(PolicyCheck):
304
331
  except re.error:
305
332
  pass
306
333
 
334
+ # AWS wildcard match in statement_action (e.g., "iam:Creat*" in policy)
335
+ # Check if this wildcard would grant access to actions matching our patterns
336
+ if "*" in statement_action:
337
+ # Convert statement wildcard to regex pattern
338
+ stmt_wildcard_pattern = statement_action.replace("*", ".*").replace("?", ".")
339
+
340
+ # Check if statement wildcard overlaps with required action
341
+ if "*" not in required_action:
342
+ # Required action is specific (e.g., "iam:CreateUser")
343
+ # Check if statement wildcard would grant it
344
+ try:
345
+ if re.match(f"^{stmt_wildcard_pattern}$", required_action):
346
+ return True
347
+ except re.error:
348
+ pass
349
+
350
+ # Check if statement wildcard overlaps with any of our action patterns
351
+ # Strategy: Use real AWS actions from the fetcher instead of hardcoded guesses
352
+ # For example: "iam:C*" should match pattern "^iam:Create" because:
353
+ # - "iam:C*" grants iam:CreateUser, iam:CreateRole, etc. (from AWS)
354
+ # - "^iam:Create" pattern is meant to catch iam:CreateUser, iam:CreateRole, etc.
355
+ # - Therefore they overlap
356
+ if patterns:
357
+ try:
358
+ # Parse the service from the wildcard action
359
+ service_prefix, _ = fetcher.parse_action(statement_action)
360
+
361
+ # Fetch the real list of actions for this service
362
+ service_detail = await fetcher.fetch_service_by_name(service_prefix)
363
+ available_actions = list(service_detail.actions.keys())
364
+
365
+ # Find which actual AWS actions the wildcard would grant
366
+ _, granted_actions = fetcher._match_wildcard_action(
367
+ statement_action.split(":", 1)[1], # Just the action part (e.g., "C*")
368
+ available_actions,
369
+ )
370
+
371
+ # Check if any of the granted actions match our patterns
372
+ for granted_action in granted_actions:
373
+ full_granted_action = f"{service_prefix}:{granted_action}"
374
+ for pattern in patterns:
375
+ try:
376
+ if re.match(pattern, full_granted_action):
377
+ return True
378
+ except re.error:
379
+ continue
380
+
381
+ except (ValueError, Exception):
382
+ # If we can't fetch the service or parse the action, fall back to prefix matching
383
+ stmt_prefix = statement_action.rstrip("*")
384
+ for pattern in patterns:
385
+ try:
386
+ if re.match(pattern, stmt_prefix):
387
+ return True
388
+ except re.error:
389
+ continue
390
+
307
391
  # Regex pattern match (from action_patterns config)
308
392
  for pattern in patterns:
309
393
  try:
@@ -91,14 +91,27 @@ class SecurityBestPracticesCheck(PolicyCheck):
91
91
  if self._is_sub_check_enabled(config, "wildcard_action_check"):
92
92
  if "*" in actions:
93
93
  severity = self._get_sub_check_severity(config, "wildcard_action_check", "warning")
94
+ sub_check_config = config.config.get("wildcard_action_check", {})
95
+
96
+ message = sub_check_config.get("message", "Statement allows all actions (*)")
97
+ suggestion_text = sub_check_config.get(
98
+ "suggestion", "Consider limiting to specific actions needed"
99
+ )
100
+ example = sub_check_config.get("example", "")
101
+
102
+ # Combine suggestion + example like action_condition_enforcement does
103
+ suggestion = (
104
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
105
+ )
106
+
94
107
  issues.append(
95
108
  ValidationIssue(
96
109
  severity=severity,
97
110
  statement_sid=statement_sid,
98
111
  statement_index=statement_idx,
99
112
  issue_type="overly_permissive",
100
- message="Statement allows all actions (*)",
101
- suggestion="Consider limiting to specific actions needed",
113
+ message=message,
114
+ suggestion=suggestion,
102
115
  line_number=line_number,
103
116
  )
104
117
  )
@@ -109,14 +122,27 @@ class SecurityBestPracticesCheck(PolicyCheck):
109
122
  severity = self._get_sub_check_severity(
110
123
  config, "wildcard_resource_check", "warning"
111
124
  )
125
+ sub_check_config = config.config.get("wildcard_resource_check", {})
126
+
127
+ message = sub_check_config.get("message", "Statement applies to all resources (*)")
128
+ suggestion_text = sub_check_config.get(
129
+ "suggestion", "Consider limiting to specific resources"
130
+ )
131
+ example = sub_check_config.get("example", "")
132
+
133
+ # Combine suggestion + example
134
+ suggestion = (
135
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
136
+ )
137
+
112
138
  issues.append(
113
139
  ValidationIssue(
114
140
  severity=severity,
115
141
  statement_sid=statement_sid,
116
142
  statement_index=statement_idx,
117
143
  issue_type="overly_permissive",
118
- message="Statement applies to all resources (*)",
119
- suggestion="Consider limiting to specific resources",
144
+ message=message,
145
+ suggestion=suggestion,
120
146
  line_number=line_number,
121
147
  )
122
148
  )
@@ -125,14 +151,31 @@ class SecurityBestPracticesCheck(PolicyCheck):
125
151
  if self._is_sub_check_enabled(config, "full_wildcard_check"):
126
152
  if "*" in actions and "*" in resources:
127
153
  severity = self._get_sub_check_severity(config, "full_wildcard_check", "error")
154
+ sub_check_config = config.config.get("full_wildcard_check", {})
155
+
156
+ message = sub_check_config.get(
157
+ "message",
158
+ "Statement allows all actions on all resources - CRITICAL SECURITY RISK",
159
+ )
160
+ suggestion_text = sub_check_config.get(
161
+ "suggestion",
162
+ "This grants full administrative access. Restrict to specific actions and resources.",
163
+ )
164
+ example = sub_check_config.get("example", "")
165
+
166
+ # Combine suggestion + example
167
+ suggestion = (
168
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
169
+ )
170
+
128
171
  issues.append(
129
172
  ValidationIssue(
130
173
  severity=severity,
131
174
  statement_sid=statement_sid,
132
175
  statement_index=statement_idx,
133
176
  issue_type="security_risk",
134
- message="Statement allows all actions on all resources - CRITICAL SECURITY RISK",
135
- suggestion="This grants full administrative access. Restrict to specific actions and resources.",
177
+ message=message,
178
+ suggestion=suggestion,
136
179
  line_number=line_number,
137
180
  )
138
181
  )
@@ -155,15 +198,43 @@ class SecurityBestPracticesCheck(PolicyCheck):
155
198
  severity = self._get_sub_check_severity(
156
199
  config, "service_wildcard_check", "warning"
157
200
  )
201
+ sub_check_config = config.config.get("service_wildcard_check", {})
202
+
203
+ # Get message template and replace placeholders
204
+ message_template = sub_check_config.get(
205
+ "message",
206
+ "Service-level wildcard '{action}' grants all permissions for {service} service",
207
+ )
208
+ suggestion_template = sub_check_config.get(
209
+ "suggestion",
210
+ "Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
211
+ )
212
+ example_template = sub_check_config.get("example", "")
213
+
214
+ message = message_template.format(action=action, service=service)
215
+ suggestion_text = suggestion_template.format(action=action, service=service)
216
+ example = (
217
+ example_template.format(action=action, service=service)
218
+ if example_template
219
+ else ""
220
+ )
221
+
222
+ # Combine suggestion + example
223
+ suggestion = (
224
+ f"{suggestion_text}\nExample:\n{example}"
225
+ if example
226
+ else suggestion_text
227
+ )
228
+
158
229
  issues.append(
159
230
  ValidationIssue(
160
231
  severity=severity,
161
232
  statement_sid=statement_sid,
162
233
  statement_index=statement_idx,
163
234
  issue_type="overly_permissive",
164
- message=f"Service-level wildcard '{action}' grants all permissions for {service} service",
235
+ message=message,
165
236
  action=action,
166
- suggestion=f"Consider specifying explicit actions instead of '{action}'. If you need multiple actions, list them individually or use more specific wildcards like '{service}:Get*' or '{service}:List*'.",
237
+ suggestion=suggestion,
167
238
  line_number=line_number,
168
239
  )
169
240
  )
@@ -177,13 +248,33 @@ class SecurityBestPracticesCheck(PolicyCheck):
177
248
 
178
249
  if is_sensitive and not has_conditions:
179
250
  severity = self._get_sub_check_severity(config, "sensitive_action_check", "warning")
251
+ sub_check_config = config.config.get("sensitive_action_check", {})
180
252
 
181
- # Create appropriate message based on matched actions
253
+ # Create appropriate message based on matched actions using configurable templates
182
254
  if len(matched_actions) == 1:
183
- message = f"Sensitive action '{matched_actions[0]}' should have conditions to limit when it can be used"
255
+ message_template = sub_check_config.get(
256
+ "message_single",
257
+ "Sensitive action '{action}' should have conditions to limit when it can be used",
258
+ )
259
+ message = message_template.format(action=matched_actions[0])
184
260
  else:
185
261
  action_list = "', '".join(matched_actions)
186
- message = f"Sensitive actions '{action_list}' should have conditions to limit when they can be used"
262
+ message_template = sub_check_config.get(
263
+ "message_multiple",
264
+ "Sensitive actions '{actions}' should have conditions to limit when they can be used",
265
+ )
266
+ message = message_template.format(actions=action_list)
267
+
268
+ suggestion_text = sub_check_config.get(
269
+ "suggestion",
270
+ "Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
271
+ )
272
+ example = sub_check_config.get("example", "")
273
+
274
+ # Combine suggestion + example
275
+ suggestion = (
276
+ f"{suggestion_text}\nExample:\n{example}" if example else suggestion_text
277
+ )
187
278
 
188
279
  issues.append(
189
280
  ValidationIssue(
@@ -193,7 +284,7 @@ class SecurityBestPracticesCheck(PolicyCheck):
193
284
  issue_type="missing_condition",
194
285
  message=message,
195
286
  action=(matched_actions[0] if len(matched_actions) == 1 else None),
196
- suggestion="Add conditions like 'aws:Resource/owner must match aws:Principal/owner', IP restrictions, MFA requirements, or time-based restrictions",
287
+ suggestion=suggestion,
197
288
  line_number=line_number,
198
289
  )
199
290
  )