iam-policy-validator 1.6.0__py3-none-any.whl → 1.7.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.
- iam_policy_validator-1.7.1.dist-info/METADATA +429 -0
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/RECORD +32 -31
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +3 -1
- iam_validator/checks/action_resource_matching.py +23 -6
- iam_validator/checks/full_wildcard.py +5 -1
- iam_validator/checks/policy_size.py +3 -7
- iam_validator/checks/policy_type_validation.py +9 -3
- iam_validator/checks/principal_validation.py +1 -1
- iam_validator/checks/resource_validation.py +54 -24
- iam_validator/checks/sensitive_action.py +5 -1
- iam_validator/checks/service_wildcard.py +3 -1
- iam_validator/checks/utils/sensitive_action_matcher.py +1 -2
- iam_validator/checks/utils/wildcard_expansion.py +1 -2
- iam_validator/checks/wildcard_action.py +7 -2
- iam_validator/checks/wildcard_resource.py +5 -1
- iam_validator/commands/analyze.py +98 -1
- iam_validator/commands/validate.py +4 -2
- iam_validator/core/access_analyzer.py +5 -0
- iam_validator/core/access_analyzer_report.py +2 -5
- iam_validator/core/aws_fetcher.py +14 -4
- iam_validator/core/config/config_loader.py +3 -6
- iam_validator/core/constants.py +74 -0
- iam_validator/core/models.py +29 -13
- iam_validator/core/pr_commenter.py +104 -18
- iam_validator/core/report.py +49 -36
- iam_validator/integrations/github_integration.py +21 -1
- iam_validator/sdk/arn_matching.py +108 -0
- iam_validator/utils/regex.py +7 -8
- iam_policy_validator-1.6.0.dist-info/METADATA +0 -1050
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.6.0.dist-info → iam_policy_validator-1.7.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: iam-policy-validator
|
|
3
|
+
Version: 1.7.1
|
|
4
|
+
Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
|
|
5
|
+
Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
|
|
6
|
+
Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/boogy/iam-policy-validator
|
|
8
|
+
Project-URL: Issues, https://github.com/boogy/iam-policy-validator/issues
|
|
9
|
+
Project-URL: Changelog, https://github.com/boogy/iam-policy-validator/blob/main/docs/CHANGELOG.md
|
|
10
|
+
Author-email: boogy <0xboogy@gmail.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: aws,github-action,iam,policy,security,validation
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Intended Audience :: System Administrators
|
|
17
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Security
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: boto3>=1.28.0
|
|
27
|
+
Requires-Dist: botocore>=1.40.55
|
|
28
|
+
Requires-Dist: httpx[http2]>=0.27.0
|
|
29
|
+
Requires-Dist: pydantic>=2.0.0
|
|
30
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
|
+
Requires-Dist: rich>=13.0.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: mypy>=1.0.0; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest-benchmark>=4.0.0; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov>=7.0.0; extra == 'dev'
|
|
37
|
+
Requires-Dist: pytest>=7.0.0; extra == 'dev'
|
|
38
|
+
Requires-Dist: ruff>=0.1.0; extra == 'dev'
|
|
39
|
+
Requires-Dist: types-boto3; extra == 'dev'
|
|
40
|
+
Requires-Dist: types-pyyaml; extra == 'dev'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
# IAM Policy Validator
|
|
44
|
+
|
|
45
|
+
> **⚡ Catch IAM policy security issues and errors before they reach production** - A comprehensive validation tool for AWS IAM policies with built-in security checks and optional AWS Access Analyzer integration.
|
|
46
|
+
|
|
47
|
+
[](https://github.com/marketplace/actions/iam-policy-validator)
|
|
48
|
+
[](https://www.python.org/downloads/)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
[](https://scorecard.dev/viewer/?uri=github.com/boogy/iam-policy-validator)
|
|
51
|
+
|
|
52
|
+
## 🚀 Why IAM Policy Validator?
|
|
53
|
+
|
|
54
|
+
**IAM policy errors are costly and dangerous.** A single misconfigured policy can:
|
|
55
|
+
- ❌ Grant unintended admin access (privilege escalation)
|
|
56
|
+
- ❌ Expose sensitive data to the public
|
|
57
|
+
- ❌ Break production deployments with invalid syntax
|
|
58
|
+
- ❌ Create security vulnerabilities that persist for months
|
|
59
|
+
|
|
60
|
+
**This tool prevents these issues** by:
|
|
61
|
+
- ✅ **Dual validation** - built-in checks + optional AWS Access Analyzer
|
|
62
|
+
- ✅ **Catches real threats** - Privilege escalation, wildcards, missing conditions
|
|
63
|
+
- ✅ **PR integration** - Automated validation in GitHub Actions
|
|
64
|
+
- ✅ **Saves security team time** - Catches common issues before manual review
|
|
65
|
+
- ✅ **Developer-friendly** - Clear errors with fix suggestions
|
|
66
|
+
- ✅ **Zero setup** - Works as a GitHub Action out of the box
|
|
67
|
+
|
|
68
|
+
## ✨ What Makes It Special
|
|
69
|
+
|
|
70
|
+
### 🔍 Two Validation Layers
|
|
71
|
+
|
|
72
|
+
**1. Built-in Checks (No AWS Credentials Required)**
|
|
73
|
+
- **Security & Compliance Checks** - Works offline, no AWS account needed
|
|
74
|
+
- **Privilege Escalation Detection** - Detects dangerous IAM actions and configurable combination patterns
|
|
75
|
+
- **Wildcard Analysis** - Catches overly permissive wildcards (`*`, `s3:*`)
|
|
76
|
+
- **Sensitive Action Enforcement** - 490 actions requiring conditions (MFA, IP, tags)
|
|
77
|
+
- **AWS Requirements Validation** - Actions, conditions, ARN formats, policy size
|
|
78
|
+
|
|
79
|
+
**2. AWS Access Analyzer (Optional)**
|
|
80
|
+
- **Official AWS Validation** - Syntax, semantics, and security checks
|
|
81
|
+
- **Public Access Detection** - Checks 29+ resource types (S3, Lambda, SNS, etc.)
|
|
82
|
+
- **Policy Comparison** - Detect new permissions vs baseline
|
|
83
|
+
- **Cross-account Analysis** - Validates external access
|
|
84
|
+
|
|
85
|
+
### 🎯 Developer Experience
|
|
86
|
+
- **Auto-detects IAM policies** - Scans mixed JSON/YAML repos automatically
|
|
87
|
+
- **PR comments & reviews** - Line-specific feedback in GitHub
|
|
88
|
+
- **7 output formats** - Console, JSON, Markdown, SARIF, CSV, HTML, Enhanced
|
|
89
|
+
- **Extensible** - Add custom checks via Python plugins
|
|
90
|
+
|
|
91
|
+
**📖 See [full feature documentation](docs/README.md) for details**
|
|
92
|
+
|
|
93
|
+
## 📈 What It Catches
|
|
94
|
+
|
|
95
|
+
### Example 1: Privilege Escalation (Built-in Check)
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"Statement": [
|
|
99
|
+
{"Effect": "Allow", "Action": "iam:CreateUser", "Resource": "*"},
|
|
100
|
+
{"Effect": "Allow", "Action": "iam:AttachUserPolicy", "Resource": "*"}
|
|
101
|
+
]
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Detected:**
|
|
106
|
+
```
|
|
107
|
+
🚨 CRITICAL: Privilege escalation risk detected!
|
|
108
|
+
Actions ['iam:CreateUser', 'iam:AttachUserPolicy'] enable:
|
|
109
|
+
1. Create new IAM user
|
|
110
|
+
2. Attach AdministratorAccess to that user
|
|
111
|
+
3. Gain full AWS account access
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Example 2: Overly Permissive Wildcards (Built-in Check)
|
|
115
|
+
```json
|
|
116
|
+
{
|
|
117
|
+
"Effect": "Allow",
|
|
118
|
+
"Action": "s3:*",
|
|
119
|
+
"Resource": "*"
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Detected:**
|
|
124
|
+
```
|
|
125
|
+
❌ HIGH: Service wildcard 's3:*' detected
|
|
126
|
+
❌ MEDIUM: Wildcard resource '*' - applies to all S3 buckets
|
|
127
|
+
❌ CRITICAL: Full wildcard (Action + Resource) grants excessive access
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Example 3: Missing Required Conditions (Built-in Check)
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"Effect": "Allow",
|
|
134
|
+
"Action": "iam:PassRole",
|
|
135
|
+
"Resource": "*"
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Detected:**
|
|
140
|
+
```
|
|
141
|
+
❌ HIGH: iam:PassRole missing required condition
|
|
142
|
+
💡 Add condition: iam:PassedToService to restrict role passing
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Example 4: Public Access (Access Analyzer - Optional)
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"Principal": "*",
|
|
149
|
+
"Action": "s3:GetObject",
|
|
150
|
+
"Resource": "arn:aws:s3:::private-bucket/*"
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Detected:**
|
|
155
|
+
```
|
|
156
|
+
🛑 CRITICAL: Resource policy allows public internet access
|
|
157
|
+
Principal "*" grants world-readable access to S3 bucket
|
|
158
|
+
💡 Use specific AWS principals or add aws:SourceIp conditions
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Quick Start
|
|
162
|
+
|
|
163
|
+
### GitHub Action (Recommended)
|
|
164
|
+
|
|
165
|
+
Create `.github/workflows/iam-validator.yml`:
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
name: IAM Policy Validation
|
|
169
|
+
|
|
170
|
+
on:
|
|
171
|
+
pull_request:
|
|
172
|
+
paths: ['policies/**/*.json']
|
|
173
|
+
|
|
174
|
+
jobs:
|
|
175
|
+
validate:
|
|
176
|
+
runs-on: ubuntu-latest
|
|
177
|
+
permissions:
|
|
178
|
+
contents: read
|
|
179
|
+
pull-requests: write
|
|
180
|
+
steps:
|
|
181
|
+
- uses: actions/checkout@v5
|
|
182
|
+
- uses: boogy/iam-policy-validator@v1
|
|
183
|
+
with:
|
|
184
|
+
path: policies/
|
|
185
|
+
fail-on-warnings: true
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**With AWS Access Analyzer (optional):**
|
|
189
|
+
```yaml
|
|
190
|
+
- uses: aws-actions/configure-aws-credentials@v4
|
|
191
|
+
with:
|
|
192
|
+
role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
|
|
193
|
+
aws-region: us-east-1
|
|
194
|
+
- uses: boogy/iam-policy-validator@v1
|
|
195
|
+
with:
|
|
196
|
+
path: policies/
|
|
197
|
+
use-access-analyzer: true
|
|
198
|
+
run-all-checks: true # Run both Access Analyzer + built-in checks
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
**📖 For all GitHub Action inputs and advanced workflows, see [GitHub Actions Guide](docs/github-actions-workflows.md)**
|
|
202
|
+
|
|
203
|
+
### CLI Tool
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
# Install
|
|
207
|
+
pip install iam-policy-validator
|
|
208
|
+
|
|
209
|
+
# Validate (built-in checks only - no AWS credentials needed)
|
|
210
|
+
iam-validator validate --path ./policies/
|
|
211
|
+
|
|
212
|
+
# Validate with AWS Access Analyzer (requires AWS credentials)
|
|
213
|
+
iam-validator analyze --path ./policies/
|
|
214
|
+
|
|
215
|
+
# With both Access Analyzer + built-in checks
|
|
216
|
+
iam-validator analyze --path ./policies/ --run-all-checks
|
|
217
|
+
|
|
218
|
+
# Different policy types
|
|
219
|
+
iam-validator validate --path ./policies/ --policy-type RESOURCE_POLICY
|
|
220
|
+
|
|
221
|
+
# Output formats
|
|
222
|
+
iam-validator validate --path ./policies/ --format json --output report.json
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**📖 See [CLI documentation](docs/README.md) for all commands and options**
|
|
226
|
+
|
|
227
|
+
### Python Library
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from iam_validator.core.policy_loader import PolicyLoader
|
|
231
|
+
from iam_validator.core.policy_checks import validate_policies
|
|
232
|
+
|
|
233
|
+
# Load and validate
|
|
234
|
+
loader = PolicyLoader()
|
|
235
|
+
policies = loader.load_from_path("./policies")
|
|
236
|
+
results = await validate_policies(policies)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**📖 See [Python Library Guide](docs/python-library-usage.md) for complete examples**
|
|
240
|
+
|
|
241
|
+
## Built-in Validation Checks
|
|
242
|
+
|
|
243
|
+
**All checks are fully configurable** - Enable/disable checks, adjust severity levels, add custom requirements, and define ignore patterns through the configuration file.
|
|
244
|
+
|
|
245
|
+
### AWS Correctness Checks (12)
|
|
246
|
+
Validates policies against AWS IAM requirements:
|
|
247
|
+
- **Action validation** - Verify actions exist in AWS services
|
|
248
|
+
- **Condition key validation** - Check condition keys are valid for actions
|
|
249
|
+
- **Condition type matching** - Ensure condition values match expected types
|
|
250
|
+
- **Resource ARN validation** - Validate ARN formats and patterns
|
|
251
|
+
- **Principal validation** - Check principal formats (resource policies)
|
|
252
|
+
- **Policy size limits** - Enforce AWS size constraints
|
|
253
|
+
- **SID uniqueness** - Ensure statement IDs are unique
|
|
254
|
+
- **Set operator validation** - Validate ForAllValues/ForAnyValue usage
|
|
255
|
+
- **MFA condition patterns** - Detect common MFA anti-patterns
|
|
256
|
+
- **Policy type validation** - Enforce policy type requirements (RCP, SCP, etc.)
|
|
257
|
+
- **Action-resource matching** - Detect impossible action-resource combinations
|
|
258
|
+
- **Action-resource constraints** - Validate service-specific constraints
|
|
259
|
+
|
|
260
|
+
### Security Best Practices (6)
|
|
261
|
+
Identifies security risks and overly permissive permissions:
|
|
262
|
+
- **Wildcard action** (`Action: "*"`)
|
|
263
|
+
- **Wildcard resource** (`Resource: "*"`)
|
|
264
|
+
- **Full wildcard** (CRITICAL: both `Action: "*"` and `Resource: "*"`)
|
|
265
|
+
- **Service wildcards** (`s3:*`, `iam:*`, etc.)
|
|
266
|
+
- **Sensitive actions** - ~490 actions across 4 risk categories requiring conditions
|
|
267
|
+
- **Action condition enforcement** - Enforce required conditions (MFA, IP, SourceArn, etc.)
|
|
268
|
+
|
|
269
|
+
### Configuration & Customization
|
|
270
|
+
|
|
271
|
+
All checks can be customized via a yaml configuration file ex: `.iam-validator.yaml`:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
settings:
|
|
275
|
+
enable_builtin_checks: true
|
|
276
|
+
fail_on_severity: high
|
|
277
|
+
|
|
278
|
+
# Customize individual checks
|
|
279
|
+
wildcard_action:
|
|
280
|
+
enabled: true
|
|
281
|
+
severity: critical
|
|
282
|
+
|
|
283
|
+
# Detect privilege escalation patterns
|
|
284
|
+
sensitive_action:
|
|
285
|
+
enabled: true
|
|
286
|
+
severity: critical
|
|
287
|
+
sensitive_actions:
|
|
288
|
+
# all_of: Detects when ALL actions exist across the entire policy
|
|
289
|
+
# (checks multiple statements - finds scattered dangerous combinations)
|
|
290
|
+
- all_of:
|
|
291
|
+
- "iam:CreateUser"
|
|
292
|
+
- "iam:AttachUserPolicy"
|
|
293
|
+
|
|
294
|
+
# any_of: Detects when ANY action exists in a single statement
|
|
295
|
+
# (per-statement check - flags individual dangerous actions)
|
|
296
|
+
- any_of:
|
|
297
|
+
- "iam:PutUserPolicy"
|
|
298
|
+
- "iam:PutGroupPolicy"
|
|
299
|
+
- "iam:PutRolePolicy"
|
|
300
|
+
|
|
301
|
+
# Lambda backdoor: Needs both actions somewhere in policy
|
|
302
|
+
- all_of:
|
|
303
|
+
- "lambda:CreateFunction"
|
|
304
|
+
- "iam:PassRole"
|
|
305
|
+
|
|
306
|
+
# Regex patterns work with all_of (policy-wide check)
|
|
307
|
+
- all_of:
|
|
308
|
+
- "iam:Create.*" # Any IAM Create action
|
|
309
|
+
- "iam:Attach.*" # Any IAM Attach action
|
|
310
|
+
|
|
311
|
+
# Enforce required conditions for sensitive actions
|
|
312
|
+
action_condition_enforcement:
|
|
313
|
+
enabled: true
|
|
314
|
+
action_condition_requirements:
|
|
315
|
+
- actions: ["iam:PassRole"]
|
|
316
|
+
severity: critical
|
|
317
|
+
required_conditions:
|
|
318
|
+
- condition_key: "iam:PassedToService"
|
|
319
|
+
|
|
320
|
+
# Ignore specific patterns
|
|
321
|
+
ignore_patterns:
|
|
322
|
+
- filepath: "terraform/modules/admin/*.json"
|
|
323
|
+
- action: "s3:*"
|
|
324
|
+
filepath: "policies/s3-admin-policy.json"
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**📖 Complete documentation:**
|
|
328
|
+
- [Check Reference Guide](docs/check-reference.md) - All 18 checks with examples
|
|
329
|
+
- [Configuration Guide](docs/configuration.md) - Full configuration options
|
|
330
|
+
- [Condition Requirements](docs/condition-requirements.md) - Action-specific requirements
|
|
331
|
+
- [Privilege Escalation Detection](docs/privilege-escalation.md) - How privilege escalation works
|
|
332
|
+
|
|
333
|
+
## Output Formats & GitHub Integration
|
|
334
|
+
|
|
335
|
+
### Output Formats
|
|
336
|
+
- **Console** - Clean terminal output with colors
|
|
337
|
+
- **Enhanced** - Visual output with progress bars
|
|
338
|
+
- **JSON** - Structured data for automation
|
|
339
|
+
- **Markdown** - GitHub PR comments
|
|
340
|
+
- **SARIF** - GitHub Code Scanning integration
|
|
341
|
+
- **CSV** - Spreadsheet analysis
|
|
342
|
+
- **HTML** - Interactive reports
|
|
343
|
+
|
|
344
|
+
### GitHub PR Integration
|
|
345
|
+
|
|
346
|
+
**Three comment modes (use any combination):**
|
|
347
|
+
- `--github-comment` - Summary in PR conversation
|
|
348
|
+
- `--github-review` - Line-specific review comments on files
|
|
349
|
+
- `--github-summary` - Overview in GitHub Actions summary tab
|
|
350
|
+
|
|
351
|
+
**Smart comment management:**
|
|
352
|
+
- Automatically cleans up old comments from previous runs
|
|
353
|
+
- Updates summaries instead of duplicating
|
|
354
|
+
- No stale comments left behind
|
|
355
|
+
|
|
356
|
+
**📖 See [GitHub Integration Guide](docs/github-actions-workflows.md) for detailed examples**
|
|
357
|
+
|
|
358
|
+
## AWS Access Analyzer (Optional)
|
|
359
|
+
|
|
360
|
+
In addition to the 18 built-in checks, optionally enable AWS Access Analyzer for additional validation capabilities that require AWS credentials:
|
|
361
|
+
|
|
362
|
+
### Access Analyzer Capabilities
|
|
363
|
+
|
|
364
|
+
**Custom Policy Checks:**
|
|
365
|
+
- `check-access-not-granted` - Verify policies DON'T grant specific actions (max 100 actions)
|
|
366
|
+
- `check-no-new-access` - Compare against baseline to detect permission creep
|
|
367
|
+
- `check-no-public-access` - Validate 29+ resource types for public exposure
|
|
368
|
+
|
|
369
|
+
**Example:**
|
|
370
|
+
```bash
|
|
371
|
+
# Prevent dangerous actions
|
|
372
|
+
iam-validator analyze --path policies/ \
|
|
373
|
+
--check-access-not-granted "s3:DeleteBucket iam:AttachUserPolicy"
|
|
374
|
+
|
|
375
|
+
# Compare against baseline
|
|
376
|
+
iam-validator analyze --path new-policy.json \
|
|
377
|
+
--check-no-new-access baseline-policy.json
|
|
378
|
+
|
|
379
|
+
# Check for public access
|
|
380
|
+
iam-validator analyze --path bucket-policy.json \
|
|
381
|
+
--policy-type RESOURCE_POLICY \
|
|
382
|
+
--check-no-public-access \
|
|
383
|
+
--public-access-resource-type "AWS::S3::Bucket"
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
**Supported Policy Types:**
|
|
387
|
+
- `IDENTITY_POLICY` (default) - User/role policies
|
|
388
|
+
- `RESOURCE_POLICY` - S3, SNS, KMS resource policies
|
|
389
|
+
- `SERVICE_CONTROL_POLICY` - AWS Organizations SCPs
|
|
390
|
+
- `RESOURCE_CONTROL_POLICY` - AWS Organizations RCPs (2024)
|
|
391
|
+
|
|
392
|
+
**📖 See [Access Analyzer documentation](docs/custom-checks.md) for complete details**
|
|
393
|
+
|
|
394
|
+
## 📚 Documentation
|
|
395
|
+
|
|
396
|
+
**Guides:**
|
|
397
|
+
- [Check Reference](docs/check-reference.md) - All 18 checks with examples
|
|
398
|
+
- [Configuration Guide](docs/configuration.md) - Customize checks and behavior
|
|
399
|
+
- [GitHub Actions Guide](docs/github-actions-workflows.md) - CI/CD integration
|
|
400
|
+
- [Python Library Guide](docs/python-library-usage.md) - Use as Python package
|
|
401
|
+
- [Contributing Guide](CONTRIBUTING.md) - How to contribute
|
|
402
|
+
|
|
403
|
+
**Examples:**
|
|
404
|
+
- [Configuration Examples](examples/configs/) - 9 config file templates
|
|
405
|
+
- [Workflow Examples](examples/github-actions/) - GitHub Actions workflows
|
|
406
|
+
- [Custom Checks](examples/custom_checks/) - Add your own validation rules
|
|
407
|
+
|
|
408
|
+
## 🤝 Contributing
|
|
409
|
+
|
|
410
|
+
Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
|
411
|
+
|
|
412
|
+
**Quick start:**
|
|
413
|
+
```bash
|
|
414
|
+
git clone https://github.com/YOUR-USERNAME/iam-policy-validator.git
|
|
415
|
+
cd iam-policy-validator
|
|
416
|
+
uv sync --extra dev
|
|
417
|
+
uv run pytest
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
## 📄 License
|
|
421
|
+
|
|
422
|
+
MIT License - see [LICENSE](LICENSE) file for details.
|
|
423
|
+
|
|
424
|
+
**Third-party code:** ARN pattern matching in [iam_validator/sdk/arn_matching.py](iam_validator/sdk/arn_matching.py) is derived from [Parliament](https://github.com/duo-labs/parliament) (BSD 3-Clause License).
|
|
425
|
+
|
|
426
|
+
## 🆘 Support
|
|
427
|
+
|
|
428
|
+
- **Issues**: [GitHub Issues](https://github.com/boogy/iam-policy-validator/issues)
|
|
429
|
+
- **Discussions**: [GitHub Discussions](https://github.com/boogy/iam-policy-validator/discussions)
|
|
@@ -1,53 +1,54 @@
|
|
|
1
1
|
iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
|
|
2
2
|
iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
|
|
3
|
-
iam_validator/__version__.py,sha256=
|
|
3
|
+
iam_validator/__version__.py,sha256=hhtguX-fvQWNuUzOdKMFXPyDTbhXhMm7VQgXxQD2xCQ,206
|
|
4
4
|
iam_validator/checks/__init__.py,sha256=eDiDlVon0CwWGSBnZgM-arn1i5R5ZSG89pgR-ifETxE,1782
|
|
5
|
-
iam_validator/checks/action_condition_enforcement.py,sha256=
|
|
6
|
-
iam_validator/checks/action_resource_matching.py,sha256=
|
|
5
|
+
iam_validator/checks/action_condition_enforcement.py,sha256=n-F7NEmQm76Hs-Aj5qxgXney3MpkzbWElZUu1Ig73pw,36723
|
|
6
|
+
iam_validator/checks/action_resource_matching.py,sha256=X9dqWy1s_-h1rA81wZRLOxAVLmUHlGVPjxMo0WKIlwM,17433
|
|
7
7
|
iam_validator/checks/action_validation.py,sha256=IpxtTsk58f2zEZ-xzAoyHw4QK8BCRV43OffP-8ydf9E,2578
|
|
8
8
|
iam_validator/checks/condition_key_validation.py,sha256=E-doe2QjvKSkyjXZO9TBp0QS7M0Fv2oYYQQ9738QNxg,3918
|
|
9
9
|
iam_validator/checks/condition_type_mismatch.py,sha256=qAbP6pP_vM1aBvIBRHji56XLH_5cQI4cDhpMQe19CHM,10588
|
|
10
|
-
iam_validator/checks/full_wildcard.py,sha256=
|
|
10
|
+
iam_validator/checks/full_wildcard.py,sha256=0_F6h4goWlc3DuZwo1F9YGw5hvpnkfZxYSDxhXXK50I,2449
|
|
11
11
|
iam_validator/checks/mfa_condition_check.py,sha256=s7K2r9hxlJI1KWk8qXl-JOWE6jLIhpxooK26Pr7acKs,4915
|
|
12
|
-
iam_validator/checks/policy_size.py,sha256=
|
|
13
|
-
iam_validator/checks/policy_type_validation.py,sha256=
|
|
14
|
-
iam_validator/checks/principal_validation.py,sha256=
|
|
15
|
-
iam_validator/checks/resource_validation.py,sha256=
|
|
16
|
-
iam_validator/checks/sensitive_action.py,sha256=
|
|
17
|
-
iam_validator/checks/service_wildcard.py,sha256=
|
|
12
|
+
iam_validator/checks/policy_size.py,sha256=ibgmrErpkz6OfUAN6bFuHe1KHzpzzra9gHwNtVAkPWc,5729
|
|
13
|
+
iam_validator/checks/policy_type_validation.py,sha256=9qmrA8CXwsVpCU4rT0RrqDXgVOzNamMEpdg3cXWAtBI,15213
|
|
14
|
+
iam_validator/checks/principal_validation.py,sha256=Bm4pH6eiJLDa9ID7UyM63phgffh-P5DpPpSBUbYyVn8,29851
|
|
15
|
+
iam_validator/checks/resource_validation.py,sha256=fGi9QuX-lIHDtLm8xB3VReFFhbZpQ2Yub-FKRafQCkw,5984
|
|
16
|
+
iam_validator/checks/sensitive_action.py,sha256=mdl4g67HBioYTvAvar9CaTjxfaPvpYkNo9phL4E1c1w,9794
|
|
17
|
+
iam_validator/checks/service_wildcard.py,sha256=CiQQoti06nqVgvH-HpBIjoW23tnTJqDU4S-ZnM1DwsA,4218
|
|
18
18
|
iam_validator/checks/set_operator_validation.py,sha256=1XjOdf-xk-m6m1bODuHsELZccriGqOJTDI-HCcuId80,7464
|
|
19
19
|
iam_validator/checks/sid_uniqueness.py,sha256=1Ux9W1hPPhzgdCzfxwxvD-nSBRo1SyrxFWlnTXDcOys,6887
|
|
20
|
-
iam_validator/checks/wildcard_action.py,sha256=
|
|
21
|
-
iam_validator/checks/wildcard_resource.py,sha256=
|
|
20
|
+
iam_validator/checks/wildcard_action.py,sha256=XAVuk5L9dQqiWPgd3HJXGNmYr2bh2szJMVVcHSBXb_8,2140
|
|
21
|
+
iam_validator/checks/wildcard_resource.py,sha256=IEpyoU4mA3t2kRxSwVavtROYIyF0Bq1xZJAL9P7XbVQ,5582
|
|
22
22
|
iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
|
|
23
23
|
iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
|
|
24
|
-
iam_validator/checks/utils/sensitive_action_matcher.py,sha256=
|
|
25
|
-
iam_validator/checks/utils/wildcard_expansion.py,sha256=
|
|
24
|
+
iam_validator/checks/utils/sensitive_action_matcher.py,sha256=tcWK4nImpSVNia0FUsN2uLK9LM5EnzjRFtaPQLHZaLw,10667
|
|
25
|
+
iam_validator/checks/utils/wildcard_expansion.py,sha256=fSSoquVdVZaVWS_qBxAx7LMOzxgHed4ffQ6OAZnuqos,3132
|
|
26
26
|
iam_validator/commands/__init__.py,sha256=M-5bo8w0TCWydK0cXgJyPD2fmk8bpQs-3b26YbgLzlc,565
|
|
27
|
-
iam_validator/commands/analyze.py,sha256=
|
|
27
|
+
iam_validator/commands/analyze.py,sha256=rvLBJ5_A3HB530xtixhaIsC19QON68olEQnn8TievgI,20784
|
|
28
28
|
iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
|
|
29
29
|
iam_validator/commands/cache.py,sha256=p4ucRVuh42sbK3Lk0b610L3ofAR5TnUreF00fpO6VFg,14219
|
|
30
30
|
iam_validator/commands/download_services.py,sha256=KKz3ybMLT8DQUf9aFZ0tilJ-o1b6PE8Pf1pC4K6cT8I,9175
|
|
31
31
|
iam_validator/commands/post_to_pr.py,sha256=CvUXs2xvO-UhluxdfNM6F0TCWD8hDBEOiYw60fm1Dms,2363
|
|
32
|
-
iam_validator/commands/validate.py,sha256=
|
|
32
|
+
iam_validator/commands/validate.py,sha256=Eik-w613zCnX7hUHziBq4k5la3e3qJ0CO1__7aw-gBk,23554
|
|
33
33
|
iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
|
|
34
|
-
iam_validator/core/access_analyzer.py,sha256=
|
|
35
|
-
iam_validator/core/access_analyzer_report.py,sha256=
|
|
36
|
-
iam_validator/core/aws_fetcher.py,sha256=
|
|
34
|
+
iam_validator/core/access_analyzer.py,sha256=8GgkR-vCkCtSxtXGywvQNBPYq-rvDLexUuLSyflq0V4,24520
|
|
35
|
+
iam_validator/core/access_analyzer_report.py,sha256=O17gagknvkNMTTlq7BrLM68FjlCEm4LjIKD9oqxEbPg,24860
|
|
36
|
+
iam_validator/core/aws_fetcher.py,sha256=cZFo5JMSoNLx1tpM6NzYr2cnq8Bvc2KQx2nJDmo69lc,36504
|
|
37
37
|
iam_validator/core/check_registry.py,sha256=cMjtJROkZOLzXxl-mTdLYHdxyajNnOsaHGs-EeaSZ7k,21741
|
|
38
38
|
iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
|
|
39
39
|
iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
|
|
40
|
-
iam_validator/core/
|
|
40
|
+
iam_validator/core/constants.py,sha256=oblMWsjoroIhwjYgZdcyLxaATsGeR99zQwRg6h59Nlo,3145
|
|
41
|
+
iam_validator/core/models.py,sha256=59yqvHoX3nCSJyQDmWCuEsQzNz9PNiF7um7A1wti-2w,12176
|
|
41
42
|
iam_validator/core/policy_checks.py,sha256=Uz2yCsqRaoIja31F4ZM-39a1pHv51yZqKyWWkGUZKNY,26489
|
|
42
43
|
iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
|
|
43
|
-
iam_validator/core/pr_commenter.py,sha256=
|
|
44
|
-
iam_validator/core/report.py,sha256=
|
|
44
|
+
iam_validator/core/pr_commenter.py,sha256=MU-t7SfdHUpSc6BDbh8_dNAbxDiG-bZBCry-jUXivAc,15066
|
|
45
|
+
iam_validator/core/report.py,sha256=j6uWlFL6Xavl4BnpaQtQoxFOEgKEiuY0IYBq8I9DH5Q,34134
|
|
45
46
|
iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
|
|
46
47
|
iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
|
|
47
48
|
iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
|
|
48
49
|
iam_validator/core/config/category_suggestions.py,sha256=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
|
|
49
50
|
iam_validator/core/config/condition_requirements.py,sha256=1PuADTB9pLqh-kNUGC7kSU6LMLtXMSc003tvI7qKeAY,5170
|
|
50
|
-
iam_validator/core/config/config_loader.py,sha256=
|
|
51
|
+
iam_validator/core/config/config_loader.py,sha256=7YkuPnroR-Up5CUTQOXIyS_b732WrzNn8o1EH9O6lyI,17730
|
|
51
52
|
iam_validator/core/config/defaults.py,sha256=w5ievxkqki3zYr7NaREoWtVx5rTfxBpZlgoNdovcILs,27112
|
|
52
53
|
iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
|
|
53
54
|
iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
|
|
@@ -63,10 +64,10 @@ iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFV
|
|
|
63
64
|
iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
|
|
64
65
|
iam_validator/core/formatters/sarif.py,sha256=O3pn7whqFq5xxk-tuoqSb2k4Fk5ai_A2SKX_ph8GLV4,10469
|
|
65
66
|
iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
|
|
66
|
-
iam_validator/integrations/github_integration.py,sha256=
|
|
67
|
+
iam_validator/integrations/github_integration.py,sha256=QoPkaxdRDQTzmHN4cKEXoGcn8BRv37JW4IvD2W5jEtc,26474
|
|
67
68
|
iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
|
|
68
69
|
iam_validator/sdk/__init__.py,sha256=fRDSXAclGmCU3KDft4StL8JUcpAsdzwIRf8mVj461q0,5306
|
|
69
|
-
iam_validator/sdk/arn_matching.py,sha256=
|
|
70
|
+
iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urchEFDA,12778
|
|
70
71
|
iam_validator/sdk/context.py,sha256=SBFeedu8rhCzFA-zC2cH4wLZxEJT6XOW30hIZAyXPVU,6826
|
|
71
72
|
iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
|
|
72
73
|
iam_validator/sdk/helpers.py,sha256=OVBg4xrW95LT74wXCg1LQkba9kw5RfFqeCLuTqhgL-A,5697
|
|
@@ -74,9 +75,9 @@ iam_validator/sdk/policy_utils.py,sha256=CZS1OGSdiWsd2lsCwg0BDcUNWa61tUwgvn-P5rK
|
|
|
74
75
|
iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
|
|
75
76
|
iam_validator/utils/__init__.py,sha256=V8u-SSdnL4a7NwF-yg9x0JRl5epKAXEs2f5RiwK2qPo,856
|
|
76
77
|
iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
|
|
77
|
-
iam_validator/utils/regex.py,sha256=
|
|
78
|
-
iam_policy_validator-1.
|
|
79
|
-
iam_policy_validator-1.
|
|
80
|
-
iam_policy_validator-1.
|
|
81
|
-
iam_policy_validator-1.
|
|
82
|
-
iam_policy_validator-1.
|
|
78
|
+
iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
|
|
79
|
+
iam_policy_validator-1.7.1.dist-info/METADATA,sha256=WCjnDcJ38j-LRUz2EwdT2b2lX2IOTkW3xT1SLtCxiWY,15343
|
|
80
|
+
iam_policy_validator-1.7.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
81
|
+
iam_policy_validator-1.7.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
|
|
82
|
+
iam_policy_validator-1.7.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
|
|
83
|
+
iam_policy_validator-1.7.1.dist-info/RECORD,,
|
iam_validator/__version__.py
CHANGED
|
@@ -491,6 +491,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
491
491
|
if re.match(f"^{wildcard_pattern}$", statement_action):
|
|
492
492
|
return True
|
|
493
493
|
except re.error:
|
|
494
|
+
# Invalid regex pattern - skip this match attempt
|
|
494
495
|
pass
|
|
495
496
|
|
|
496
497
|
# AWS wildcard match in statement_action (e.g., "iam:Creat*" in policy)
|
|
@@ -507,6 +508,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
507
508
|
if re.match(f"^{stmt_wildcard_pattern}$", required_action):
|
|
508
509
|
return True
|
|
509
510
|
except re.error:
|
|
511
|
+
# Invalid regex pattern - skip this match attempt
|
|
510
512
|
pass
|
|
511
513
|
|
|
512
514
|
# Check if statement wildcard overlaps with any of our action patterns
|
|
@@ -794,7 +796,7 @@ class ActionConditionEnforcementCheck(PolicyCheck):
|
|
|
794
796
|
|
|
795
797
|
# Build example based on condition key type
|
|
796
798
|
if example:
|
|
797
|
-
parts.append(f"Example:\n{example}")
|
|
799
|
+
parts.append(f"Example:\n```json\n{example}\n```")
|
|
798
800
|
else:
|
|
799
801
|
# Auto-generate example
|
|
800
802
|
example_lines = ['Add to "Condition" block:', f' "{operator}": {{']
|
|
@@ -27,6 +27,8 @@ from iam_validator.core.models import Statement, ValidationIssue
|
|
|
27
27
|
from iam_validator.sdk.arn_matching import (
|
|
28
28
|
arn_strictly_valid,
|
|
29
29
|
convert_aws_pattern_to_wildcard,
|
|
30
|
+
has_template_variables,
|
|
31
|
+
normalize_template_variables,
|
|
30
32
|
)
|
|
31
33
|
|
|
32
34
|
|
|
@@ -71,6 +73,13 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
71
73
|
"""
|
|
72
74
|
issues = []
|
|
73
75
|
|
|
76
|
+
# Check if template variable support is enabled (default: true)
|
|
77
|
+
# Try global settings first, then check-specific config
|
|
78
|
+
allow_template_variables = config.root_config.get("settings", {}).get(
|
|
79
|
+
"allow_template_variables",
|
|
80
|
+
config.config.get("allow_template_variables", True),
|
|
81
|
+
)
|
|
82
|
+
|
|
74
83
|
# Get actions and resources
|
|
75
84
|
actions = statement.get_actions()
|
|
76
85
|
resources = statement.get_resources()
|
|
@@ -157,7 +166,13 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
157
166
|
|
|
158
167
|
# Check if any policy resource matches this ARN pattern
|
|
159
168
|
for resource in resources:
|
|
160
|
-
|
|
169
|
+
# Normalize template variables (Terraform/CloudFormation) before matching
|
|
170
|
+
# This allows policies with ${aws_account_id}, ${AWS::AccountId}, etc.
|
|
171
|
+
validation_resource = resource
|
|
172
|
+
if allow_template_variables and has_template_variables(resource):
|
|
173
|
+
validation_resource = normalize_template_variables(resource)
|
|
174
|
+
|
|
175
|
+
if arn_strictly_valid(wildcard_pattern, validation_resource, resource_name):
|
|
161
176
|
match_found = True
|
|
162
177
|
break
|
|
163
178
|
|
|
@@ -185,8 +200,8 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
185
200
|
issues.append(
|
|
186
201
|
self._create_mismatch_issue(
|
|
187
202
|
action=action,
|
|
188
|
-
required_format=required_formats[0]["format"] if required_formats else "",
|
|
189
|
-
required_type=required_formats[0]["type"] if required_formats else "",
|
|
203
|
+
required_format=(required_formats[0]["format"] if required_formats else ""),
|
|
204
|
+
required_type=(required_formats[0]["type"] if required_formats else ""),
|
|
190
205
|
provided_resources=resources,
|
|
191
206
|
statement_idx=statement_idx,
|
|
192
207
|
statement_sid=statement_sid,
|
|
@@ -236,9 +251,11 @@ class ActionResourceMatchingCheck(PolicyCheck):
|
|
|
236
251
|
issue_type="resource_mismatch",
|
|
237
252
|
message=message,
|
|
238
253
|
action=action,
|
|
239
|
-
resource=
|
|
240
|
-
|
|
241
|
-
|
|
254
|
+
resource=(
|
|
255
|
+
", ".join(provided_resources)
|
|
256
|
+
if len(provided_resources) <= 3
|
|
257
|
+
else f"{provided_resources[0]}..."
|
|
258
|
+
),
|
|
242
259
|
suggestion=suggestion,
|
|
243
260
|
line_number=line_number,
|
|
244
261
|
)
|
|
@@ -50,7 +50,11 @@ class FullWildcardCheck(PolicyCheck):
|
|
|
50
50
|
example = config.config.get("example", "")
|
|
51
51
|
|
|
52
52
|
# Combine suggestion + example
|
|
53
|
-
suggestion =
|
|
53
|
+
suggestion = (
|
|
54
|
+
f"{suggestion_text}\nExample:\n```json\n{example}\n```"
|
|
55
|
+
if example
|
|
56
|
+
else suggestion_text
|
|
57
|
+
)
|
|
54
58
|
|
|
55
59
|
issues.append(
|
|
56
60
|
ValidationIssue(
|