iam-policy-validator 1.3.1__py3-none-any.whl → 1.5.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 (41) hide show
  1. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/METADATA +164 -19
  2. iam_policy_validator-1.5.0.dist-info/RECORD +67 -0
  3. iam_validator/__version__.py +1 -1
  4. iam_validator/checks/__init__.py +15 -3
  5. iam_validator/checks/action_condition_enforcement.py +1 -6
  6. iam_validator/checks/condition_key_validation.py +21 -1
  7. iam_validator/checks/full_wildcard.py +67 -0
  8. iam_validator/checks/policy_size.py +1 -0
  9. iam_validator/checks/policy_type_validation.py +299 -0
  10. iam_validator/checks/principal_validation.py +776 -0
  11. iam_validator/checks/sensitive_action.py +178 -0
  12. iam_validator/checks/service_wildcard.py +105 -0
  13. iam_validator/checks/sid_uniqueness.py +45 -7
  14. iam_validator/checks/utils/sensitive_action_matcher.py +39 -31
  15. iam_validator/checks/wildcard_action.py +62 -0
  16. iam_validator/checks/wildcard_resource.py +131 -0
  17. iam_validator/commands/download_services.py +3 -8
  18. iam_validator/commands/post_to_pr.py +7 -0
  19. iam_validator/commands/validate.py +204 -16
  20. iam_validator/core/aws_fetcher.py +25 -12
  21. iam_validator/core/check_registry.py +25 -21
  22. iam_validator/core/config/__init__.py +83 -0
  23. iam_validator/core/config/aws_api.py +35 -0
  24. iam_validator/core/config/condition_requirements.py +535 -0
  25. iam_validator/core/config/defaults.py +390 -0
  26. iam_validator/core/config/principal_requirements.py +421 -0
  27. iam_validator/core/config/sensitive_actions.py +133 -0
  28. iam_validator/core/config/service_principals.py +95 -0
  29. iam_validator/core/config/wildcards.py +124 -0
  30. iam_validator/core/config_loader.py +29 -9
  31. iam_validator/core/formatters/enhanced.py +11 -5
  32. iam_validator/core/formatters/sarif.py +78 -14
  33. iam_validator/core/models.py +13 -3
  34. iam_validator/core/policy_checks.py +39 -6
  35. iam_validator/core/pr_commenter.py +30 -9
  36. iam_policy_validator-1.3.1.dist-info/RECORD +0 -54
  37. iam_validator/checks/security_best_practices.py +0 -535
  38. iam_validator/core/defaults.py +0 -366
  39. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/WHEEL +0 -0
  40. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/entry_points.txt +0 -0
  41. {iam_policy_validator-1.3.1.dist-info → iam_policy_validator-1.5.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,124 @@
1
+ """
2
+ Default wildcard configurations for security best practices checks.
3
+
4
+ These wildcards define which actions are considered "safe" to use with
5
+ Resource: "*" (e.g., read-only describe operations).
6
+
7
+ Using Python tuples instead of YAML lists provides:
8
+ - Zero parsing overhead
9
+ - Immutable by default (tuples)
10
+ - Better performance
11
+ - Easy PyPI packaging
12
+ """
13
+
14
+ from typing import Final
15
+
16
+ # ============================================================================
17
+ # Allowed Wildcards for Resource: "*"
18
+ # ============================================================================
19
+ # These action patterns are considered safe to use with wildcard resources
20
+ # They are typically read-only operations that need broad resource access
21
+
22
+ DEFAULT_ALLOWED_WILDCARDS: Final[tuple[str, ...]] = (
23
+ # Auto Scaling
24
+ "autoscaling:Describe*",
25
+ # CloudWatch
26
+ "cloudwatch:Describe*",
27
+ "cloudwatch:Get*",
28
+ "cloudwatch:List*",
29
+ # DynamoDB
30
+ "dynamodb:Describe*",
31
+ # EC2
32
+ "ec2:Describe*",
33
+ # Elastic Load Balancing
34
+ "elasticloadbalancing:Describe*",
35
+ # IAM (non-sensitive read operations)
36
+ "iam:Get*",
37
+ "iam:List*",
38
+ # KMS
39
+ "kms:Describe*",
40
+ # Lambda
41
+ "lambda:Get*",
42
+ "lambda:List*",
43
+ # CloudWatch Logs
44
+ "logs:Describe*",
45
+ "logs:Filter*",
46
+ "logs:Get*",
47
+ # RDS
48
+ "rds:Describe*",
49
+ # Route53
50
+ "route53:Get*",
51
+ "route53:List*",
52
+ # S3 (safe read operations only)
53
+ "s3:Describe*",
54
+ "s3:GetBucket*",
55
+ "s3:GetM*",
56
+ "s3:List*",
57
+ # SQS
58
+ "sqs:Get*",
59
+ "sqs:List*",
60
+ # API Gateway
61
+ "apigateway:GET",
62
+ )
63
+
64
+ # ============================================================================
65
+ # Service-Level Wildcards (Allowed Services)
66
+ # ============================================================================
67
+ # Services that are allowed to use service-level wildcards like "logs:*"
68
+ # These are typically low-risk monitoring/logging services
69
+
70
+ DEFAULT_SERVICE_WILDCARDS: Final[tuple[str, ...]] = (
71
+ "logs",
72
+ "cloudwatch",
73
+ "xray",
74
+ )
75
+
76
+
77
+ def get_allowed_wildcards() -> tuple[str, ...]:
78
+ """
79
+ Get tuple of allowed wildcard action patterns.
80
+
81
+ Returns:
82
+ Tuple of action patterns that are safe to use with Resource: "*"
83
+ """
84
+ return DEFAULT_ALLOWED_WILDCARDS
85
+
86
+
87
+ def get_allowed_service_wildcards() -> tuple[str, ...]:
88
+ """
89
+ Get tuple of services allowed to use service-level wildcards.
90
+
91
+ Returns:
92
+ Tuple of service names (e.g., "logs", "cloudwatch")
93
+ """
94
+ return DEFAULT_SERVICE_WILDCARDS
95
+
96
+
97
+ def is_allowed_wildcard(pattern: str) -> bool:
98
+ """
99
+ Check if a wildcard pattern is in the allowed list.
100
+
101
+ Args:
102
+ pattern: Action pattern to check (e.g., "s3:List*")
103
+
104
+ Returns:
105
+ True if pattern is in allowed wildcards
106
+
107
+ Performance: O(n) but typically small list (~25 items)
108
+ """
109
+ return pattern in DEFAULT_ALLOWED_WILDCARDS
110
+
111
+
112
+ def is_allowed_service_wildcard(service: str) -> bool:
113
+ """
114
+ Check if a service is allowed to use service-level wildcards.
115
+
116
+ Args:
117
+ service: Service name (e.g., "logs", "s3")
118
+
119
+ Returns:
120
+ True if service is in allowed list
121
+
122
+ Performance: O(n) but very small list (~3 items)
123
+ """
124
+ return service in DEFAULT_SERVICE_WILDCARDS
@@ -15,7 +15,7 @@ from typing import Any
15
15
  import yaml
16
16
 
17
17
  from iam_validator.core.check_registry import CheckConfig, CheckRegistry, PolicyCheck
18
- from iam_validator.core.defaults import get_default_config
18
+ from iam_validator.core.config.defaults import get_default_config
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
@@ -68,18 +68,38 @@ class ValidatorConfig:
68
68
  self.config_dict = config_dict or {}
69
69
 
70
70
  # Support both nested and flat structure
71
- # New flat structure: each check is a top-level key ending with "_check"
72
- # Old nested structure: all checks under "checks" key
71
+ # 1. Old nested structure: all checks under "checks" key
72
+ # 2. New flat structure: each check is a top-level key ending with "_check"
73
+ # 3. Default config structure: check IDs directly at top level (without "_check" suffix)
73
74
  if "checks" in self.config_dict:
74
75
  # Old nested structure
75
76
  self.checks_config = self.config_dict.get("checks", {})
76
77
  else:
77
- # New flat structure - extract all keys ending with "_check"
78
- self.checks_config = {
79
- key.replace("_check", ""): value
80
- for key, value in self.config_dict.items()
81
- if key.endswith("_check") and isinstance(value, dict)
82
- }
78
+ # New flat structure and default config structure
79
+ # Extract all keys ending with "_check" OR that look like check configurations
80
+ self.checks_config = {}
81
+
82
+ # First, add keys ending with "_check"
83
+ for key, value in self.config_dict.items():
84
+ if key.endswith("_check") and isinstance(value, dict):
85
+ self.checks_config[key.replace("_check", "")] = value
86
+
87
+ # Then, add top-level keys that look like check configurations
88
+ # (they have dict values and contain typical check config keys like enabled, severity, etc.)
89
+ for key, value in self.config_dict.items():
90
+ if (
91
+ key
92
+ not in [
93
+ "settings",
94
+ "custom_checks",
95
+ "custom_checks_dir",
96
+ ] # Skip special config keys
97
+ and not key.endswith("_check") # Skip if already processed above
98
+ and isinstance(value, dict) # Must be a dict
99
+ and key not in self.checks_config # Not already added
100
+ ):
101
+ # This looks like a check configuration
102
+ self.checks_config[key] = value
83
103
 
84
104
  self.custom_checks = self.config_dict.get("custom_checks", [])
85
105
  self.custom_checks_dir = self.config_dict.get("custom_checks_dir")
@@ -36,13 +36,18 @@ class EnhancedFormatter(OutputFormatter):
36
36
 
37
37
  Args:
38
38
  report: Validation report to format
39
- **kwargs: Additional options (color: bool = True)
39
+ **kwargs: Additional options:
40
+ - color (bool): Enable color output (default: True)
41
+ - show_summary (bool): Show Executive Summary panel (default: True)
42
+ - show_severity_breakdown (bool): Show Issue Severity Breakdown panel (default: True)
40
43
 
41
44
  Returns:
42
45
  Formatted string with ANSI codes for console display
43
46
  """
44
47
  # Allow disabling color for plain text output
45
48
  color = kwargs.get("color", True)
49
+ show_summary = kwargs.get("show_summary", True)
50
+ show_severity_breakdown = kwargs.get("show_severity_breakdown", True)
46
51
 
47
52
  # Use StringIO to capture Rich console output
48
53
  string_buffer = StringIO()
@@ -58,11 +63,12 @@ class EnhancedFormatter(OutputFormatter):
58
63
  console.print(Panel(title, border_style="bright_blue", padding=(1, 0)))
59
64
  console.print()
60
65
 
61
- # Executive Summary with progress bars
62
- self._print_summary_panel(console, report)
66
+ # Executive Summary with progress bars (optional)
67
+ if show_summary:
68
+ self._print_summary_panel(console, report)
63
69
 
64
- # Severity breakdown if there are issues
65
- if report.total_issues > 0:
70
+ # Severity breakdown if there are issues (optional)
71
+ if show_severity_breakdown and report.total_issues > 0:
66
72
  console.print()
67
73
  self._print_severity_breakdown(console, report)
68
74
 
@@ -100,7 +100,7 @@ class SARIFFormatter(OutputFormatter):
100
100
  "text": "The specified condition key is not valid for this action"
101
101
  },
102
102
  "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_condition.html",
103
- "defaultConfiguration": {"level": "warning"},
103
+ "defaultConfiguration": {"level": "error"},
104
104
  },
105
105
  {
106
106
  "id": "invalid-resource",
@@ -110,18 +110,55 @@ class SARIFFormatter(OutputFormatter):
110
110
  "defaultConfiguration": {"level": "error"},
111
111
  },
112
112
  {
113
- "id": "security-wildcard",
114
- "shortDescription": {"text": "Overly Permissive Wildcard"},
113
+ "id": "duplicate-sid",
114
+ "shortDescription": {"text": "Duplicate Statement ID"},
115
+ "fullDescription": {
116
+ "text": "Multiple statements use the same Statement ID (Sid), which can cause confusion"
117
+ },
118
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_sid.html",
119
+ "defaultConfiguration": {"level": "error"},
120
+ },
121
+ {
122
+ "id": "overly-permissive",
123
+ "shortDescription": {"text": "Overly Permissive Policy"},
115
124
  "fullDescription": {
116
- "text": "Using wildcards in actions or resources can be a security risk"
125
+ "text": "Policy grants overly broad permissions using wildcards in actions or resources"
117
126
  },
118
- "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html",
127
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege",
119
128
  "defaultConfiguration": {"level": "warning"},
120
129
  },
121
130
  {
122
- "id": "security-sensitive-action",
123
- "shortDescription": {"text": "Sensitive Action Without Conditions"},
124
- "fullDescription": {"text": "Sensitive actions should have condition restrictions"},
131
+ "id": "missing-condition",
132
+ "shortDescription": {"text": "Missing Condition Restrictions"},
133
+ "fullDescription": {
134
+ "text": "Sensitive actions should include condition restrictions to limit when they can be used"
135
+ },
136
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#use-policy-conditions",
137
+ "defaultConfiguration": {"level": "warning"},
138
+ },
139
+ {
140
+ "id": "missing-required-condition",
141
+ "shortDescription": {"text": "Missing Required Condition"},
142
+ "fullDescription": {
143
+ "text": "Specific actions require certain conditions to prevent privilege escalation or security issues"
144
+ },
145
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html",
146
+ "defaultConfiguration": {"level": "error"},
147
+ },
148
+ {
149
+ "id": "invalid-principal",
150
+ "shortDescription": {"text": "Invalid Principal"},
151
+ "fullDescription": {
152
+ "text": "The specified principal is invalid or improperly formatted"
153
+ },
154
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html",
155
+ "defaultConfiguration": {"level": "error"},
156
+ },
157
+ {
158
+ "id": "general-issue",
159
+ "shortDescription": {"text": "IAM Policy Issue"},
160
+ "fullDescription": {"text": "General IAM policy validation issue"},
161
+ "helpUri": "https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies.html",
125
162
  "defaultConfiguration": {"level": "warning"},
126
163
  },
127
164
  ]
@@ -170,18 +207,45 @@ class SARIFFormatter(OutputFormatter):
170
207
  return results
171
208
 
172
209
  def _get_rule_id(self, issue: ValidationIssue) -> str:
173
- """Map issue to SARIF rule ID."""
210
+ """Map issue to SARIF rule ID.
211
+
212
+ Uses the issue_type field directly, converting underscores to hyphens
213
+ for SARIF rule ID format. Falls back to heuristic matching for unknown types.
214
+ """
215
+ # Map common issue types directly
216
+ issue_type_map = {
217
+ "invalid_action": "invalid-action",
218
+ "invalid_condition_key": "invalid-condition-key",
219
+ "invalid_resource": "invalid-resource",
220
+ "duplicate_sid": "duplicate-sid",
221
+ "overly_permissive": "overly-permissive",
222
+ "missing_condition": "missing-condition",
223
+ "missing_required_condition": "missing-required-condition",
224
+ "invalid_principal": "invalid-principal",
225
+ }
226
+
227
+ # Try direct mapping from issue_type
228
+ if issue.issue_type in issue_type_map:
229
+ return issue_type_map[issue.issue_type]
230
+
231
+ # Fallback: heuristic matching based on message
174
232
  message_lower = issue.message.lower()
175
233
 
176
234
  if "action" in message_lower and "not found" in message_lower:
177
235
  return "invalid-action"
178
236
  elif "condition key" in message_lower:
179
237
  return "invalid-condition-key"
180
- elif "arn" in message_lower or "resource" in message_lower:
238
+ elif "duplicate" in message_lower and "sid" in message_lower:
239
+ return "duplicate-sid"
240
+ elif "wildcard" in message_lower or "overly permissive" in message_lower:
241
+ return "overly-permissive"
242
+ elif "missing" in message_lower and "condition" in message_lower:
243
+ if "required" in message_lower:
244
+ return "missing-required-condition"
245
+ return "missing-condition"
246
+ elif "principal" in message_lower:
247
+ return "invalid-principal"
248
+ elif "resource" in message_lower or "arn" in message_lower:
181
249
  return "invalid-resource"
182
- elif "wildcard" in message_lower or "*" in issue.message:
183
- return "security-wildcard"
184
- elif "sensitive" in message_lower:
185
- return "security-sensitive-action"
186
250
  else:
187
251
  return "general-issue"
@@ -4,10 +4,18 @@ This module defines Pydantic models for AWS service information,
4
4
  IAM policies, and validation results.
5
5
  """
6
6
 
7
- from typing import Any, ClassVar
7
+ from typing import Any, ClassVar, Literal
8
8
 
9
9
  from pydantic import BaseModel, ConfigDict, Field
10
10
 
11
+ # Policy Type Constants
12
+ PolicyType = Literal[
13
+ "IDENTITY_POLICY",
14
+ "RESOURCE_POLICY",
15
+ "SERVICE_CONTROL_POLICY",
16
+ "RESOURCE_CONTROL_POLICY",
17
+ ]
18
+
11
19
 
12
20
  # AWS Service Reference Models
13
21
  class ServiceInfo(BaseModel):
@@ -202,9 +210,10 @@ class ValidationIssue(BaseModel):
202
210
 
203
211
  parts = []
204
212
 
205
- # Add identifier for bot comment cleanup
213
+ # Add identifier for bot comment cleanup (HTML comment - not visible to users)
206
214
  if include_identifier:
207
- parts.append("🤖 IAM Policy Validator\n")
215
+ parts.append("<!-- iam-policy-validator-review -->\n")
216
+ parts.append("🤖 **IAM Policy Validator**\n")
208
217
 
209
218
  # Build statement context for better navigation
210
219
  statement_context = f"Statement[{self.statement_index}]"
@@ -240,6 +249,7 @@ class PolicyValidationResult(BaseModel):
240
249
 
241
250
  policy_file: str
242
251
  is_valid: bool
252
+ policy_type: PolicyType = "IDENTITY_POLICY"
243
253
  issues: list[ValidationIssue] = Field(default_factory=list)
244
254
  actions_checked: int = 0
245
255
  condition_keys_checked: int = 0
@@ -16,6 +16,7 @@ from iam_validator.core.aws_fetcher import AWSServiceFetcher
16
16
  from iam_validator.core.check_registry import CheckRegistry
17
17
  from iam_validator.core.models import (
18
18
  IAMPolicy,
19
+ PolicyType,
19
20
  PolicyValidationResult,
20
21
  Statement,
21
22
  ValidationIssue,
@@ -112,17 +113,30 @@ class PolicyValidator:
112
113
  logger.debug(f"Could not find field line in {policy_file}: {e}")
113
114
  return None
114
115
 
115
- async def validate_policy(self, policy: IAMPolicy, policy_file: str) -> PolicyValidationResult:
116
+ async def validate_policy(
117
+ self, policy: IAMPolicy, policy_file: str, policy_type: PolicyType = "IDENTITY_POLICY"
118
+ ) -> PolicyValidationResult:
116
119
  """Validate a complete IAM policy.
117
120
 
118
121
  Args:
119
122
  policy: IAM policy to validate
120
123
  policy_file: Path to the policy file
124
+ policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
121
125
 
122
126
  Returns:
123
127
  PolicyValidationResult with all findings
124
128
  """
125
- result = PolicyValidationResult(policy_file=policy_file, is_valid=True)
129
+ result = PolicyValidationResult(
130
+ policy_file=policy_file, is_valid=True, policy_type=policy_type
131
+ )
132
+
133
+ # Apply automatic policy-type validation (not configurable - always runs)
134
+ from iam_validator.checks import policy_type_validation
135
+
136
+ policy_type_issues = await policy_type_validation.execute_policy(
137
+ policy, policy_file, policy_type=policy_type
138
+ )
139
+ result.issues.extend(policy_type_issues)
126
140
 
127
141
  for idx, statement in enumerate(policy.statement):
128
142
  # Get line number for this statement
@@ -460,6 +474,7 @@ async def validate_policies(
460
474
  config_path: str | None = None,
461
475
  use_registry: bool = True,
462
476
  custom_checks_dir: str | None = None,
477
+ policy_type: PolicyType = "IDENTITY_POLICY",
463
478
  ) -> list[PolicyValidationResult]:
464
479
  """Validate multiple policies concurrently.
465
480
 
@@ -468,6 +483,7 @@ async def validate_policies(
468
483
  config_path: Optional path to configuration file
469
484
  use_registry: If True, use CheckRegistry system; if False, use legacy validator
470
485
  custom_checks_dir: Optional path to directory containing custom checks for auto-discovery
486
+ policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
471
487
 
472
488
  Returns:
473
489
  List of validation results
@@ -492,7 +508,10 @@ async def validate_policies(
492
508
  ) as fetcher:
493
509
  validator = PolicyValidator(fetcher)
494
510
 
495
- tasks = [validator.validate_policy(policy, file_path) for file_path, policy in policies]
511
+ tasks = [
512
+ validator.validate_policy(policy, file_path, policy_type)
513
+ for file_path, policy in policies
514
+ ]
496
515
 
497
516
  results = await asyncio.gather(*tasks)
498
517
 
@@ -560,7 +579,9 @@ async def validate_policies(
560
579
  aws_services_dir=aws_services_dir,
561
580
  ) as fetcher:
562
581
  tasks = [
563
- _validate_policy_with_registry(policy, file_path, registry, fetcher, fail_on_severities)
582
+ _validate_policy_with_registry(
583
+ policy, file_path, registry, fetcher, fail_on_severities, policy_type
584
+ )
564
585
  for file_path, policy in policies
565
586
  ]
566
587
 
@@ -575,6 +596,7 @@ async def _validate_policy_with_registry(
575
596
  registry: CheckRegistry,
576
597
  fetcher: AWSServiceFetcher,
577
598
  fail_on_severities: list[str] | None = None,
599
+ policy_type: PolicyType = "IDENTITY_POLICY",
578
600
  ) -> PolicyValidationResult:
579
601
  """Validate a single policy using the CheckRegistry system.
580
602
 
@@ -584,15 +606,26 @@ async def _validate_policy_with_registry(
584
606
  registry: CheckRegistry instance with configured checks
585
607
  fetcher: AWS service fetcher instance
586
608
  fail_on_severities: List of severity levels that should cause validation to fail
609
+ policy_type: Type of policy (IDENTITY_POLICY, RESOURCE_POLICY, SERVICE_CONTROL_POLICY)
587
610
 
588
611
  Returns:
589
612
  PolicyValidationResult with all findings
590
613
  """
591
- result = PolicyValidationResult(policy_file=policy_file, is_valid=True)
614
+ result = PolicyValidationResult(policy_file=policy_file, is_valid=True, policy_type=policy_type)
615
+
616
+ # Apply automatic policy-type validation (not configurable - always runs)
617
+ from iam_validator.checks import policy_type_validation
618
+
619
+ policy_type_issues = await policy_type_validation.execute_policy(
620
+ policy, policy_file, policy_type=policy_type
621
+ )
622
+ result.issues.extend(policy_type_issues)
592
623
 
593
624
  # Run policy-level checks first (checks that need to see the entire policy)
594
625
  # These checks examine relationships between statements, not individual statements
595
- policy_level_issues = await registry.execute_policy_checks(policy, policy_file, fetcher)
626
+ policy_level_issues = await registry.execute_policy_checks(
627
+ policy, policy_file, fetcher, policy_type
628
+ )
596
629
  result.issues.extend(policy_level_issues)
597
630
 
598
631
  # Execute all statement-level checks for each statement
@@ -20,17 +20,25 @@ class PRCommenter:
20
20
  # Identifier for bot comments (used for cleanup/updates)
21
21
  BOT_IDENTIFIER = "🤖 IAM Policy Validator"
22
22
  SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
23
- REVIEW_IDENTIFIER = "🤖 IAM Policy Validator"
23
+ REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
24
24
 
25
- def __init__(self, github: GitHubIntegration | None = None, cleanup_old_comments: bool = True):
25
+ def __init__(
26
+ self,
27
+ github: GitHubIntegration | None = None,
28
+ cleanup_old_comments: bool = True,
29
+ fail_on_severities: list[str] | None = None,
30
+ ):
26
31
  """Initialize PR commenter.
27
32
 
28
33
  Args:
29
34
  github: GitHubIntegration instance (will create one if None)
30
35
  cleanup_old_comments: Whether to clean up old bot comments before posting new ones
36
+ fail_on_severities: List of severity levels that should trigger REQUEST_CHANGES
37
+ (e.g., ["error", "critical", "high"])
31
38
  """
32
39
  self.github = github
33
40
  self.cleanup_old_comments = cleanup_old_comments
41
+ self.fail_on_severities = fail_on_severities or ["error", "critical"]
34
42
 
35
43
  async def post_findings_to_pr(
36
44
  self,
@@ -136,17 +144,22 @@ class PRCommenter:
136
144
  for file_comments in comments_by_file.values():
137
145
  all_comments.extend(file_comments)
138
146
 
139
- # Determine review event based on issues
140
- has_errors = any(
141
- issue.severity == "error" for result in report.results for issue in result.issues
147
+ # Determine review event based on fail_on_severities config
148
+ # Check if any issue has a severity that should trigger REQUEST_CHANGES
149
+ has_blocking_issues = any(
150
+ issue.severity in self.fail_on_severities
151
+ for result in report.results
152
+ for issue in result.issues
142
153
  )
143
154
 
144
- event = ReviewEvent.REQUEST_CHANGES if has_errors else ReviewEvent.COMMENT
155
+ # Set review event: request changes if any blocking issues, else comment
156
+ event = ReviewEvent.REQUEST_CHANGES if has_blocking_issues else ReviewEvent.COMMENT
145
157
 
146
- # Post review with comments (include identifier in review body for potential future cleanup)
158
+ # Post review with comments (include identifier in review body for cleanup)
147
159
  review_body = (
148
160
  f"{self.REVIEW_IDENTIFIER}\n\n"
149
- f"## IAM Policy Validation Results\n\n"
161
+ f"🤖 **IAM Policy Validator**\n\n"
162
+ f"## Validation Results\n\n"
150
163
  f"Found {report.total_issues} issues across {report.total_policies} policies.\n"
151
164
  f"See inline comments for details."
152
165
  )
@@ -279,6 +292,7 @@ async def post_report_to_pr(
279
292
  report_file: str,
280
293
  create_review: bool = True,
281
294
  add_summary: bool = True,
295
+ config_path: str | None = None,
282
296
  ) -> bool:
283
297
  """Post a JSON report to a PR.
284
298
 
@@ -286,6 +300,7 @@ async def post_report_to_pr(
286
300
  report_file: Path to JSON report file
287
301
  create_review: Whether to create line-specific review
288
302
  add_summary: Whether to add summary comment
303
+ config_path: Optional path to config file (to get fail_on_severity)
289
304
 
290
305
  Returns:
291
306
  True if successful, False otherwise
@@ -297,9 +312,15 @@ async def post_report_to_pr(
297
312
 
298
313
  report = ValidationReport.model_validate(report_data)
299
314
 
315
+ # Load config to get fail_on_severity setting
316
+ from iam_validator.core.config_loader import ConfigLoader
317
+
318
+ config = ConfigLoader.load_config(config_path)
319
+ fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
320
+
300
321
  # Post to PR
301
322
  async with GitHubIntegration() as github:
302
- commenter = PRCommenter(github)
323
+ commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
303
324
  return await commenter.post_findings_to_pr(
304
325
  report,
305
326
  create_review=create_review,
@@ -1,54 +0,0 @@
1
- iam_validator/__init__.py,sha256=APnMR3Fu4fHhxfsHBvUM2dJIwazgvLKQbfOsSgFPidg,693
2
- iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=hbgDe5p_vG5JrspHS61bAQLyKxbRMqbUDzeKUVq_gmo,206
4
- iam_validator/checks/__init__.py,sha256=eKTPgiZ1i3zvyP6OdKgLx9s3u69onITMYifmJPJwZgM,968
5
- iam_validator/checks/action_condition_enforcement.py,sha256=3M1Wj89Af6H-ywBTruZbJPzhCBBQVanVb5hwv-fkiDE,29721
6
- iam_validator/checks/action_resource_constraint.py,sha256=p-gP7S9QYR6M7vffrnJY6LOlMUTn0kpEbrxQ8pTY5rs,6031
7
- iam_validator/checks/action_validation.py,sha256=IpxtTsk58f2zEZ-xzAoyHw4QK8BCRV43OffP-8ydf9E,2578
8
- iam_validator/checks/condition_key_validation.py,sha256=bc4LQ8IRKyt0RquaQvQvVjmeJnuOUAFRL8xdduLPa_U,2661
9
- iam_validator/checks/policy_size.py,sha256=4cvZiWRJXGuvYo8PRcdD1Py_ZL8Xw0lOJfXTs6EX-_I,5753
10
- iam_validator/checks/resource_validation.py,sha256=AEIoiR6AKYLuVaA8ne3QE5qy6NCMDe98_2JAiwE9-JU,4261
11
- iam_validator/checks/security_best_practices.py,sha256=uf3ZAhBkyN8ka9bZHWi2kkAGIibhqWMIF06DBXsgu9U,23093
12
- iam_validator/checks/sid_uniqueness.py,sha256=U2Kk5lYi9mHhhTpCWAD0ZQfxcLnIJJa7KGC5nOzTEbY,5145
13
- iam_validator/checks/utils/__init__.py,sha256=j0X4ibUB6RGx2a-kNoJnlVZwHfoEvzZsIeTmJIAoFzA,45
14
- iam_validator/checks/utils/policy_level_checks.py,sha256=2V60C0zhKfsFPjQ-NMlD3EemtwA9S6-4no8nETgXdQE,5274
15
- iam_validator/checks/utils/sensitive_action_matcher.py,sha256=VlTpgjMnympYa28kOdm6xRIUL2P87rOvm1O2NdnjtVI,8900
16
- iam_validator/checks/utils/wildcard_expansion.py,sha256=V3V_KRpapOzPBhpUObJjGHoMhvCH90QvDxppeEHIG_U,3152
17
- iam_validator/commands/__init__.py,sha256=M-5bo8w0TCWydK0cXgJyPD2fmk8bpQs-3b26YbgLzlc,565
18
- iam_validator/commands/analyze.py,sha256=TWlDaZ8gVOdNv6__KQQfzeLVW36qLiL5IzlhGYfvq_g,16501
19
- iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gzs,1074
20
- iam_validator/commands/cache.py,sha256=NHfbIDWI8tj-3o-4fIZJQS-Vvd9bxIH3Lk6kBtNuiUU,14212
21
- iam_validator/commands/download_services.py,sha256=anRcobOuhkiEmHpwW_AJb1e2ifgkgYAO2-b9-JBrBcg,9152
22
- iam_validator/commands/post_to_pr.py,sha256=hl_K-XlELYN-ArjMdgQqysvIE-26yf9XdrMl4ToDwG0,2148
23
- iam_validator/commands/validate.py,sha256=R295cOTly8n7zL1jfvbh9RuCgiM5edBqbf6YMn_4G9A,14013
24
- iam_validator/core/__init__.py,sha256=1FvJPMrbzJfS9YbRUJCshJLd5gzWwR9Fd_slS0Aq9c8,416
25
- iam_validator/core/access_analyzer.py,sha256=poeT1i74jXpKr1B3UmvqiTvCTbq82zffWgZHwiFUwoo,24337
26
- iam_validator/core/access_analyzer_report.py,sha256=IrQVszlhFfQ6WykYLpig7TU3hf8dnQTegPDsOvHjR5Q,24873
27
- iam_validator/core/aws_fetcher.py,sha256=0rG7qi3Lz6ulU6pDL0nZ6sklgSAS5pwo0ViykDspRt8,33382
28
- iam_validator/core/aws_global_conditions.py,sha256=ADVcMEWhgvDZWdBmRUQN3HB7a9OycbTLecXFAy3LPbo,5837
29
- iam_validator/core/check_registry.py,sha256=wxqaF2t_3lWgT6x7_PnnZ8XGjHKUxUk72UlmdYBLFyo,15679
30
- iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,3843
31
- iam_validator/core/config_loader.py,sha256=Pq2rd6LJtEZET0ZeW4hEZS2ZRLC5gNRsKbtLyIsT21I,16516
32
- iam_validator/core/defaults.py,sha256=brGPx0_8zmsMNddYryMKbcoIh8VJq2mdXZdGDItAsQs,13251
33
- iam_validator/core/models.py,sha256=rWIZnD-I81Sg4asgOhnB10FWJC5mxQ2JO9bdS0sHb4Q,10772
34
- iam_validator/core/policy_checks.py,sha256=pMlZ2XkuqppVOUZq__e8w_yGoy7lIHjAB5RiTXwJo4Q,25114
35
- iam_validator/core/policy_loader.py,sha256=TR7SpzlRG3TwH4HBGEFUuhNOmxIR8Cud2SQ-AmHWBpM,14040
36
- iam_validator/core/pr_commenter.py,sha256=TOhVXKTFcRHQ9EVuShXQcKXn9aNjB1mU6FnR2jvltmw,10581
37
- iam_validator/core/report.py,sha256=Yeh_u9jQvTyDV3ignyPcWEQVfFcxNZNrxf4T0fjeWb4,33283
38
- iam_validator/core/formatters/__init__.py,sha256=fnCKAEBXItnOf2m4rhVs7zwMaTxbG6ESh3CF8V5j5ec,868
39
- iam_validator/core/formatters/base.py,sha256=SShDeDiy5mYQnS6BpA8xYg91N-KX1EObkOtlrVHqx1Q,4451
40
- iam_validator/core/formatters/console.py,sha256=lX4Yp4bTW61fxe0fCiHuO6bCZtC_6cjCwqDNQ55nT_8,1937
41
- iam_validator/core/formatters/csv.py,sha256=2FaN6Y_0TPMFOb3A3tNtj0-9bkEc5P-6eZ7eLROIqFE,5899
42
- iam_validator/core/formatters/enhanced.py,sha256=-W9JACV4FNVWoWtfVxXLla4d__Gg96SASbNAijpJnT0,16638
43
- iam_validator/core/formatters/html.py,sha256=j4sQi-wXiD9kCHldW5JCzbJe0frhiP5uQI9KlH3Sj_g,22994
44
- iam_validator/core/formatters/json.py,sha256=A7gZ8P32GEdbDvrSn6v56yQ4fOP_kyMaoFVXG2bgnew,939
45
- iam_validator/core/formatters/markdown.py,sha256=aPAY6FpZBHsVBDag3FAsB_X9CZzznFjX9dQr0ysDrTE,2251
46
- iam_validator/core/formatters/sarif.py,sha256=tqp8g7RmUh0HRk-kKDaucx4sa-5I9ikgkSpy1MM8Vi4,7200
47
- iam_validator/integrations/__init__.py,sha256=7Hlor_X9j0NZaEjFuSvoXAAuSKQ-zgY19Rk-Dz3JpKo,616
48
- iam_validator/integrations/github_integration.py,sha256=bKs94vNT4PmcmUPUeuY2WJFhCYpUY2SWiBP1vj-andA,25673
49
- iam_validator/integrations/ms_teams.py,sha256=t2PlWuTDb6GGH-eDU1jnOKd8D1w4FCB68bahGA7MJcE,14475
50
- iam_policy_validator-1.3.1.dist-info/METADATA,sha256=NNF1fvnG9g8pGMopQ71yn5rHtWnRIVMBUGPEeNLX9jI,29465
51
- iam_policy_validator-1.3.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
52
- iam_policy_validator-1.3.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
53
- iam_policy_validator-1.3.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
54
- iam_policy_validator-1.3.1.dist-info/RECORD,,