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.
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.0.dist-info}/METADATA +88 -10
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.0.dist-info}/RECORD +18 -16
- iam_validator/checks/action_condition_enforcement.py +112 -28
- iam_validator/checks/security_best_practices.py +103 -12
- iam_validator/commands/validate.py +7 -5
- iam_validator/core/config_loader.py +39 -3
- iam_validator/core/defaults.py +304 -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 +428 -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/report.py +104 -25
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.0.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.0.4.dist-info → iam_policy_validator-1.1.0.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
|
|
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
|
|
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.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
|
-
|
|
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 | "" |
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
25
|
-
iam_validator/core/
|
|
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=
|
|
30
|
-
iam_validator/core/formatters/__init__.py,sha256=
|
|
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=
|
|
33
|
-
iam_validator/core/formatters/csv.py,sha256=
|
|
34
|
-
iam_validator/core/formatters/
|
|
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=
|
|
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.
|
|
42
|
-
iam_policy_validator-1.0.
|
|
43
|
-
iam_policy_validator-1.0.
|
|
44
|
-
iam_policy_validator-1.0.
|
|
45
|
-
iam_policy_validator-1.0.
|
|
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
|
|
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:
|
|
@@ -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=
|
|
101
|
-
suggestion=
|
|
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=
|
|
119
|
-
suggestion=
|
|
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=
|
|
135
|
-
suggestion=
|
|
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=
|
|
235
|
+
message=message,
|
|
165
236
|
action=action,
|
|
166
|
-
suggestion=
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
287
|
+
suggestion=suggestion,
|
|
197
288
|
line_number=line_number,
|
|
198
289
|
)
|
|
199
290
|
)
|