iam-policy-validator 1.14.6__py3-none-any.whl → 1.15.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.
- {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/METADATA +34 -23
- {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/RECORD +42 -29
- iam_policy_validator-1.15.0.dist-info/entry_points.txt +4 -0
- iam_validator/__version__.py +1 -1
- iam_validator/checks/__init__.py +2 -0
- iam_validator/checks/action_validation.py +91 -27
- iam_validator/checks/not_action_not_resource.py +163 -0
- iam_validator/checks/resource_validation.py +132 -81
- iam_validator/checks/wildcard_resource.py +136 -6
- iam_validator/commands/__init__.py +3 -0
- iam_validator/commands/cache.py +66 -24
- iam_validator/commands/completion.py +94 -15
- iam_validator/commands/mcp.py +210 -0
- iam_validator/commands/query.py +489 -65
- iam_validator/core/aws_service/__init__.py +5 -1
- iam_validator/core/aws_service/cache.py +20 -0
- iam_validator/core/aws_service/fetcher.py +180 -11
- iam_validator/core/aws_service/storage.py +14 -6
- iam_validator/core/aws_service/validators.py +32 -41
- iam_validator/core/check_registry.py +100 -35
- iam_validator/core/config/aws_global_conditions.py +13 -0
- iam_validator/core/config/check_documentation.py +104 -51
- iam_validator/core/config/config_loader.py +39 -3
- iam_validator/core/config/defaults.py +6 -0
- iam_validator/core/constants.py +11 -4
- iam_validator/core/models.py +39 -14
- iam_validator/mcp/__init__.py +162 -0
- iam_validator/mcp/models.py +118 -0
- iam_validator/mcp/server.py +2928 -0
- iam_validator/mcp/session_config.py +319 -0
- iam_validator/mcp/templates/__init__.py +79 -0
- iam_validator/mcp/templates/builtin.py +856 -0
- iam_validator/mcp/tools/__init__.py +72 -0
- iam_validator/mcp/tools/generation.py +888 -0
- iam_validator/mcp/tools/org_config_tools.py +263 -0
- iam_validator/mcp/tools/query.py +395 -0
- iam_validator/mcp/tools/validation.py +376 -0
- iam_validator/sdk/__init__.py +64 -63
- iam_validator/sdk/context.py +3 -2
- iam_validator/sdk/policy_utils.py +31 -5
- iam_policy_validator-1.14.6.dist-info/entry_points.txt +0 -2
- {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -71,6 +71,19 @@ AWS_GLOBAL_CONDITION_KEYS = {
|
|
|
71
71
|
"aws:ViaAWSService": "Bool", # Whether AWS service made the request
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
# Global condition keys that restrict resource scope.
|
|
75
|
+
# These conditions are always valid for all services and directly constrain
|
|
76
|
+
# which resources can be accessed, making them suitable for lowering severity
|
|
77
|
+
# when used with wildcard resources.
|
|
78
|
+
# Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-resourceaccount
|
|
79
|
+
GLOBAL_RESOURCE_SCOPING_CONDITION_KEYS = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"aws:ResourceAccount", # Limits to specific AWS account(s)
|
|
82
|
+
"aws:ResourceOrgID", # Limits to specific AWS Organization
|
|
83
|
+
"aws:ResourceOrgPaths", # Limits to specific OU paths
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
74
87
|
# Patterns that should be recognized (wildcards and tag-based keys)
|
|
75
88
|
# These allow things like aws:RequestTag/Department or aws:PrincipalTag/Environment
|
|
76
89
|
# Uses centralized tag key character class from constants
|
|
@@ -9,6 +9,18 @@ Used to enhance ValidationIssue objects with actionable guidance.
|
|
|
9
9
|
from dataclasses import dataclass, field
|
|
10
10
|
from typing import ClassVar
|
|
11
11
|
|
|
12
|
+
# Risk category icons for display in PR comments
|
|
13
|
+
RISK_CATEGORY_ICONS = {
|
|
14
|
+
"privilege_escalation": "🔐",
|
|
15
|
+
"data_exfiltration": "📤",
|
|
16
|
+
"denial_of_service": "🚫",
|
|
17
|
+
"resource_exposure": "🌐",
|
|
18
|
+
"credential_exposure": "🔑",
|
|
19
|
+
"compliance": "📋",
|
|
20
|
+
"configuration": "⚙️",
|
|
21
|
+
"validation": "✅",
|
|
22
|
+
}
|
|
23
|
+
|
|
12
24
|
|
|
13
25
|
@dataclass
|
|
14
26
|
class CheckDocumentation:
|
|
@@ -19,12 +31,14 @@ class CheckDocumentation:
|
|
|
19
31
|
risk_explanation: Why this issue is a security risk
|
|
20
32
|
documentation_url: Link to relevant AWS docs or runbook
|
|
21
33
|
remediation_steps: Step-by-step fix guidance
|
|
34
|
+
risk_category: Category of risk (e.g., "privilege_escalation", "data_exfiltration")
|
|
22
35
|
"""
|
|
23
36
|
|
|
24
37
|
check_id: str
|
|
25
38
|
risk_explanation: str
|
|
26
39
|
documentation_url: str
|
|
27
40
|
remediation_steps: list[str] = field(default_factory=list)
|
|
41
|
+
risk_category: str | None = None
|
|
28
42
|
|
|
29
43
|
|
|
30
44
|
class CheckDocumentationRegistry:
|
|
@@ -80,15 +94,16 @@ CheckDocumentationRegistry.register(
|
|
|
80
94
|
CheckDocumentation(
|
|
81
95
|
check_id="action_validation",
|
|
82
96
|
risk_explanation=(
|
|
83
|
-
"Invalid
|
|
97
|
+
"Invalid `Action`s may silently fail to grant intended permissions, "
|
|
84
98
|
"or indicate a typo that could expose unintended access."
|
|
85
99
|
),
|
|
86
100
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
87
101
|
remediation_steps=[
|
|
88
|
-
"Verify the
|
|
89
|
-
"Use the IAM policy simulator to test your intended permissions",
|
|
90
|
-
"Check for common typos (e.g.,
|
|
102
|
+
"Verify the `Action` name against AWS documentation for the target service",
|
|
103
|
+
"Use the AWS IAM policy simulator to test your intended permissions",
|
|
104
|
+
"Check for common typos (e.g., `S3` vs `s3`, `GetObjects` vs `GetObject`)",
|
|
91
105
|
],
|
|
106
|
+
risk_category="validation",
|
|
92
107
|
)
|
|
93
108
|
)
|
|
94
109
|
|
|
@@ -96,15 +111,16 @@ CheckDocumentationRegistry.register(
|
|
|
96
111
|
CheckDocumentation(
|
|
97
112
|
check_id="condition_key_validation",
|
|
98
113
|
risk_explanation=(
|
|
99
|
-
"Invalid condition keys are silently ignored by IAM, meaning your "
|
|
114
|
+
"Invalid condition keys are silently ignored by AWS IAM, meaning your "
|
|
100
115
|
"intended access restrictions may not be enforced."
|
|
101
116
|
),
|
|
102
117
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_condition-keys.html",
|
|
103
118
|
remediation_steps=[
|
|
104
|
-
"Verify the
|
|
105
|
-
"Check AWS documentation for the correct key name and format",
|
|
106
|
-
"Use global condition keys (aws
|
|
119
|
+
"Verify the `Condition` key exists for the target service",
|
|
120
|
+
"Check AWS documentation for the correct `Condition` key name and format for the target service",
|
|
121
|
+
"Use global condition keys (`aws:*`) for cross-service restrictions",
|
|
107
122
|
],
|
|
123
|
+
risk_category="validation",
|
|
108
124
|
)
|
|
109
125
|
)
|
|
110
126
|
|
|
@@ -112,15 +128,16 @@ CheckDocumentationRegistry.register(
|
|
|
112
128
|
CheckDocumentation(
|
|
113
129
|
check_id="condition_type_mismatch",
|
|
114
130
|
risk_explanation=(
|
|
115
|
-
"Using the wrong condition operator type (e.g., StringEquals with a "
|
|
131
|
+
"Using the wrong condition operator type (e.g., `StringEquals` with a "
|
|
116
132
|
"numeric value) may cause unexpected behavior or silent failures."
|
|
117
133
|
),
|
|
118
134
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_condition_operators.html",
|
|
119
135
|
remediation_steps=[
|
|
120
|
-
"Match the condition operator to the key's data type",
|
|
121
|
-
"Use String operators for string keys, Numeric for numbers, Date for timestamps",
|
|
122
|
-
"Consider using IfExists variants for optional conditions",
|
|
136
|
+
"Match the condition operator to the `Condition` key's data type",
|
|
137
|
+
"Use `String` operators for string keys, `Numeric` for numbers, `Date` for timestamps",
|
|
138
|
+
"Consider using `IfExists` variants for optional conditions",
|
|
123
139
|
],
|
|
140
|
+
risk_category="validation",
|
|
124
141
|
)
|
|
125
142
|
)
|
|
126
143
|
|
|
@@ -128,15 +145,16 @@ CheckDocumentationRegistry.register(
|
|
|
128
145
|
CheckDocumentation(
|
|
129
146
|
check_id="resource_validation",
|
|
130
147
|
risk_explanation=(
|
|
131
|
-
"Invalid
|
|
148
|
+
"Invalid `Resource` ARNs may silently fail to match intended resources, "
|
|
132
149
|
"leaving permissions ineffective or overly broad."
|
|
133
150
|
),
|
|
134
151
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
135
152
|
remediation_steps=[
|
|
136
|
-
"Verify ARN format matches the target service's documentation",
|
|
153
|
+
"Verify `Resource` ARN format matches the target service's documentation",
|
|
137
154
|
"Ensure region and account ID are correct or use wildcards intentionally",
|
|
138
|
-
"Test the policy with IAM policy simulator before deployment",
|
|
155
|
+
"Test the policy with AWS IAM policy simulator before deployment",
|
|
139
156
|
],
|
|
157
|
+
risk_category="validation",
|
|
140
158
|
)
|
|
141
159
|
)
|
|
142
160
|
|
|
@@ -153,6 +171,7 @@ CheckDocumentationRegistry.register(
|
|
|
153
171
|
"Use descriptive SIDs that indicate the statement's purpose",
|
|
154
172
|
"Consider a naming convention like 'AllowS3ReadAccess' or 'DenyPublicAccess'",
|
|
155
173
|
],
|
|
174
|
+
risk_category="compliance",
|
|
156
175
|
)
|
|
157
176
|
)
|
|
158
177
|
|
|
@@ -161,15 +180,16 @@ CheckDocumentationRegistry.register(
|
|
|
161
180
|
check_id="policy_size",
|
|
162
181
|
risk_explanation=(
|
|
163
182
|
"Policies exceeding AWS size limits cannot be attached to IAM entities. "
|
|
164
|
-
"Inline policies have a 2KB limit, managed policies have a 6KB limit."
|
|
183
|
+
"Inline policies have a 2KB limit, managed policies have a 6KB limit (for the entire policy document)."
|
|
165
184
|
),
|
|
166
185
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_iam-quotas.html",
|
|
167
186
|
remediation_steps=[
|
|
168
187
|
"Split large policies into multiple smaller policies",
|
|
169
188
|
"Use managed policies instead of inline policies for larger permissions",
|
|
170
|
-
"Remove redundant statements or consolidate similar
|
|
189
|
+
"Remove redundant statements or consolidate similar `Action`s",
|
|
171
190
|
"Consider using permission boundaries or SCPs for broad restrictions",
|
|
172
191
|
],
|
|
192
|
+
risk_category="validation",
|
|
173
193
|
)
|
|
174
194
|
)
|
|
175
195
|
|
|
@@ -177,15 +197,16 @@ CheckDocumentationRegistry.register(
|
|
|
177
197
|
CheckDocumentation(
|
|
178
198
|
check_id="policy_structure",
|
|
179
199
|
risk_explanation=(
|
|
180
|
-
"Malformed policy structure will cause IAM to reject the policy entirely, "
|
|
200
|
+
"Malformed policy structure will cause AWS IAM to reject the policy entirely, "
|
|
181
201
|
"preventing any permissions from being granted."
|
|
182
202
|
),
|
|
183
203
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_grammar.html",
|
|
184
204
|
remediation_steps=[
|
|
185
205
|
"Verify the policy follows AWS IAM policy grammar",
|
|
186
|
-
"Ensure all required elements (Version
|
|
187
|
-
"Check that Effect
|
|
206
|
+
"Ensure all required elements (`Version`, `Statement`) are present",
|
|
207
|
+
"Check that `Effect`, `Action`, and `Resource` are properly formatted",
|
|
188
208
|
],
|
|
209
|
+
risk_category="validation",
|
|
189
210
|
)
|
|
190
211
|
)
|
|
191
212
|
|
|
@@ -193,15 +214,16 @@ CheckDocumentationRegistry.register(
|
|
|
193
214
|
CheckDocumentation(
|
|
194
215
|
check_id="set_operator_validation",
|
|
195
216
|
risk_explanation=(
|
|
196
|
-
"Invalid ForAllValues
|
|
217
|
+
"Invalid `ForAllValues`/`ForAnyValue` operators may cause conditions to "
|
|
197
218
|
"behave unexpectedly, potentially granting or denying unintended access."
|
|
198
219
|
),
|
|
199
220
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_multi-value-conditions.html",
|
|
200
221
|
remediation_steps=[
|
|
201
|
-
"Use ForAllValues when ALL values must match the condition",
|
|
202
|
-
"Use ForAnyValue when ANY value matching is sufficient",
|
|
203
|
-
"Consider the empty set behavior: ForAllValues returns true for empty sets",
|
|
222
|
+
"Use `ForAllValues` when ALL values must match the condition",
|
|
223
|
+
"Use `ForAnyValue` when ANY value matching is sufficient",
|
|
224
|
+
"Consider the empty set behavior: `ForAllValues` returns true for empty sets",
|
|
204
225
|
],
|
|
226
|
+
risk_category="validation",
|
|
205
227
|
)
|
|
206
228
|
)
|
|
207
229
|
|
|
@@ -214,10 +236,11 @@ CheckDocumentationRegistry.register(
|
|
|
214
236
|
),
|
|
215
237
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/id_credentials_mfa_configure-api-require.html",
|
|
216
238
|
remediation_steps=[
|
|
217
|
-
"Add
|
|
218
|
-
"Consider using
|
|
239
|
+
"Add `aws:MultiFactorAuthPresent`: `true` condition for sensitive actions",
|
|
240
|
+
"Consider using `aws:MultiFactorAuthAge` to require recent MFA",
|
|
219
241
|
"Ensure MFA is enforced at the identity level as well as policy level",
|
|
220
242
|
],
|
|
243
|
+
risk_category="credential_exposure",
|
|
221
244
|
)
|
|
222
245
|
)
|
|
223
246
|
|
|
@@ -234,6 +257,7 @@ CheckDocumentationRegistry.register(
|
|
|
234
257
|
"Use specific principals instead of wildcards where possible",
|
|
235
258
|
"For service principals, use the canonical format (e.g., 's3.amazonaws.com')",
|
|
236
259
|
],
|
|
260
|
+
risk_category="resource_exposure",
|
|
237
261
|
)
|
|
238
262
|
)
|
|
239
263
|
|
|
@@ -246,10 +270,11 @@ CheckDocumentationRegistry.register(
|
|
|
246
270
|
),
|
|
247
271
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/access_policies.html",
|
|
248
272
|
remediation_steps=[
|
|
249
|
-
"Identity policies: Don't include Principal element",
|
|
250
|
-
"Resource policies: Include Principal element",
|
|
251
|
-
"SCPs: Use only Allow statements with specific conditions",
|
|
273
|
+
"Identity policies: Don't include `Principal` element",
|
|
274
|
+
"Resource policies: Include `Principal` element",
|
|
275
|
+
"SCPs: Use only `Allow` statements with specific conditions",
|
|
252
276
|
],
|
|
277
|
+
risk_category="configuration",
|
|
253
278
|
)
|
|
254
279
|
)
|
|
255
280
|
|
|
@@ -257,15 +282,16 @@ CheckDocumentationRegistry.register(
|
|
|
257
282
|
CheckDocumentation(
|
|
258
283
|
check_id="action_resource_matching",
|
|
259
284
|
risk_explanation=(
|
|
260
|
-
"
|
|
285
|
+
"`Action`s that don't support the specified `Resource`s will silently fail, "
|
|
261
286
|
"resulting in permissions that don't work as intended."
|
|
262
287
|
),
|
|
263
288
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
264
289
|
remediation_steps=[
|
|
265
290
|
"Check AWS documentation for supported resource types per action",
|
|
266
|
-
"Use
|
|
291
|
+
"Use `*` (wildcard) for actions that don't support resource-level permissions",
|
|
267
292
|
"Split statements when actions require different resource types",
|
|
268
293
|
],
|
|
294
|
+
risk_category="validation",
|
|
269
295
|
)
|
|
270
296
|
)
|
|
271
297
|
|
|
@@ -278,11 +304,12 @@ CheckDocumentationRegistry.register(
|
|
|
278
304
|
),
|
|
279
305
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/id_roles_create_for-user.html",
|
|
280
306
|
remediation_steps=[
|
|
281
|
-
"Restrict Principal to specific accounts/roles/users",
|
|
307
|
+
"Restrict `Principal` to specific accounts/roles/users (e.g., `arn:aws:iam::123456789012:role/foo`)",
|
|
282
308
|
"Add conditions to limit who can assume the role",
|
|
283
|
-
"Avoid wildcards in Principal unless absolutely necessary",
|
|
309
|
+
"Avoid wildcards in `Principal` unless absolutely necessary",
|
|
284
310
|
"Use ExternalId for cross-account role assumption",
|
|
285
311
|
],
|
|
312
|
+
risk_category="privilege_escalation",
|
|
286
313
|
)
|
|
287
314
|
)
|
|
288
315
|
|
|
@@ -293,16 +320,16 @@ CheckDocumentationRegistry.register(
|
|
|
293
320
|
CheckDocumentation(
|
|
294
321
|
check_id="wildcard_action",
|
|
295
322
|
risk_explanation=(
|
|
296
|
-
"Wildcard actions (e.g.,
|
|
323
|
+
"Wildcard actions (e.g., `s3:*`) grant all current AND future permissions "
|
|
297
324
|
"for a service, violating least privilege and increasing attack surface."
|
|
298
325
|
),
|
|
299
326
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
300
327
|
remediation_steps=[
|
|
301
|
-
"Replace wildcards with specific
|
|
302
|
-
"Use action groups like
|
|
303
|
-
"
|
|
304
|
-
"Review and reduce permissions periodically",
|
|
328
|
+
"Replace wildcards with specific `Action` lists needed for the use case",
|
|
329
|
+
"Use action groups like `s3:Get*` for read-only access",
|
|
330
|
+
"Review and reduce permissions periodically if not needed",
|
|
305
331
|
],
|
|
332
|
+
risk_category="privilege_escalation",
|
|
306
333
|
)
|
|
307
334
|
)
|
|
308
335
|
|
|
@@ -310,15 +337,17 @@ CheckDocumentationRegistry.register(
|
|
|
310
337
|
CheckDocumentation(
|
|
311
338
|
check_id="wildcard_resource",
|
|
312
339
|
risk_explanation=(
|
|
313
|
-
"Wildcard resources (
|
|
340
|
+
"Wildcard resources (`*`) grant access to ALL resources of a type, "
|
|
314
341
|
"including resources created in the future, violating least privilege."
|
|
315
342
|
),
|
|
316
343
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
317
344
|
remediation_steps=[
|
|
318
|
-
"Specify exact
|
|
319
|
-
"Use resource tags and conditions for dynamic access control",
|
|
345
|
+
"Specify exact `Resource` ARNs when possible",
|
|
346
|
+
"Use resource tags and conditions for dynamic access control (ABAC)",
|
|
320
347
|
"Limit scope to specific accounts, regions, or resource prefixes",
|
|
348
|
+
"Use `aws:ResourceAccount`, `aws:ResourceOrgID`, or `aws:ResourceOrgPaths` conditions to restrict scope",
|
|
321
349
|
],
|
|
350
|
+
risk_category="resource_exposure",
|
|
322
351
|
)
|
|
323
352
|
)
|
|
324
353
|
|
|
@@ -336,6 +365,7 @@ CheckDocumentationRegistry.register(
|
|
|
336
365
|
"Implement permission boundaries to limit maximum possible permissions",
|
|
337
366
|
"Consider using service control policies (SCPs) as guardrails",
|
|
338
367
|
],
|
|
368
|
+
risk_category="privilege_escalation",
|
|
339
369
|
)
|
|
340
370
|
)
|
|
341
371
|
|
|
@@ -343,15 +373,16 @@ CheckDocumentationRegistry.register(
|
|
|
343
373
|
CheckDocumentation(
|
|
344
374
|
check_id="service_wildcard",
|
|
345
375
|
risk_explanation=(
|
|
346
|
-
"Service-level wildcards (e.g.,
|
|
376
|
+
"Service-level wildcards (e.g., `iam:*`) grant all permissions for "
|
|
347
377
|
"an entire service, including destructive and privilege escalation actions."
|
|
348
378
|
),
|
|
349
379
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
350
380
|
remediation_steps=[
|
|
351
381
|
"Replace with specific actions required for the use case",
|
|
352
382
|
"Use AWS managed policies for common patterns",
|
|
353
|
-
"Consider permission boundaries to limit sensitive actions",
|
|
383
|
+
"Consider permission boundaries to limit sensitive actions and enforce least privilege",
|
|
354
384
|
],
|
|
385
|
+
risk_category="privilege_escalation",
|
|
355
386
|
)
|
|
356
387
|
)
|
|
357
388
|
|
|
@@ -359,16 +390,16 @@ CheckDocumentationRegistry.register(
|
|
|
359
390
|
CheckDocumentation(
|
|
360
391
|
check_id="sensitive_action",
|
|
361
392
|
risk_explanation=(
|
|
362
|
-
"Sensitive actions (e.g., iam
|
|
393
|
+
"Sensitive actions (e.g., `iam:*`, `sts:AssumeRole`, `kms:Decrypt`) can lead "
|
|
363
394
|
"to privilege escalation, data exfiltration, or account compromise."
|
|
364
395
|
),
|
|
365
396
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
366
397
|
remediation_steps=[
|
|
367
398
|
"Add conditions to restrict when these actions can be used",
|
|
368
|
-
"Require
|
|
369
|
-
"Limit to specific resources where possible",
|
|
370
|
-
"Implement monitoring and alerting for sensitive action usage",
|
|
399
|
+
"Require Attribute Based Access Control (ABAC) for sensitive operations",
|
|
400
|
+
"Limit to specific resources and accounts where possible",
|
|
371
401
|
],
|
|
402
|
+
risk_category="privilege_escalation",
|
|
372
403
|
)
|
|
373
404
|
)
|
|
374
405
|
|
|
@@ -377,14 +408,36 @@ CheckDocumentationRegistry.register(
|
|
|
377
408
|
check_id="action_condition_enforcement",
|
|
378
409
|
risk_explanation=(
|
|
379
410
|
"Certain sensitive actions should always have conditions to prevent "
|
|
380
|
-
"misuse, such as
|
|
411
|
+
"misuse, such as Account/Organization boundaries, VPC/VPCe restrictions, MFA requirements, or time-based access."
|
|
381
412
|
),
|
|
382
413
|
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_condition.html",
|
|
383
414
|
remediation_steps=[
|
|
384
415
|
"Add appropriate conditions based on the action type",
|
|
385
|
-
"Use aws:
|
|
386
|
-
"Use aws:
|
|
387
|
-
"Use aws:
|
|
416
|
+
"Use `aws:ResourceAccount` and `aws:PrincipalAccount` for account-restricted actions",
|
|
417
|
+
"Use `aws:ResourceOrgID` and `aws:PrincipalOrgID` for organization-restricted actions",
|
|
418
|
+
"Use `aws:SourceVpc` or `aws:SourceVpce` for VPC-restricted actions",
|
|
419
|
+
"Use `aws:SourceIp` for network-restricted actions",
|
|
420
|
+
"Use `aws:RequestedRegion` to limit geographic scope",
|
|
421
|
+
],
|
|
422
|
+
risk_category="compliance",
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
CheckDocumentationRegistry.register(
|
|
427
|
+
CheckDocumentation(
|
|
428
|
+
check_id="not_action_not_resource",
|
|
429
|
+
risk_explanation=(
|
|
430
|
+
"`NotAction` and `NotResource` grant permissions by exclusion rather than "
|
|
431
|
+
"inclusion. This can accidentally grant far more access than intended, "
|
|
432
|
+
"including access to actions and resources created in the future."
|
|
433
|
+
),
|
|
434
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_notaction.html",
|
|
435
|
+
remediation_steps=[
|
|
436
|
+
"Replace `NotAction` with explicit `Action` lists when possible",
|
|
437
|
+
"Replace `NotResource` with specific `Resource` ARNs",
|
|
438
|
+
"If `NotAction` is required, add strict conditions (`aws:SourceIp`, `aws:SourceVpc`, `aws:SourceVpce`, `aws:ResourceAccount`, `aws:ResourceOrgID`, `aws:RequestedRegion`, etc.)",
|
|
439
|
+
"Document why exclusion-based permissions are necessary",
|
|
388
440
|
],
|
|
441
|
+
risk_category="privilege_escalation",
|
|
389
442
|
)
|
|
390
443
|
)
|
|
@@ -47,6 +47,7 @@ KNOWN_CHECK_IDS = frozenset(
|
|
|
47
47
|
"service_wildcard",
|
|
48
48
|
"sensitive_action",
|
|
49
49
|
"action_condition_enforcement",
|
|
50
|
+
"not_action_not_resource",
|
|
50
51
|
]
|
|
51
52
|
)
|
|
52
53
|
|
|
@@ -82,6 +83,7 @@ class CheckConfigSchema(BaseModel):
|
|
|
82
83
|
severity: str | None = None
|
|
83
84
|
description: str | None = None
|
|
84
85
|
ignore_patterns: list[dict[str, Any]] = []
|
|
86
|
+
hide_severities: list[str] | None = None # Per-check severity filtering
|
|
85
87
|
|
|
86
88
|
@field_validator("severity")
|
|
87
89
|
@classmethod
|
|
@@ -90,6 +92,18 @@ class CheckConfigSchema(BaseModel):
|
|
|
90
92
|
raise ValueError(f"Invalid severity: {v}. Must be one of: {sorted(SEVERITY_LEVELS)}")
|
|
91
93
|
return v
|
|
92
94
|
|
|
95
|
+
@field_validator("hide_severities")
|
|
96
|
+
@classmethod
|
|
97
|
+
def validate_hide_severities(cls, v: list[str] | None) -> list[str] | None:
|
|
98
|
+
if v is not None:
|
|
99
|
+
for severity in v:
|
|
100
|
+
if severity not in SEVERITY_LEVELS:
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"Invalid severity in hide_severities: {severity}. "
|
|
103
|
+
f"Must be one of: {sorted(SEVERITY_LEVELS)}"
|
|
104
|
+
)
|
|
105
|
+
return v
|
|
106
|
+
|
|
93
107
|
|
|
94
108
|
class IgnoreSettingsSchema(BaseModel):
|
|
95
109
|
"""Schema for ignore settings."""
|
|
@@ -122,6 +136,7 @@ class SettingsSchema(BaseModel):
|
|
|
122
136
|
severity_labels: dict[str, str | list[str]] = {}
|
|
123
137
|
ignore_settings: IgnoreSettingsSchema = IgnoreSettingsSchema()
|
|
124
138
|
documentation: DocumentationSettingsSchema = DocumentationSettingsSchema()
|
|
139
|
+
hide_severities: list[str] | None = None # Global severity filtering
|
|
125
140
|
|
|
126
141
|
@field_validator("fail_on_severity")
|
|
127
142
|
@classmethod
|
|
@@ -134,6 +149,18 @@ class SettingsSchema(BaseModel):
|
|
|
134
149
|
)
|
|
135
150
|
return v
|
|
136
151
|
|
|
152
|
+
@field_validator("hide_severities")
|
|
153
|
+
@classmethod
|
|
154
|
+
def validate_hide_severities(cls, v: list[str] | None) -> list[str] | None:
|
|
155
|
+
if v is not None:
|
|
156
|
+
for severity in v:
|
|
157
|
+
if severity not in SEVERITY_LEVELS:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Invalid severity in hide_severities: {severity}. "
|
|
160
|
+
f"Must be one of: {sorted(SEVERITY_LEVELS)}"
|
|
161
|
+
)
|
|
162
|
+
return v
|
|
163
|
+
|
|
137
164
|
|
|
138
165
|
class CustomCheckSchema(BaseModel):
|
|
139
166
|
"""Schema for custom check definitions."""
|
|
@@ -446,6 +473,9 @@ class ConfigLoader:
|
|
|
446
473
|
config: Loaded configuration
|
|
447
474
|
registry: Check registry to configure
|
|
448
475
|
"""
|
|
476
|
+
# Get global hide_severities from settings (for fallback)
|
|
477
|
+
global_hide_severities = config.settings.get("hide_severities")
|
|
478
|
+
|
|
449
479
|
# Configure built-in checks
|
|
450
480
|
for check in registry.get_all_checks():
|
|
451
481
|
check_id = check.check_id
|
|
@@ -455,6 +485,13 @@ class ConfigLoader:
|
|
|
455
485
|
existing_config = registry.get_config(check_id)
|
|
456
486
|
existing_enabled = existing_config.enabled if existing_config else True
|
|
457
487
|
|
|
488
|
+
# Parse hide_severities: per-check overrides global
|
|
489
|
+
hide_severities = check_config_dict.get("hide_severities")
|
|
490
|
+
if hide_severities is None:
|
|
491
|
+
hide_severities = global_hide_severities
|
|
492
|
+
if hide_severities is not None:
|
|
493
|
+
hide_severities = frozenset(hide_severities)
|
|
494
|
+
|
|
458
495
|
# Create CheckConfig object
|
|
459
496
|
# If there's explicit config, use it; otherwise preserve existing enabled state
|
|
460
497
|
check_config = CheckConfig(
|
|
@@ -464,9 +501,8 @@ class ConfigLoader:
|
|
|
464
501
|
config=check_config_dict,
|
|
465
502
|
description=check_config_dict.get("description", check.description),
|
|
466
503
|
root_config=config.config_dict, # Pass full config for cross-check access
|
|
467
|
-
ignore_patterns=check_config_dict.get(
|
|
468
|
-
|
|
469
|
-
), # NEW: Ignore patterns
|
|
504
|
+
ignore_patterns=check_config_dict.get("ignore_patterns", []),
|
|
505
|
+
hide_severities=hide_severities,
|
|
470
506
|
)
|
|
471
507
|
|
|
472
508
|
registry.configure_check(check_id, check_config)
|
|
@@ -110,6 +110,12 @@ DEFAULT_CONFIG = {
|
|
|
110
110
|
# Include AWS documentation links alongside org docs
|
|
111
111
|
"include_aws_docs": True,
|
|
112
112
|
},
|
|
113
|
+
# Severity filtering - hide specific severity levels from output
|
|
114
|
+
# When set, issues with these severities will be filtered out globally
|
|
115
|
+
# Can be overridden per-check using check-level hide_severities
|
|
116
|
+
# Valid values: "error", "warning", "info", "critical", "high", "medium", "low"
|
|
117
|
+
# Example: ["low", "info"] - hide low and info severity findings
|
|
118
|
+
"hide_severities": None,
|
|
113
119
|
},
|
|
114
120
|
# ========================================================================
|
|
115
121
|
# AWS IAM Validation Checks (17 checks total)
|
iam_validator/core/constants.py
CHANGED
|
@@ -77,6 +77,17 @@ MEDIUM_SEVERITY_LEVELS = ("warning", "medium")
|
|
|
77
77
|
# Low severity issues (informational)
|
|
78
78
|
LOW_SEVERITY_LEVELS = ("info", "low")
|
|
79
79
|
|
|
80
|
+
# Severity configuration with emoji and action guidance for PR comments
|
|
81
|
+
SEVERITY_CONFIG = {
|
|
82
|
+
"critical": {"emoji": "🔴", "action": "Block deployment"},
|
|
83
|
+
"high": {"emoji": "🟠", "action": "Fix before merge"},
|
|
84
|
+
"medium": {"emoji": "🟡", "action": "Address soon"},
|
|
85
|
+
"low": {"emoji": "🔵", "action": "Consider fixing"},
|
|
86
|
+
"error": {"emoji": "❌", "action": "Must fix - AWS will reject"},
|
|
87
|
+
"warning": {"emoji": "⚠️", "action": "Review"},
|
|
88
|
+
"info": {"emoji": "ℹ️", "action": "Optional"},
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
# ============================================================================
|
|
81
92
|
# GitHub Integration
|
|
82
93
|
# ============================================================================
|
|
@@ -161,10 +172,6 @@ AWS_TAG_KEY_ALLOWED_CHARS = r"a-zA-Z0-9 +\-=._:/@"
|
|
|
161
172
|
# Maximum length for AWS tag keys (per AWS documentation)
|
|
162
173
|
AWS_TAG_KEY_MAX_LENGTH = 128
|
|
163
174
|
|
|
164
|
-
# Tag-key placeholder patterns used in AWS service definitions
|
|
165
|
-
# These patterns indicate where a tag key should be substituted
|
|
166
|
-
AWS_TAG_KEY_PLACEHOLDERS = ("/tag-key", "/${TagKey}", "/${tag-key}")
|
|
167
|
-
|
|
168
175
|
# --- Tag Value Constraints ---
|
|
169
176
|
# Allowed characters in AWS tag values: letters, numbers, spaces, and + - = . _ : / @
|
|
170
177
|
# Same character set as tag keys
|
iam_validator/core/models.py
CHANGED
|
@@ -126,12 +126,24 @@ class Statement(BaseModel):
|
|
|
126
126
|
return []
|
|
127
127
|
return [self.action] if isinstance(self.action, str) else self.action
|
|
128
128
|
|
|
129
|
+
def get_not_actions(self) -> list[str]:
|
|
130
|
+
"""Get list of NotAction values, handling both string and list formats."""
|
|
131
|
+
if self.not_action is None:
|
|
132
|
+
return []
|
|
133
|
+
return [self.not_action] if isinstance(self.not_action, str) else self.not_action
|
|
134
|
+
|
|
129
135
|
def get_resources(self) -> list[str]:
|
|
130
136
|
"""Get list of resources, handling both string and list formats."""
|
|
131
137
|
if self.resource is None:
|
|
132
138
|
return []
|
|
133
139
|
return [self.resource] if isinstance(self.resource, str) else self.resource
|
|
134
140
|
|
|
141
|
+
def get_not_resources(self) -> list[str]:
|
|
142
|
+
"""Get list of NotResource values, handling both string and list formats."""
|
|
143
|
+
if self.not_resource is None:
|
|
144
|
+
return []
|
|
145
|
+
return [self.not_resource] if isinstance(self.not_resource, str) else self.not_resource
|
|
146
|
+
|
|
135
147
|
|
|
136
148
|
class IAMPolicy(BaseModel):
|
|
137
149
|
"""IAM policy document."""
|
|
@@ -179,6 +191,8 @@ class ValidationIssue(BaseModel):
|
|
|
179
191
|
documentation_url: str | None = None
|
|
180
192
|
# Step-by-step remediation guidance
|
|
181
193
|
remediation_steps: list[str] | None = None
|
|
194
|
+
# Risk category for classification (e.g., "privilege_escalation", "data_exfiltration")
|
|
195
|
+
risk_category: str | None = None
|
|
182
196
|
|
|
183
197
|
# Severity level constants (ClassVar to avoid Pydantic treating them as fields)
|
|
184
198
|
VALID_SEVERITIES: ClassVar[frozenset[str]] = frozenset(
|
|
@@ -226,18 +240,23 @@ class ValidationIssue(BaseModel):
|
|
|
226
240
|
Returns:
|
|
227
241
|
Formatted comment string
|
|
228
242
|
"""
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
"
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
# Get severity config with emoji and action guidance
|
|
244
|
+
severity_config = constants.SEVERITY_CONFIG.get(
|
|
245
|
+
self.severity, {"emoji": "•", "action": "Review"}
|
|
246
|
+
)
|
|
247
|
+
emoji = severity_config["emoji"]
|
|
248
|
+
action = severity_config["action"]
|
|
249
|
+
|
|
250
|
+
# Get risk category icon if available
|
|
251
|
+
from iam_validator.core.config.check_documentation import RISK_CATEGORY_ICONS
|
|
252
|
+
|
|
253
|
+
risk_icon = ""
|
|
254
|
+
if self.risk_category:
|
|
255
|
+
icon = RISK_CATEGORY_ICONS.get(self.risk_category, "")
|
|
256
|
+
if icon:
|
|
257
|
+
# Format risk category for display (e.g., "privilege_escalation" -> "Privilege Escalation")
|
|
258
|
+
category_display = self.risk_category.replace("_", " ").title()
|
|
259
|
+
risk_icon = f" | {icon} {category_display}"
|
|
241
260
|
|
|
242
261
|
parts = []
|
|
243
262
|
|
|
@@ -263,13 +282,19 @@ class ValidationIssue(BaseModel):
|
|
|
263
282
|
)
|
|
264
283
|
parts.append(f"<!-- finding-id: {finding_hash} -->\n")
|
|
265
284
|
|
|
285
|
+
# Main issue header with severity, action guidance, and risk category
|
|
286
|
+
parts.append(f"{emoji} **{self.severity.upper()}** - {action}{risk_icon}")
|
|
287
|
+
parts.append("")
|
|
288
|
+
|
|
266
289
|
# Build statement context for better navigation
|
|
267
290
|
statement_context = f"Statement[{self.statement_index}]"
|
|
268
291
|
if self.statement_sid:
|
|
269
292
|
statement_context = f"`{self.statement_sid}` ({statement_context})"
|
|
293
|
+
if self.line_number:
|
|
294
|
+
statement_context = f"{statement_context} (line {self.line_number})"
|
|
270
295
|
|
|
271
|
-
#
|
|
272
|
-
parts.append(f"
|
|
296
|
+
# Statement context on its own line
|
|
297
|
+
parts.append(f"**Statement:** {statement_context}")
|
|
273
298
|
parts.append("")
|
|
274
299
|
|
|
275
300
|
# Show message immediately (not collapsed)
|