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.
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/METADATA +88 -10
- iam_policy_validator-1.1.1.dist-info/RECORD +53 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_condition_enforcement.py +112 -28
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +18 -138
- iam_validator/checks/security_best_practices.py +241 -400
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
- iam_validator/checks/utils/wildcard_expansion.py +89 -0
- iam_validator/commands/__init__.py +3 -1
- iam_validator/commands/cache.py +402 -0
- iam_validator/commands/validate.py +7 -5
- iam_validator/core/access_analyzer_report.py +2 -1
- iam_validator/core/aws_fetcher.py +79 -19
- iam_validator/core/check_registry.py +3 -0
- iam_validator/core/cli.py +1 -1
- iam_validator/core/config_loader.py +40 -3
- iam_validator/core/defaults.py +334 -0
- iam_validator/core/formatters/__init__.py +2 -0
- iam_validator/core/formatters/console.py +44 -7
- iam_validator/core/formatters/csv.py +7 -2
- iam_validator/core/formatters/enhanced.py +433 -0
- iam_validator/core/formatters/html.py +127 -37
- iam_validator/core/formatters/markdown.py +10 -2
- iam_validator/core/models.py +30 -6
- iam_validator/core/policy_checks.py +21 -2
- iam_validator/core/report.py +112 -26
- iam_policy_validator-1.0.4.dist-info/RECORD +0 -45
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.1.dist-info}/entry_points.txt +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
-
####
|
|
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
|
-
|
|
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
|
-
####
|
|
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
|
-
|
|
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 [
|
|
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)
|
|
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 | `
|
|
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,,
|
iam_validator/__version__.py
CHANGED
iam_validator/checks/__init__.py
CHANGED
|
@@ -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
|
|
212
|
-
|
|
213
|
-
|
|
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
|
|
216
|
-
for
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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 =
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
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
|