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.
Files changed (43) hide show
  1. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/METADATA +34 -23
  2. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/RECORD +42 -29
  3. iam_policy_validator-1.15.0.dist-info/entry_points.txt +4 -0
  4. iam_validator/__version__.py +1 -1
  5. iam_validator/checks/__init__.py +2 -0
  6. iam_validator/checks/action_validation.py +91 -27
  7. iam_validator/checks/not_action_not_resource.py +163 -0
  8. iam_validator/checks/resource_validation.py +132 -81
  9. iam_validator/checks/wildcard_resource.py +136 -6
  10. iam_validator/commands/__init__.py +3 -0
  11. iam_validator/commands/cache.py +66 -24
  12. iam_validator/commands/completion.py +94 -15
  13. iam_validator/commands/mcp.py +210 -0
  14. iam_validator/commands/query.py +489 -65
  15. iam_validator/core/aws_service/__init__.py +5 -1
  16. iam_validator/core/aws_service/cache.py +20 -0
  17. iam_validator/core/aws_service/fetcher.py +180 -11
  18. iam_validator/core/aws_service/storage.py +14 -6
  19. iam_validator/core/aws_service/validators.py +32 -41
  20. iam_validator/core/check_registry.py +100 -35
  21. iam_validator/core/config/aws_global_conditions.py +13 -0
  22. iam_validator/core/config/check_documentation.py +104 -51
  23. iam_validator/core/config/config_loader.py +39 -3
  24. iam_validator/core/config/defaults.py +6 -0
  25. iam_validator/core/constants.py +11 -4
  26. iam_validator/core/models.py +39 -14
  27. iam_validator/mcp/__init__.py +162 -0
  28. iam_validator/mcp/models.py +118 -0
  29. iam_validator/mcp/server.py +2928 -0
  30. iam_validator/mcp/session_config.py +319 -0
  31. iam_validator/mcp/templates/__init__.py +79 -0
  32. iam_validator/mcp/templates/builtin.py +856 -0
  33. iam_validator/mcp/tools/__init__.py +72 -0
  34. iam_validator/mcp/tools/generation.py +888 -0
  35. iam_validator/mcp/tools/org_config_tools.py +263 -0
  36. iam_validator/mcp/tools/query.py +395 -0
  37. iam_validator/mcp/tools/validation.py +376 -0
  38. iam_validator/sdk/__init__.py +64 -63
  39. iam_validator/sdk/context.py +3 -2
  40. iam_validator/sdk/policy_utils.py +31 -5
  41. iam_policy_validator-1.14.6.dist-info/entry_points.txt +0 -2
  42. {iam_policy_validator-1.14.6.dist-info → iam_policy_validator-1.15.0.dist-info}/WHEEL +0 -0
  43. {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 actions may silently fail to grant intended permissions, "
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 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')",
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 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",
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 resource ARNs may silently fail to match intended resources, "
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 actions",
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, Statement) are present",
187
- "Check that Effect, Action, and Resource are properly formatted",
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/ForAnyValue operators may cause conditions to "
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 'aws:MultiFactorAuthPresent': 'true' condition for sensitive actions",
218
- "Consider using 'aws:MultiFactorAuthAge' to require recent MFA",
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
- "Actions that don't support the specified resources will silently fail, "
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 '*' for actions that don't support resource-level permissions",
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., 's3:*') grant all current AND future permissions "
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 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",
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 ('*') grant access to ALL resources of a type, "
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 resource ARNs when possible",
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., 'iam:*') grant all permissions for "
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:*, sts:AssumeRole, kms:Decrypt) can lead "
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 MFA for sensitive operations",
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 IP restrictions, MFA requirements, or time-based access."
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:SourceIp for network-restricted actions",
386
- "Use aws:MultiFactorAuthPresent for authentication-sensitive actions",
387
- "Use aws:RequestedRegion to limit geographic scope",
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
- "ignore_patterns", []
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)
@@ -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
@@ -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
- severity_emoji = {
230
- # IAM validity severities
231
- "error": "",
232
- "warning": "⚠️",
233
- "info": "ℹ️",
234
- # Security severities
235
- "critical": "🔴",
236
- "high": "🟠",
237
- "medium": "🟡",
238
- "low": "🔵",
239
- }
240
- emoji = severity_emoji.get(self.severity, "•")
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
- # Main issue header with statement context
272
- parts.append(f"{emoji} **{self.severity.upper()}** in **{statement_context}**")
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)