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.
Files changed (45) hide show
  1. {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/METADATA +1 -1
  2. {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/RECORD +45 -39
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/action_condition_enforcement.py +6 -0
  5. iam_validator/checks/action_resource_matching.py +12 -12
  6. iam_validator/checks/action_validation.py +1 -0
  7. iam_validator/checks/condition_key_validation.py +2 -0
  8. iam_validator/checks/condition_type_mismatch.py +3 -0
  9. iam_validator/checks/full_wildcard.py +1 -0
  10. iam_validator/checks/mfa_condition_check.py +2 -0
  11. iam_validator/checks/policy_structure.py +9 -0
  12. iam_validator/checks/policy_type_validation.py +11 -0
  13. iam_validator/checks/principal_validation.py +5 -0
  14. iam_validator/checks/resource_validation.py +4 -0
  15. iam_validator/checks/sensitive_action.py +1 -0
  16. iam_validator/checks/service_wildcard.py +6 -3
  17. iam_validator/checks/set_operator_validation.py +3 -0
  18. iam_validator/checks/sid_uniqueness.py +2 -0
  19. iam_validator/checks/trust_policy_validation.py +3 -0
  20. iam_validator/checks/utils/__init__.py +16 -0
  21. iam_validator/checks/utils/action_parser.py +149 -0
  22. iam_validator/checks/wildcard_action.py +1 -0
  23. iam_validator/checks/wildcard_resource.py +231 -4
  24. iam_validator/commands/analyze.py +19 -1
  25. iam_validator/commands/completion.py +6 -2
  26. iam_validator/commands/validate.py +231 -12
  27. iam_validator/core/aws_service/fetcher.py +21 -9
  28. iam_validator/core/codeowners.py +245 -0
  29. iam_validator/core/config/check_documentation.py +390 -0
  30. iam_validator/core/config/config_loader.py +199 -0
  31. iam_validator/core/config/defaults.py +25 -0
  32. iam_validator/core/constants.py +1 -0
  33. iam_validator/core/diff_parser.py +8 -4
  34. iam_validator/core/finding_fingerprint.py +131 -0
  35. iam_validator/core/formatters/sarif.py +370 -128
  36. iam_validator/core/ignore_processor.py +309 -0
  37. iam_validator/core/ignored_findings.py +400 -0
  38. iam_validator/core/models.py +54 -4
  39. iam_validator/core/policy_loader.py +313 -4
  40. iam_validator/core/pr_commenter.py +223 -22
  41. iam_validator/core/report.py +22 -6
  42. iam_validator/integrations/github_integration.py +881 -123
  43. {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/WHEEL +0 -0
  44. {iam_policy_validator-1.13.1.dist-info → iam_policy_validator-1.14.1.dist-info}/entry_points.txt +0 -0
  45. {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)
@@ -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}, skipping diff parsing")
92
- # Still track the file with empty change sets
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