iam-policy-validator 1.0.4__py3-none-any.whl → 1.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (34) hide show
  1. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/METADATA +88 -10
  2. iam_policy_validator-1.1.1.dist-info/RECORD +53 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +2 -0
  5. iam_validator/checks/action_condition_enforcement.py +112 -28
  6. iam_validator/checks/action_resource_constraint.py +151 -0
  7. iam_validator/checks/action_validation.py +18 -138
  8. iam_validator/checks/security_best_practices.py +241 -400
  9. iam_validator/checks/utils/__init__.py +1 -0
  10. iam_validator/checks/utils/policy_level_checks.py +143 -0
  11. iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
  12. iam_validator/checks/utils/wildcard_expansion.py +89 -0
  13. iam_validator/commands/__init__.py +3 -1
  14. iam_validator/commands/cache.py +402 -0
  15. iam_validator/commands/validate.py +7 -5
  16. iam_validator/core/access_analyzer_report.py +2 -1
  17. iam_validator/core/aws_fetcher.py +79 -19
  18. iam_validator/core/check_registry.py +3 -0
  19. iam_validator/core/cli.py +1 -1
  20. iam_validator/core/config_loader.py +40 -3
  21. iam_validator/core/defaults.py +334 -0
  22. iam_validator/core/formatters/__init__.py +2 -0
  23. iam_validator/core/formatters/console.py +44 -7
  24. iam_validator/core/formatters/csv.py +7 -2
  25. iam_validator/core/formatters/enhanced.py +433 -0
  26. iam_validator/core/formatters/html.py +127 -37
  27. iam_validator/core/formatters/markdown.py +10 -2
  28. iam_validator/core/models.py +30 -6
  29. iam_validator/core/policy_checks.py +21 -2
  30. iam_validator/core/report.py +112 -26
  31. iam_policy_validator-1.0.4.dist-info/RECORD +0 -45
  32. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/WHEEL +0 -0
  33. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/entry_points.txt +0 -0
  34. {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.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.0.4
3
+ Version: 1.1.1
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.12'
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 | "" |
@@ -0,0 +1,53 @@
1
+ iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
2
+ iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
+ iam_validator/__version__.py,sha256=xEe2pX5CjvpoW3wJ6rXXULgJzJ3B6BM7RqL5dElKRA4,206
4
+ iam_validator/checks/__init__.py,sha256=eKTPgiZ1i3zvyP6OdKgLx9s3u69onITMYifmJPJwZgM,968
5
+ iam_validator/checks/action_condition_enforcement.py,sha256=3M1Wj89Af6H-ywBTruZbJPzhCBBQVanVb5hwv-fkiDE,29721
6
+ iam_validator/checks/action_resource_constraint.py,sha256=p-gP7S9QYR6M7vffrnJY6LOlMUTn0kpEbrxQ8pTY5rs,6031
7
+ iam_validator/checks/action_validation.py,sha256=IpxtTsk58f2zEZ-xzAoyHw4QK8BCRV43OffP-8ydf9E,2578
8
+ iam_validator/checks/condition_key_validation.py,sha256=bc4LQ8IRKyt0RquaQvQvVjmeJnuOUAFRL8xdduLPa_U,2661
9
+ iam_validator/checks/policy_size.py,sha256=4cvZiWRJXGuvYo8PRcdD1Py_ZL8Xw0lOJfXTs6EX-_I,5753
10
+ iam_validator/checks/resource_validation.py,sha256=AEIoiR6AKYLuVaA8ne3QE5qy6NCMDe98_2JAiwE9-JU,4261
11
+ iam_validator/checks/security_best_practices.py,sha256=-gqxtcx_cUV1ZnyZ8Flydwan1vxb-RmnanWoIlU6YyY,21711
12
+ iam_validator/checks/sid_uniqueness.py,sha256=7S8RtVJgYPTKgr7gSEmxgT0oIGkSoXN6iu0ALHbcSfw,5015
13
+ iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
14
+ iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
15
+ iam_validator/checks/utils/sensitive_action_matcher.py,sha256=_kQRGRJDQ0TLHymZbucXmKOfZJ_OUsf5p0blLfP54EY,8883
16
+ iam_validator/checks/utils/wildcard_expansion.py,sha256=L6AWrLRacqXqk9k-5ZmXv5HyoAyz98cg5AlTvzH2tTI,3158
17
+ iam_validator/commands/__init__.py,sha256=lF0fSUukLSxTAvhjg-0P79YMseYwihIr_tmQYbfNgcY,425
18
+ iam_validator/commands/analyze.py,sha256=TWlDaZ8gVOdNv6__KQQfzeLVW36qLiL5IzlhGYfvq_g,16501
19
+ iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
20
+ iam_validator/commands/cache.py,sha256=1E-irKF3e2CFUEG9s1z64hIKLVSYFQ-L92ld6L3za5g,14368
21
+ iam_validator/commands/post_to_pr.py,sha256=hl_K-XlELYN-ArjMdgQqysvIE-26yf9XdrMl4ToDwG0,2148
22
+ iam_validator/commands/validate.py,sha256=R295cOTly8n7zL1jfvbh9RuCgiM5edBqbf6YMn_4G9A,14013
23
+ iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
24
+ iam_validator/core/access_analyzer.py,sha256=poeT1i74jXpKr1B3UmvqiTvCTbq82zffWgZHwiFUwoo,24337
25
+ iam_validator/core/access_analyzer_report.py,sha256=IrQVszlhFfQ6WykYLpig7TU3hf8dnQTegPDsOvHjR5Q,24873
26
+ iam_validator/core/aws_fetcher.py,sha256=P7fYX1Q1TICuTOlGaqH97U3m8B0bqGE9jP7cxfmny8k,27418
27
+ iam_validator/core/aws_global_conditions.py,sha256=ADVcMEWhgvDZWdBmRUQN3HB7a9OycbTLecXFAy3LPbo,5837
28
+ iam_validator/core/check_registry.py,sha256=wxqaF2t_3lWgT6x7_PnnZ8XGjHKUxUk72UlmdYBLFyo,15679
29
+ iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
30
+ iam_validator/core/config_loader.py,sha256=Pq2rd6LJtEZET0ZeW4hEZS2ZRLC5gNRsKbtLyIsT21I,16516
31
+ iam_validator/core/defaults.py,sha256=sPQJUMyjv4yalGCuyQhlY42rDc_-BZLq6qS0GgoP4mc,11893
32
+ iam_validator/core/models.py,sha256=rWIZnD-I81Sg4asgOhnB10FWJC5mxQ2JO9bdS0sHb4Q,10772
33
+ iam_validator/core/policy_checks.py,sha256=vIzRkqf5k1BB0elry5a4E2SRBlp6Vz3jPqrav29k3PM,24842
34
+ iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
35
+ iam_validator/core/pr_commenter.py,sha256=TOhVXKTFcRHQ9EVuShXQcKXn9aNjB1mU6FnR2jvltmw,10581
36
+ iam_validator/core/report.py,sha256=wPkLvsIej-AaW5FlqntvUuHuEMvyi2iBI6NQF496gCM,33064
37
+ iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
38
+ iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
39
+ iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
40
+ iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
41
+ iam_validator/core/formatters/enhanced.py,sha256=k_DwzhGTARUKMv4bjkaCrpI6ypT10z9LcSk8gKlyDIM,16547
42
+ iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
43
+ iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
44
+ iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
45
+ iam_validator/core/formatters/sarif.py,sha256=tqp8g7RmUh0HRk-kKDaucx4sa-5I9ikgkSpy1MM8Vi4,7200
46
+ iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
47
+ iam_validator/integrations/github_integration.py,sha256=bKs94vNT4PmcmUPUeuY2WJFhCYpUY2SWiBP1vj-andA,25673
48
+ iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
49
+ iam_policy_validator-1.1.1.dist-info/METADATA,sha256=tliAMa89Y5CSxbEy0PCV_IXr-FSn07I91AKl44QpoJQ,25144
50
+ iam_policy_validator-1.1.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
51
+ iam_policy_validator-1.1.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
52
+ iam_policy_validator-1.1.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
53
+ iam_policy_validator-1.1.1.dist-info/RECORD,,
@@ -3,5 +3,5 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.0.4"
6
+ __version__ = "1.1.1"
7
7
  __version_info__ = tuple(int(part) for part in __version__.split("."))
@@ -5,6 +5,7 @@ Built-in policy checks for IAM Policy Validator.
5
5
  from iam_validator.checks.action_condition_enforcement import (
6
6
  ActionConditionEnforcementCheck,
7
7
  )
8
+ from iam_validator.checks.action_resource_constraint import ActionResourceConstraintCheck
8
9
  from iam_validator.checks.action_validation import ActionValidationCheck
9
10
  from iam_validator.checks.condition_key_validation import ConditionKeyValidationCheck
10
11
  from iam_validator.checks.policy_size import PolicySizeCheck
@@ -14,6 +15,7 @@ from iam_validator.checks.sid_uniqueness import SidUniquenessCheck
14
15
 
15
16
  __all__ = [
16
17
  "ActionConditionEnforcementCheck",
18
+ "ActionResourceConstraintCheck",
17
19
  "ActionValidationCheck",
18
20
  "ConditionKeyValidationCheck",
19
21
  "PolicySizeCheck",
@@ -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:
@@ -0,0 +1,151 @@
1
+ """Action resource constraint check - validates resource constraints for actions.
2
+
3
+ This check ensures that:
4
+ - Actions WITHOUT required resource types (empty or missing Resources field in AWS API)
5
+ MUST use Resource: "*" because they are account-level operations
6
+
7
+ Examples of actions without required resources:
8
+ - s3:ListAllMyBuckets (lists all buckets in account)
9
+ - iam:ListRoles (lists all roles in account)
10
+ - ec2:DescribeInstances (describes instances across all regions)
11
+
12
+ These actions cannot target specific resources because they operate at the account level.
13
+ """
14
+
15
+ from iam_validator.core.aws_fetcher import AWSServiceFetcher
16
+ from iam_validator.core.check_registry import CheckConfig, PolicyCheck
17
+ from iam_validator.core.models import Statement, ValidationIssue
18
+
19
+
20
+ class ActionResourceConstraintCheck(PolicyCheck):
21
+ """Validates resource constraints based on action requirements.
22
+ This check ensures that actions without required resource types use Resource: "*".
23
+
24
+ Examples of such actions include s3:ListAllMyBuckets, iam:ListRoles, etc.
25
+ """
26
+
27
+ @property
28
+ def check_id(self) -> str:
29
+ return "action_resource_constraint"
30
+
31
+ @property
32
+ def description(self) -> str:
33
+ return "Validates that actions without required resource types use Resource: '*'"
34
+
35
+ @property
36
+ def default_severity(self) -> str:
37
+ return "error"
38
+
39
+ async def execute(
40
+ self,
41
+ statement: Statement,
42
+ statement_idx: int,
43
+ fetcher: AWSServiceFetcher,
44
+ config: CheckConfig,
45
+ ) -> list[ValidationIssue]:
46
+ """Execute action resource constraint validation on a statement."""
47
+ issues = []
48
+
49
+ # Only check Allow statements
50
+ if statement.effect != "Allow":
51
+ return issues
52
+
53
+ # Get actions and resources from statement
54
+ actions = statement.get_actions()
55
+ resources = statement.get_resources()
56
+ statement_sid = statement.sid
57
+ line_number = statement.line_number
58
+
59
+ # Skip if no actions or wildcard action
60
+ if not actions or "*" in actions:
61
+ return issues
62
+
63
+ # Skip if already using wildcard resource (this is correct for these actions)
64
+ if "*" in resources:
65
+ return issues
66
+
67
+ # Check each action for resource requirements
68
+ actions_without_required_resources = []
69
+
70
+ for action in actions:
71
+ # Skip wildcard actions
72
+ if "*" in action:
73
+ continue
74
+
75
+ try:
76
+ # Parse action to get service and action name
77
+ service_prefix, action_name = fetcher.parse_action(action)
78
+
79
+ # Fetch service detail to check resource requirements
80
+ service_detail = await fetcher.fetch_service_by_name(service_prefix)
81
+
82
+ # Check if action exists
83
+ if action_name not in service_detail.actions:
84
+ # Action doesn't exist - skip (will be caught by action_validation_check)
85
+ continue
86
+
87
+ action_detail = service_detail.actions[action_name]
88
+
89
+ # Check if action has NO required resources (empty or missing Resources field)
90
+ has_no_required_resources = (
91
+ not action_detail.resources or len(action_detail.resources) == 0
92
+ )
93
+
94
+ if has_no_required_resources:
95
+ actions_without_required_resources.append(action)
96
+
97
+ except ValueError:
98
+ # Invalid action format - skip (will be caught by action_validation_check)
99
+ continue
100
+ except Exception:
101
+ # Service not found or other error - skip
102
+ continue
103
+
104
+ # If we found actions without required resources, report the issue
105
+ if actions_without_required_resources:
106
+ # Get a sample of the resources to show in error message
107
+ resource_sample = resources[:3] if len(resources) > 3 else resources
108
+ resource_display = ", ".join(f'"{r}"' for r in resource_sample)
109
+ if len(resources) > 3:
110
+ resource_display += f", ... ({len(resources) - 3} more)"
111
+
112
+ # Format action list
113
+ action_list = ", ".join(f'"{a}"' for a in actions_without_required_resources)
114
+
115
+ # Determine message based on how many actions are affected
116
+ if len(actions_without_required_resources) == 1:
117
+ message = (
118
+ f"Action {action_list} does not operate on specific resources "
119
+ f'and requires Resource: "*"'
120
+ )
121
+ suggestion = (
122
+ f"Action {action_list} is an account-level operation that cannot target "
123
+ 'specific resources. Move this action to a separate statement with Resource: "*", '
124
+ "and keep resource-specific actions in another statement with their specific ARNs"
125
+ )
126
+ else:
127
+ message = (
128
+ f"Actions {action_list} do not operate on specific resources "
129
+ f'and require Resource: "*"'
130
+ )
131
+ suggestion = (
132
+ "These actions are account-level operations that cannot target "
133
+ 'specific resources. Move these actions to a dedicated statement with Resource: "*", '
134
+ "and keep resource-specific actions in separate statements with their specific ARNs"
135
+ )
136
+
137
+ issues.append(
138
+ ValidationIssue(
139
+ severity=self.get_severity(config),
140
+ statement_sid=statement_sid,
141
+ statement_index=statement_idx,
142
+ issue_type="invalid_resource_constraint",
143
+ message=message,
144
+ action=action_list,
145
+ resource=resource_display,
146
+ suggestion=suggestion,
147
+ line_number=line_number,
148
+ )
149
+ )
150
+
151
+ return issues