iam-policy-validator 1.13.1__py3-none-any.whl → 1.14.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.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/METADATA +1 -1
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/RECORD +45 -39
- iam_validator/__version__.py +1 -1
- iam_validator/checks/action_condition_enforcement.py +6 -0
- iam_validator/checks/action_resource_matching.py +12 -12
- iam_validator/checks/action_validation.py +1 -0
- iam_validator/checks/condition_key_validation.py +2 -0
- iam_validator/checks/condition_type_mismatch.py +3 -0
- iam_validator/checks/full_wildcard.py +1 -0
- iam_validator/checks/mfa_condition_check.py +2 -0
- iam_validator/checks/policy_structure.py +9 -0
- iam_validator/checks/policy_type_validation.py +11 -0
- iam_validator/checks/principal_validation.py +5 -0
- iam_validator/checks/resource_validation.py +4 -0
- iam_validator/checks/sensitive_action.py +1 -0
- iam_validator/checks/service_wildcard.py +6 -3
- iam_validator/checks/set_operator_validation.py +3 -0
- iam_validator/checks/sid_uniqueness.py +2 -0
- iam_validator/checks/trust_policy_validation.py +3 -0
- iam_validator/checks/utils/__init__.py +16 -0
- iam_validator/checks/utils/action_parser.py +149 -0
- iam_validator/checks/wildcard_action.py +1 -0
- iam_validator/checks/wildcard_resource.py +231 -4
- iam_validator/commands/analyze.py +19 -1
- iam_validator/commands/completion.py +6 -2
- iam_validator/commands/validate.py +231 -12
- iam_validator/core/aws_service/fetcher.py +21 -9
- iam_validator/core/codeowners.py +245 -0
- iam_validator/core/config/check_documentation.py +390 -0
- iam_validator/core/config/config_loader.py +199 -0
- iam_validator/core/config/defaults.py +25 -0
- iam_validator/core/constants.py +1 -0
- iam_validator/core/diff_parser.py +8 -4
- iam_validator/core/finding_fingerprint.py +131 -0
- iam_validator/core/formatters/sarif.py +370 -128
- iam_validator/core/ignore_processor.py +309 -0
- iam_validator/core/ignored_findings.py +400 -0
- iam_validator/core/models.py +54 -4
- iam_validator/core/policy_loader.py +313 -4
- iam_validator/core/pr_commenter.py +223 -22
- iam_validator/core/report.py +22 -6
- iam_validator/integrations/github_integration.py +881 -123
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/WHEEL +0 -0
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/entry_points.txt +0 -0
- {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
"""Check Documentation Registry.
|
|
2
|
+
|
|
3
|
+
This module provides centralized documentation for all built-in checks,
|
|
4
|
+
including risk explanations, AWS documentation links, and remediation steps.
|
|
5
|
+
|
|
6
|
+
Used to enhance ValidationIssue objects with actionable guidance.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import ClassVar
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class CheckDocumentation:
|
|
15
|
+
"""Documentation for a single check.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
check_id: Unique check identifier (e.g., "wildcard_action")
|
|
19
|
+
risk_explanation: Why this issue is a security risk
|
|
20
|
+
documentation_url: Link to relevant AWS docs or runbook
|
|
21
|
+
remediation_steps: Step-by-step fix guidance
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
check_id: str
|
|
25
|
+
risk_explanation: str
|
|
26
|
+
documentation_url: str
|
|
27
|
+
remediation_steps: list[str] = field(default_factory=list)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CheckDocumentationRegistry:
|
|
31
|
+
"""Registry for check documentation.
|
|
32
|
+
|
|
33
|
+
Provides centralized lookup for risk explanations, documentation links,
|
|
34
|
+
and remediation steps for all built-in checks.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# AWS IAM documentation base URLs
|
|
38
|
+
AWS_IAM_DOCS = "https://docs.aws.amazon.com/IAM/latest/UserGuide"
|
|
39
|
+
AWS_IAM_REFERENCE = "https://docs.aws.amazon.com/service-authorization/latest/reference"
|
|
40
|
+
|
|
41
|
+
# Registry of all check documentation
|
|
42
|
+
_registry: ClassVar[dict[str, CheckDocumentation]] = {}
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def register(cls, doc: CheckDocumentation) -> None:
|
|
46
|
+
"""Register documentation for a check."""
|
|
47
|
+
cls._registry[doc.check_id] = doc
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get(cls, check_id: str) -> CheckDocumentation | None:
|
|
51
|
+
"""Get documentation for a check by ID."""
|
|
52
|
+
return cls._registry.get(check_id)
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def get_risk_explanation(cls, check_id: str) -> str | None:
|
|
56
|
+
"""Get risk explanation for a check."""
|
|
57
|
+
doc = cls.get(check_id)
|
|
58
|
+
return doc.risk_explanation if doc else None
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def get_documentation_url(cls, check_id: str) -> str | None:
|
|
62
|
+
"""Get documentation URL for a check."""
|
|
63
|
+
doc = cls.get(check_id)
|
|
64
|
+
return doc.documentation_url if doc else None
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def get_remediation_steps(cls, check_id: str) -> list[str] | None:
|
|
68
|
+
"""Get remediation steps for a check."""
|
|
69
|
+
doc = cls.get(check_id)
|
|
70
|
+
return doc.remediation_steps if doc else None
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# Register documentation for all built-in checks
|
|
74
|
+
# ==============================================
|
|
75
|
+
|
|
76
|
+
# AWS Validation Checks
|
|
77
|
+
# ---------------------
|
|
78
|
+
|
|
79
|
+
CheckDocumentationRegistry.register(
|
|
80
|
+
CheckDocumentation(
|
|
81
|
+
check_id="action_validation",
|
|
82
|
+
risk_explanation=(
|
|
83
|
+
"Invalid actions may silently fail to grant intended permissions, "
|
|
84
|
+
"or indicate a typo that could expose unintended access."
|
|
85
|
+
),
|
|
86
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
87
|
+
remediation_steps=[
|
|
88
|
+
"Verify the action name against AWS documentation for the target service",
|
|
89
|
+
"Use the IAM policy simulator to test your intended permissions",
|
|
90
|
+
"Check for common typos (e.g., 'S3' vs 's3', 'GetObjects' vs 'GetObject')",
|
|
91
|
+
],
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
CheckDocumentationRegistry.register(
|
|
96
|
+
CheckDocumentation(
|
|
97
|
+
check_id="condition_key_validation",
|
|
98
|
+
risk_explanation=(
|
|
99
|
+
"Invalid condition keys are silently ignored by IAM, meaning your "
|
|
100
|
+
"intended access restrictions may not be enforced."
|
|
101
|
+
),
|
|
102
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_condition-keys.html",
|
|
103
|
+
remediation_steps=[
|
|
104
|
+
"Verify the condition key exists for the target service",
|
|
105
|
+
"Check AWS documentation for the correct key name and format",
|
|
106
|
+
"Use global condition keys (aws:*) for cross-service restrictions",
|
|
107
|
+
],
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
CheckDocumentationRegistry.register(
|
|
112
|
+
CheckDocumentation(
|
|
113
|
+
check_id="condition_type_mismatch",
|
|
114
|
+
risk_explanation=(
|
|
115
|
+
"Using the wrong condition operator type (e.g., StringEquals with a "
|
|
116
|
+
"numeric value) may cause unexpected behavior or silent failures."
|
|
117
|
+
),
|
|
118
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_condition_operators.html",
|
|
119
|
+
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",
|
|
123
|
+
],
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
CheckDocumentationRegistry.register(
|
|
128
|
+
CheckDocumentation(
|
|
129
|
+
check_id="resource_validation",
|
|
130
|
+
risk_explanation=(
|
|
131
|
+
"Invalid resource ARNs may silently fail to match intended resources, "
|
|
132
|
+
"leaving permissions ineffective or overly broad."
|
|
133
|
+
),
|
|
134
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
135
|
+
remediation_steps=[
|
|
136
|
+
"Verify ARN format matches the target service's documentation",
|
|
137
|
+
"Ensure region and account ID are correct or use wildcards intentionally",
|
|
138
|
+
"Test the policy with IAM policy simulator before deployment",
|
|
139
|
+
],
|
|
140
|
+
)
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
CheckDocumentationRegistry.register(
|
|
144
|
+
CheckDocumentation(
|
|
145
|
+
check_id="sid_uniqueness",
|
|
146
|
+
risk_explanation=(
|
|
147
|
+
"Duplicate SIDs can cause confusion and make policy auditing difficult. "
|
|
148
|
+
"Some AWS services may behave unexpectedly with duplicate SIDs."
|
|
149
|
+
),
|
|
150
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_sid.html",
|
|
151
|
+
remediation_steps=[
|
|
152
|
+
"Ensure each statement has a unique SID within the policy",
|
|
153
|
+
"Use descriptive SIDs that indicate the statement's purpose",
|
|
154
|
+
"Consider a naming convention like 'AllowS3ReadAccess' or 'DenyPublicAccess'",
|
|
155
|
+
],
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
CheckDocumentationRegistry.register(
|
|
160
|
+
CheckDocumentation(
|
|
161
|
+
check_id="policy_size",
|
|
162
|
+
risk_explanation=(
|
|
163
|
+
"Policies exceeding AWS size limits cannot be attached to IAM entities. "
|
|
164
|
+
"Inline policies have a 2KB limit, managed policies have a 6KB limit."
|
|
165
|
+
),
|
|
166
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_iam-quotas.html",
|
|
167
|
+
remediation_steps=[
|
|
168
|
+
"Split large policies into multiple smaller policies",
|
|
169
|
+
"Use managed policies instead of inline policies for larger permissions",
|
|
170
|
+
"Remove redundant statements or consolidate similar actions",
|
|
171
|
+
"Consider using permission boundaries or SCPs for broad restrictions",
|
|
172
|
+
],
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
CheckDocumentationRegistry.register(
|
|
177
|
+
CheckDocumentation(
|
|
178
|
+
check_id="policy_structure",
|
|
179
|
+
risk_explanation=(
|
|
180
|
+
"Malformed policy structure will cause IAM to reject the policy entirely, "
|
|
181
|
+
"preventing any permissions from being granted."
|
|
182
|
+
),
|
|
183
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_grammar.html",
|
|
184
|
+
remediation_steps=[
|
|
185
|
+
"Verify the policy follows AWS IAM policy grammar",
|
|
186
|
+
"Ensure all required elements (Version, Statement) are present",
|
|
187
|
+
"Check that Effect, Action, and Resource are properly formatted",
|
|
188
|
+
],
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
CheckDocumentationRegistry.register(
|
|
193
|
+
CheckDocumentation(
|
|
194
|
+
check_id="set_operator_validation",
|
|
195
|
+
risk_explanation=(
|
|
196
|
+
"Invalid ForAllValues/ForAnyValue operators may cause conditions to "
|
|
197
|
+
"behave unexpectedly, potentially granting or denying unintended access."
|
|
198
|
+
),
|
|
199
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_multi-value-conditions.html",
|
|
200
|
+
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",
|
|
204
|
+
],
|
|
205
|
+
)
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
CheckDocumentationRegistry.register(
|
|
209
|
+
CheckDocumentation(
|
|
210
|
+
check_id="mfa_condition_check",
|
|
211
|
+
risk_explanation=(
|
|
212
|
+
"Sensitive operations without MFA requirements may be performed by "
|
|
213
|
+
"compromised credentials, increasing the blast radius of credential theft."
|
|
214
|
+
),
|
|
215
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/id_credentials_mfa_configure-api-require.html",
|
|
216
|
+
remediation_steps=[
|
|
217
|
+
"Add 'aws:MultiFactorAuthPresent': 'true' condition for sensitive actions",
|
|
218
|
+
"Consider using 'aws:MultiFactorAuthAge' to require recent MFA",
|
|
219
|
+
"Ensure MFA is enforced at the identity level as well as policy level",
|
|
220
|
+
],
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
CheckDocumentationRegistry.register(
|
|
225
|
+
CheckDocumentation(
|
|
226
|
+
check_id="principal_validation",
|
|
227
|
+
risk_explanation=(
|
|
228
|
+
"Invalid principals in resource policies may fail to grant access to "
|
|
229
|
+
"intended entities, or may inadvertently grant access to unintended parties."
|
|
230
|
+
),
|
|
231
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_principal.html",
|
|
232
|
+
remediation_steps=[
|
|
233
|
+
"Verify AWS account IDs and IAM ARNs are correct",
|
|
234
|
+
"Use specific principals instead of wildcards where possible",
|
|
235
|
+
"For service principals, use the canonical format (e.g., 's3.amazonaws.com')",
|
|
236
|
+
],
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
CheckDocumentationRegistry.register(
|
|
241
|
+
CheckDocumentation(
|
|
242
|
+
check_id="policy_type_validation",
|
|
243
|
+
risk_explanation=(
|
|
244
|
+
"Using policy elements not supported by the policy type may cause "
|
|
245
|
+
"silent failures or unexpected behavior."
|
|
246
|
+
),
|
|
247
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/access_policies.html",
|
|
248
|
+
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",
|
|
252
|
+
],
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
CheckDocumentationRegistry.register(
|
|
257
|
+
CheckDocumentation(
|
|
258
|
+
check_id="action_resource_matching",
|
|
259
|
+
risk_explanation=(
|
|
260
|
+
"Actions that don't support the specified resources will silently fail, "
|
|
261
|
+
"resulting in permissions that don't work as intended."
|
|
262
|
+
),
|
|
263
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_REFERENCE}/reference_policies_actions-resources-contextkeys.html",
|
|
264
|
+
remediation_steps=[
|
|
265
|
+
"Check AWS documentation for supported resource types per action",
|
|
266
|
+
"Use '*' for actions that don't support resource-level permissions",
|
|
267
|
+
"Split statements when actions require different resource types",
|
|
268
|
+
],
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
CheckDocumentationRegistry.register(
|
|
273
|
+
CheckDocumentation(
|
|
274
|
+
check_id="trust_policy_validation",
|
|
275
|
+
risk_explanation=(
|
|
276
|
+
"Misconfigured trust policies can allow unauthorized principals to "
|
|
277
|
+
"assume roles, potentially leading to privilege escalation."
|
|
278
|
+
),
|
|
279
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/id_roles_create_for-user.html",
|
|
280
|
+
remediation_steps=[
|
|
281
|
+
"Restrict Principal to specific accounts/roles/users",
|
|
282
|
+
"Add conditions to limit who can assume the role",
|
|
283
|
+
"Avoid wildcards in Principal unless absolutely necessary",
|
|
284
|
+
"Use ExternalId for cross-account role assumption",
|
|
285
|
+
],
|
|
286
|
+
)
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
# Security Checks
|
|
290
|
+
# ---------------
|
|
291
|
+
|
|
292
|
+
CheckDocumentationRegistry.register(
|
|
293
|
+
CheckDocumentation(
|
|
294
|
+
check_id="wildcard_action",
|
|
295
|
+
risk_explanation=(
|
|
296
|
+
"Wildcard actions (e.g., 's3:*') grant all current AND future permissions "
|
|
297
|
+
"for a service, violating least privilege and increasing attack surface."
|
|
298
|
+
),
|
|
299
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
300
|
+
remediation_steps=[
|
|
301
|
+
"Replace wildcards with specific actions needed for the use case",
|
|
302
|
+
"Use action groups like 's3:Get*' for read-only access",
|
|
303
|
+
"Document why each action is required",
|
|
304
|
+
"Review and reduce permissions periodically",
|
|
305
|
+
],
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
CheckDocumentationRegistry.register(
|
|
310
|
+
CheckDocumentation(
|
|
311
|
+
check_id="wildcard_resource",
|
|
312
|
+
risk_explanation=(
|
|
313
|
+
"Wildcard resources ('*') grant access to ALL resources of a type, "
|
|
314
|
+
"including resources created in the future, violating least privilege."
|
|
315
|
+
),
|
|
316
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
317
|
+
remediation_steps=[
|
|
318
|
+
"Specify exact resource ARNs when possible",
|
|
319
|
+
"Use resource tags and conditions for dynamic access control",
|
|
320
|
+
"Limit scope to specific accounts, regions, or resource prefixes",
|
|
321
|
+
],
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
CheckDocumentationRegistry.register(
|
|
326
|
+
CheckDocumentation(
|
|
327
|
+
check_id="full_wildcard",
|
|
328
|
+
risk_explanation=(
|
|
329
|
+
"Full wildcard access ('Action': '*', 'Resource': '*') grants complete "
|
|
330
|
+
"control over all AWS resources, equivalent to administrator access."
|
|
331
|
+
),
|
|
332
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
333
|
+
remediation_steps=[
|
|
334
|
+
"Immediately restrict to specific services and actions needed",
|
|
335
|
+
"Use AWS managed policies like PowerUserAccess for broad access",
|
|
336
|
+
"Implement permission boundaries to limit maximum possible permissions",
|
|
337
|
+
"Consider using service control policies (SCPs) as guardrails",
|
|
338
|
+
],
|
|
339
|
+
)
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
CheckDocumentationRegistry.register(
|
|
343
|
+
CheckDocumentation(
|
|
344
|
+
check_id="service_wildcard",
|
|
345
|
+
risk_explanation=(
|
|
346
|
+
"Service-level wildcards (e.g., 'iam:*') grant all permissions for "
|
|
347
|
+
"an entire service, including destructive and privilege escalation actions."
|
|
348
|
+
),
|
|
349
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
350
|
+
remediation_steps=[
|
|
351
|
+
"Replace with specific actions required for the use case",
|
|
352
|
+
"Use AWS managed policies for common patterns",
|
|
353
|
+
"Consider permission boundaries to limit sensitive actions",
|
|
354
|
+
],
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
CheckDocumentationRegistry.register(
|
|
359
|
+
CheckDocumentation(
|
|
360
|
+
check_id="sensitive_action",
|
|
361
|
+
risk_explanation=(
|
|
362
|
+
"Sensitive actions (e.g., iam:*, sts:AssumeRole, kms:Decrypt) can lead "
|
|
363
|
+
"to privilege escalation, data exfiltration, or account compromise."
|
|
364
|
+
),
|
|
365
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/best-practices.html#grant-least-privilege",
|
|
366
|
+
remediation_steps=[
|
|
367
|
+
"Add conditions to restrict when these actions can be used",
|
|
368
|
+
"Require MFA for sensitive operations",
|
|
369
|
+
"Limit to specific resources where possible",
|
|
370
|
+
"Implement monitoring and alerting for sensitive action usage",
|
|
371
|
+
],
|
|
372
|
+
)
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
CheckDocumentationRegistry.register(
|
|
376
|
+
CheckDocumentation(
|
|
377
|
+
check_id="action_condition_enforcement",
|
|
378
|
+
risk_explanation=(
|
|
379
|
+
"Certain sensitive actions should always have conditions to prevent "
|
|
380
|
+
"misuse, such as IP restrictions, MFA requirements, or time-based access."
|
|
381
|
+
),
|
|
382
|
+
documentation_url=f"{CheckDocumentationRegistry.AWS_IAM_DOCS}/reference_policies_elements_condition.html",
|
|
383
|
+
remediation_steps=[
|
|
384
|
+
"Add appropriate conditions based on the action type",
|
|
385
|
+
"Use aws:SourceIp for network-restricted actions",
|
|
386
|
+
"Use aws:MultiFactorAuthPresent for authentication-sensitive actions",
|
|
387
|
+
"Use aws:RequestedRegion to limit geographic scope",
|
|
388
|
+
],
|
|
389
|
+
)
|
|
390
|
+
)
|
|
@@ -14,6 +14,7 @@ from pathlib import Path
|
|
|
14
14
|
from typing import Any
|
|
15
15
|
|
|
16
16
|
import yaml
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
|
|
17
18
|
|
|
18
19
|
from iam_validator.core.check_registry import CheckConfig, CheckRegistry, PolicyCheck
|
|
19
20
|
from iam_validator.core.config.defaults import get_default_config
|
|
@@ -21,6 +22,204 @@ from iam_validator.core.constants import DEFAULT_CONFIG_FILENAMES
|
|
|
21
22
|
|
|
22
23
|
logger = logging.getLogger(__name__)
|
|
23
24
|
|
|
25
|
+
# Valid severity levels for validation
|
|
26
|
+
SEVERITY_LEVELS = frozenset(["error", "warning", "info", "critical", "high", "medium", "low"])
|
|
27
|
+
|
|
28
|
+
# Known built-in check IDs for validation warnings
|
|
29
|
+
KNOWN_CHECK_IDS = frozenset(
|
|
30
|
+
[
|
|
31
|
+
"action_validation",
|
|
32
|
+
"condition_key_validation",
|
|
33
|
+
"condition_type_mismatch",
|
|
34
|
+
"resource_validation",
|
|
35
|
+
"sid_uniqueness",
|
|
36
|
+
"policy_size",
|
|
37
|
+
"policy_structure",
|
|
38
|
+
"set_operator_validation",
|
|
39
|
+
"mfa_condition_check",
|
|
40
|
+
"principal_validation",
|
|
41
|
+
"policy_type_validation",
|
|
42
|
+
"action_resource_matching",
|
|
43
|
+
"trust_policy_validation",
|
|
44
|
+
"wildcard_action",
|
|
45
|
+
"wildcard_resource",
|
|
46
|
+
"full_wildcard",
|
|
47
|
+
"service_wildcard",
|
|
48
|
+
"sensitive_action",
|
|
49
|
+
"action_condition_enforcement",
|
|
50
|
+
]
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Pydantic Configuration Schemas
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class IgnorePatternSchema(BaseModel):
|
|
60
|
+
"""Schema for ignore patterns within check configurations."""
|
|
61
|
+
|
|
62
|
+
model_config = ConfigDict(extra="forbid")
|
|
63
|
+
|
|
64
|
+
# At least one of these should be specified
|
|
65
|
+
file: str | None = None
|
|
66
|
+
action: str | None = None
|
|
67
|
+
resource: str | None = None
|
|
68
|
+
sid: str | None = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CheckConfigSchema(BaseModel):
|
|
72
|
+
"""Flexible check config - validates core fields, allows extras for custom options.
|
|
73
|
+
|
|
74
|
+
This schema validates common check configuration fields while allowing
|
|
75
|
+
arbitrary additional options that specific checks may require (e.g.,
|
|
76
|
+
allowed_wildcards, categories, requirements).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(extra="allow") # Allow arbitrary check-specific options
|
|
80
|
+
|
|
81
|
+
enabled: bool = True
|
|
82
|
+
severity: str | None = None
|
|
83
|
+
description: str | None = None
|
|
84
|
+
ignore_patterns: list[dict[str, Any]] = []
|
|
85
|
+
|
|
86
|
+
@field_validator("severity")
|
|
87
|
+
@classmethod
|
|
88
|
+
def validate_severity(cls, v: str | None) -> str | None:
|
|
89
|
+
if v is not None and v not in SEVERITY_LEVELS:
|
|
90
|
+
raise ValueError(f"Invalid severity: {v}. Must be one of: {sorted(SEVERITY_LEVELS)}")
|
|
91
|
+
return v
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class IgnoreSettingsSchema(BaseModel):
|
|
95
|
+
"""Schema for ignore settings."""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="forbid")
|
|
98
|
+
|
|
99
|
+
enabled: bool = True
|
|
100
|
+
allowed_users: list[str] = []
|
|
101
|
+
post_denial_feedback: bool = False
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class DocumentationSettingsSchema(BaseModel):
|
|
105
|
+
"""Schema for documentation settings."""
|
|
106
|
+
|
|
107
|
+
model_config = ConfigDict(extra="forbid")
|
|
108
|
+
|
|
109
|
+
base_url: str | None = None
|
|
110
|
+
include_aws_docs: bool = True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class SettingsSchema(BaseModel):
|
|
114
|
+
"""Schema for global settings."""
|
|
115
|
+
|
|
116
|
+
model_config = ConfigDict(extra="allow") # Allow additional settings
|
|
117
|
+
|
|
118
|
+
fail_fast: bool = False
|
|
119
|
+
parallel: bool = True
|
|
120
|
+
max_workers: int | None = None
|
|
121
|
+
fail_on_severity: list[str] = ["error", "critical"]
|
|
122
|
+
severity_labels: dict[str, str | list[str]] = {}
|
|
123
|
+
ignore_settings: IgnoreSettingsSchema = IgnoreSettingsSchema()
|
|
124
|
+
documentation: DocumentationSettingsSchema = DocumentationSettingsSchema()
|
|
125
|
+
|
|
126
|
+
@field_validator("fail_on_severity")
|
|
127
|
+
@classmethod
|
|
128
|
+
def validate_fail_on_severity(cls, v: list[str]) -> list[str]:
|
|
129
|
+
for severity in v:
|
|
130
|
+
if severity not in SEVERITY_LEVELS:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"Invalid severity in fail_on_severity: {severity}. "
|
|
133
|
+
f"Must be one of: {sorted(SEVERITY_LEVELS)}"
|
|
134
|
+
)
|
|
135
|
+
return v
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class CustomCheckSchema(BaseModel):
|
|
139
|
+
"""Schema for custom check definitions."""
|
|
140
|
+
|
|
141
|
+
model_config = ConfigDict(extra="allow")
|
|
142
|
+
|
|
143
|
+
module: str
|
|
144
|
+
enabled: bool = True
|
|
145
|
+
severity: str | None = None
|
|
146
|
+
description: str | None = None
|
|
147
|
+
config: dict[str, Any] = {}
|
|
148
|
+
|
|
149
|
+
@field_validator("severity")
|
|
150
|
+
@classmethod
|
|
151
|
+
def validate_severity(cls, v: str | None) -> str | None:
|
|
152
|
+
if v is not None and v not in SEVERITY_LEVELS:
|
|
153
|
+
raise ValueError(f"Invalid severity: {v}. Must be one of: {sorted(SEVERITY_LEVELS)}")
|
|
154
|
+
return v
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ConfigSchema(BaseModel):
|
|
158
|
+
"""Top-level configuration schema.
|
|
159
|
+
|
|
160
|
+
Validates the overall structure while allowing flexibility for check configs.
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
model_config = ConfigDict(extra="allow") # Allow check configs at top level
|
|
164
|
+
|
|
165
|
+
settings: SettingsSchema = SettingsSchema()
|
|
166
|
+
custom_checks: list[CustomCheckSchema] = []
|
|
167
|
+
custom_checks_dir: str | None = None
|
|
168
|
+
|
|
169
|
+
@model_validator(mode="after")
|
|
170
|
+
def warn_unknown_checks(self) -> "ConfigSchema":
|
|
171
|
+
"""Warn about unknown check IDs (potential typos)."""
|
|
172
|
+
# Get all extra fields that might be check configs
|
|
173
|
+
if not self.model_extra:
|
|
174
|
+
return self
|
|
175
|
+
|
|
176
|
+
for key, value in self.model_extra.items():
|
|
177
|
+
if isinstance(value, dict):
|
|
178
|
+
# This looks like a check config
|
|
179
|
+
check_id = key.removesuffix("_check") if key.endswith("_check") else key
|
|
180
|
+
if check_id not in KNOWN_CHECK_IDS:
|
|
181
|
+
logger.warning(
|
|
182
|
+
f"Unknown check ID '{check_id}' in configuration. "
|
|
183
|
+
f"This may be a custom check or a typo."
|
|
184
|
+
)
|
|
185
|
+
return self
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class ConfigValidationError(Exception):
|
|
189
|
+
"""Raised when configuration validation fails."""
|
|
190
|
+
|
|
191
|
+
def __init__(self, errors: list[str]):
|
|
192
|
+
self.errors = errors
|
|
193
|
+
super().__init__(
|
|
194
|
+
"Configuration validation failed:\n" + "\n".join(f" - {e}" for e in errors)
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def validate_config(config_dict: dict[str, Any]) -> tuple[bool, list[str]]:
|
|
199
|
+
"""Validate configuration dictionary against schema.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
config_dict: Raw configuration dictionary
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (is_valid, list of error messages)
|
|
206
|
+
"""
|
|
207
|
+
errors: list[str] = []
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
ConfigSchema.model_validate(config_dict)
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# Parse Pydantic validation errors
|
|
213
|
+
if hasattr(e, "errors"):
|
|
214
|
+
for error in e.errors(): # type: ignore
|
|
215
|
+
loc = ".".join(str(x) for x in error.get("loc", []))
|
|
216
|
+
msg = error.get("msg", str(e))
|
|
217
|
+
errors.append(f"{loc}: {msg}")
|
|
218
|
+
else:
|
|
219
|
+
errors.append(str(e))
|
|
220
|
+
|
|
221
|
+
return len(errors) == 0, errors
|
|
222
|
+
|
|
24
223
|
|
|
25
224
|
def deep_merge(base: dict, override: dict) -> dict:
|
|
26
225
|
"""
|
|
@@ -85,6 +85,31 @@ DEFAULT_CONFIG = {
|
|
|
85
85
|
# Mixed: {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
|
|
86
86
|
# Default: {} (disabled)
|
|
87
87
|
"severity_labels": {},
|
|
88
|
+
# CODEOWNERS-based finding ignore settings
|
|
89
|
+
# Allows CODEOWNERS to ignore validation findings by replying "ignore" to PR comments
|
|
90
|
+
# Ignored findings won't cause the action to fail and won't be posted as comments
|
|
91
|
+
"ignore_settings": {
|
|
92
|
+
# Enable/disable the CODEOWNERS ignore feature
|
|
93
|
+
"enabled": True,
|
|
94
|
+
# Fallback list of users who can ignore findings when no CODEOWNERS file exists
|
|
95
|
+
# If empty and no CODEOWNERS, all ignore requests are denied (fail secure)
|
|
96
|
+
# Example: ["security-team-lead", "platform-admin"]
|
|
97
|
+
"allowed_users": [],
|
|
98
|
+
# Whether to post visible replies when ignore requests are denied
|
|
99
|
+
# When False (default), denials are only logged
|
|
100
|
+
# When True, a reply is posted explaining why the ignore was denied
|
|
101
|
+
"post_denial_feedback": False,
|
|
102
|
+
},
|
|
103
|
+
# Organization-specific documentation URL configuration
|
|
104
|
+
# Allows overriding default AWS documentation links with org-specific runbooks
|
|
105
|
+
"documentation": {
|
|
106
|
+
# Base URL for org-specific runbooks (null = use AWS docs)
|
|
107
|
+
# Example: "https://wiki.mycompany.com/security/iam-checks"
|
|
108
|
+
# When set, check documentation URLs will be: {base_url}/{check_id}
|
|
109
|
+
"base_url": None,
|
|
110
|
+
# Include AWS documentation links alongside org docs
|
|
111
|
+
"include_aws_docs": True,
|
|
112
|
+
},
|
|
88
113
|
},
|
|
89
114
|
# ========================================================================
|
|
90
115
|
# AWS IAM Validation Checks (17 checks total)
|
iam_validator/core/constants.py
CHANGED
|
@@ -87,6 +87,7 @@ BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
|
87
87
|
# HTML comment markers for identifying bot-generated content (for cleanup/updates)
|
|
88
88
|
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
89
89
|
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
90
|
+
IGNORED_FINDINGS_IDENTIFIER = "<!-- iam-policy-validator-ignored-findings -->"
|
|
90
91
|
|
|
91
92
|
# GitHub comment size limits
|
|
92
93
|
# GitHub's actual limit is 65536 characters, but we use a smaller limit for safety
|
|
@@ -87,15 +87,19 @@ class DiffParser:
|
|
|
87
87
|
patch = file_info.get("patch")
|
|
88
88
|
|
|
89
89
|
# Files without patches (e.g., binary files, very large files)
|
|
90
|
+
# For added/modified files, we use a "no_patch" marker to indicate
|
|
91
|
+
# that we should allow comments on any line (handled in pr_commenter)
|
|
90
92
|
if not patch or not isinstance(patch, str):
|
|
91
|
-
logger.debug(f"No patch available for {filename}
|
|
92
|
-
#
|
|
93
|
+
logger.debug(f"No patch available for {filename} (status={status})")
|
|
94
|
+
# Mark as "no_patch" so pr_commenter can handle this specially
|
|
95
|
+
# For added/modified files without patch, we'll allow inline comments
|
|
96
|
+
# on any line since GitHub likely truncated the diff due to size
|
|
93
97
|
parsed[filename] = ParsedDiff(
|
|
94
98
|
file_path=filename,
|
|
95
|
-
changed_lines=set(),
|
|
99
|
+
changed_lines=set(), # Empty, but status indicates handling
|
|
96
100
|
added_lines=set(),
|
|
97
101
|
deleted_lines=set(),
|
|
98
|
-
status=status,
|
|
102
|
+
status=f"{status}_no_patch", # Mark as no_patch variant
|
|
99
103
|
)
|
|
100
104
|
continue
|
|
101
105
|
|