iam-policy-validator 1.9.0__py3-none-any.whl → 1.10.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iam-policy-validator
3
- Version: 1.9.0
3
+ Version: 1.10.1
4
4
  Summary: Validate AWS IAM policies for correctness and security using AWS Service Reference API
5
5
  Project-URL: Homepage, https://github.com/boogy/iam-policy-validator
6
6
  Project-URL: Documentation, https://github.com/boogy/iam-policy-validator/tree/main/docs
@@ -1,6 +1,6 @@
1
1
  iam_validator/__init__.py,sha256=xHdUASOxFHwEXfT_GSr_KrkLlnxZ-pAAr1wW1PwAGko,693
2
2
  iam_validator/__main__.py,sha256=to_nz3n_IerJpVVZZ6WSFlFR5s_06J0csfPOTfQZG8g,197
3
- iam_validator/__version__.py,sha256=IUF2zxBY8L_m2_x_tEDJHqdJthjPhXgbZg7CYXJCCrA,361
3
+ iam_validator/__version__.py,sha256=SRRdMee5oIovcL25-xNSjSxoHM9bzGQhwmx_U4YhnXQ,362
4
4
  iam_validator/checks/__init__.py,sha256=OTkPnmlelu4YjMO8krjhu2wXiTV72RzopA5u1SfPQA0,1990
5
5
  iam_validator/checks/action_condition_enforcement.py,sha256=0dCH_xX-Xc0uLxtNeRjrpNjWYbdWQRzO1XNcLTSn6sI,51698
6
6
  iam_validator/checks/action_resource_matching.py,sha256=WiGJmCIJfx5yituMjZxpKmk-99N6nK20ueN02ddy9oM,19296
@@ -31,7 +31,7 @@ iam_validator/commands/base.py,sha256=5baCCMwxz7pdQ6XMpWfXFNz7i1l5dB8Qv9dKKR04Gz
31
31
  iam_validator/commands/cache.py,sha256=llfyQzPE5Azd5YcW0ohYcYjF_OCyiQ1GoJQ982t71lQ,14294
32
32
  iam_validator/commands/download_services.py,sha256=KKz3ybMLT8DQUf9aFZ0tilJ-o1b6PE8Pf1pC4K6cT8I,9175
33
33
  iam_validator/commands/post_to_pr.py,sha256=CvUXs2xvO-UhluxdfNM6F0TCWD8hDBEOiYw60fm1Dms,2363
34
- iam_validator/commands/validate.py,sha256=Z6GHLeKV8oINSTXaZ0asBxa56S1G4ORwOBqrAz3Xx-M,23945
34
+ iam_validator/commands/validate.py,sha256=cvrgYagYm7W29MYsitZsLcttIIqVKQMRm-bCGY7N3fU,24355
35
35
  iam_validator/core/__init__.py,sha256=hYXkSbxplKzhM6dqrVzV4M3k7GKLsZbgExypxKq74gs,376
36
36
  iam_validator/core/access_analyzer.py,sha256=mtMaY-FnKjKEVITky_9ywZe1FaCAm61ElRv5Z_ZeC7E,24562
37
37
  iam_validator/core/access_analyzer_report.py,sha256=UMm2RNGj2rAKav1zsCw_htQZZRwRC0jjayd2zvKma1A,24896
@@ -41,10 +41,11 @@ iam_validator/core/cli.py,sha256=PkXiZjlgrQ21QustBbspefYsdbxst4gxoClyG2_HQR8,384
41
41
  iam_validator/core/condition_validators.py,sha256=7zBjlcf2xGFKGbcFrXSLvWT5tFhWxoqwzhsJqS2E8uY,21524
42
42
  iam_validator/core/constants.py,sha256=cVBPgbXr4ALltH_NTSKsgBi6wmndLnOyUWhyBx0ZwrM,6113
43
43
  iam_validator/core/ignore_patterns.py,sha256=pZqDJBtkbck-85QK5eFPM5ZOPEKs3McRh3avqiCT5z0,10398
44
- iam_validator/core/models.py,sha256=f5d9ovtO1xMSwhyBrKIgc2psEq0eugnd3S3ioqurqEE,13242
44
+ iam_validator/core/label_manager.py,sha256=48CRASWg98wyjfVF_1pUzj6dm9itzmG7SeIWf0TSUfc,7502
45
+ iam_validator/core/models.py,sha256=yQ5iBTffdAzx88h8RyVCCmBg6kkD2zg5_lb-qLdjy3w,13386
45
46
  iam_validator/core/policy_checks.py,sha256=FNVuS2GTffwCjjrlupVIazC172gSxKYAAT_ObV6Apbo,8803
46
47
  iam_validator/core/policy_loader.py,sha256=2KJnXzGg3g9pDXWZHk3DO0xpZnZZ-wXWFEOdQ_naJ8s,17862
47
- iam_validator/core/pr_commenter.py,sha256=MU-t7SfdHUpSc6BDbh8_dNAbxDiG-bZBCry-jUXivAc,15066
48
+ iam_validator/core/pr_commenter.py,sha256=NTKoSmjvspYX2rbl3Xn8d611XkTNSfYlGUY0zBHBP4g,16801
48
49
  iam_validator/core/report.py,sha256=kzSeWnT1LqWZVA5pqKKz-maVowXVj0djdoShfRhhpz4,35899
49
50
  iam_validator/core/aws_service/__init__.py,sha256=UqMh4HUdGlx2QF5OoueJJ2UlCnhX4QW_x3KeE_bxRQc,735
50
51
  iam_validator/core/aws_service/cache.py,sha256=DPuOOPPJC867KAYgV1e0RyQs_k3mtefMdYli3jPaN64,3589
@@ -53,14 +54,14 @@ iam_validator/core/aws_service/fetcher.py,sha256=X4iI6fiLj4l9f3W6_J0E58lSP26UsBh
53
54
  iam_validator/core/aws_service/parsers.py,sha256=gJzR7HCD8ItCWCCbguTQIZpPEdj2rdMwC7LPhu7ve14,5174
54
55
  iam_validator/core/aws_service/patterns.py,sha256=gGc55Tn-EJ3cmcWtmYAZROUajKYz7DaMchYWGEhHpC0,1726
55
56
  iam_validator/core/aws_service/storage.py,sha256=PrfKdvF60IL7E_8xYs_XwFoAJPRcVYw57FVLHCoqwVk,10429
56
- iam_validator/core/aws_service/validators.py,sha256=rgCScqEjXNH8xNg2R91eJbb4eIV3jZN7a6VW0n0hgA4,16347
57
+ iam_validator/core/aws_service/validators.py,sha256=AY0BjydskXoesEzUShH4gZKp6gtSX7s1rCLP_iOZQMc,16493
57
58
  iam_validator/core/config/__init__.py,sha256=CWSyIA7kEyzrskEenjYbs9Iih10BXRpiY9H2dHg61rU,2671
58
59
  iam_validator/core/config/aws_api.py,sha256=HLIzOItQ0A37wxHcgWck6ZFO0wmNY8JNTiWMMK6JKYU,1248
59
60
  iam_validator/core/config/aws_global_conditions.py,sha256=gdmMxXGBy95B3uYUG-J7rnM6Ixgc6L7Y9Pcd2XAMb60,7170
60
61
  iam_validator/core/config/category_suggestions.py,sha256=QlrYi4BTkxDSTlL7NZGE9BWN-atWetZ6XjkI9F_7YzI,4370
61
62
  iam_validator/core/config/condition_requirements.py,sha256=qauIP73HFnOw1dchUeFpg1x7Y7QWkILo3GfxV_dxdQo,7696
62
63
  iam_validator/core/config/config_loader.py,sha256=qKD8aR8YAswaFf68pnYJLFNwKznvcc6lNxSQWU3i6SY,17713
63
- iam_validator/core/config/defaults.py,sha256=rWzDrlw0AAudtm_If6zjNFvruLg71jpLJEdRgKYSKMQ,27917
64
+ iam_validator/core/config/defaults.py,sha256=qpFP534dgCQ-vjCdhkK7ZslDoTm9Ftgy20qmYZsSYUI,28637
64
65
  iam_validator/core/config/principal_requirements.py,sha256=VCX7fBDgeDTJQyoz7_x7GI7Kf9O1Eu-sbihoHOrKv6o,15105
65
66
  iam_validator/core/config/sensitive_actions.py,sha256=uATDIp_TD3OQQlsYTZp79qd1mSK2Bf9hJ0JwcqLBr84,25344
66
67
  iam_validator/core/config/service_principals.py,sha256=8pys5H_yycVJ9KTyimAKFYBg83Aol2Iri53wiHjtnEM,3959
@@ -82,14 +83,14 @@ iam_validator/sdk/arn_matching.py,sha256=HSDpLltOYISq-SoPebAlM89mKOaUaghq_04urch
82
83
  iam_validator/sdk/context.py,sha256=FvAEyUa_s7tHWoSdgjSkzHf1CLlYpAEmLZANxs2IJ4A,6826
83
84
  iam_validator/sdk/exceptions.py,sha256=tm91TxIwU157U_UHN7w5qICf_OhU11agj6pV5W_YP-4,1023
84
85
  iam_validator/sdk/helpers.py,sha256=sjfK0na_Fo7O8GhEVhl44rVHqOdw6nAKkBL4FVL-QdU,5697
85
- iam_validator/sdk/policy_utils.py,sha256=CZS1OGSdiWsd2lsCwg0BDcUNWa61tUwgvn-P5rKqeN8,12987
86
+ iam_validator/sdk/policy_utils.py,sha256=Fh-QElhmPypzSJuF9rcrY7y46Gz3hQu3-yN5b1_mSHY,13579
86
87
  iam_validator/sdk/shortcuts.py,sha256=EVNSYV7rv4TFH03ulsZ3mS1UVmTSp2jKpc2AXs4j1q4,8531
87
88
  iam_validator/utils/__init__.py,sha256=NveA2F3G1E6-ANZzFr7J6Q6u5mogvMp862iFokmYuCs,1021
88
89
  iam_validator/utils/cache.py,sha256=wOQKOBeoG6QqC5f0oXcHz63Cjtu_-SsSS-0pTSwyAiM,3254
89
90
  iam_validator/utils/regex.py,sha256=xHoMECttb7qaMhts-c9b0GIxdhHNZTt-UBr7wNhWfzg,6219
90
91
  iam_validator/utils/terminal.py,sha256=FsRaRMH_JAyDgXWBCOgOEhbS89cs17HCmKYoughq5io,724
91
- iam_policy_validator-1.9.0.dist-info/METADATA,sha256=y2uizxt2ScM8UTUd1UPHqkazCKhTMdyzVGKFEJQqc18,19069
92
- iam_policy_validator-1.9.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
93
- iam_policy_validator-1.9.0.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
94
- iam_policy_validator-1.9.0.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
95
- iam_policy_validator-1.9.0.dist-info/RECORD,,
92
+ iam_policy_validator-1.10.1.dist-info/METADATA,sha256=lIMmE1Y6TX34sh3stfe9J2_q5ATb9fYZqVqOupYNcL8,19070
93
+ iam_policy_validator-1.10.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
94
+ iam_policy_validator-1.10.1.dist-info/entry_points.txt,sha256=8HtWd8O7mvPiPdZR5YbzY8or_qcqLM4-pKaFdhtFT8M,62
95
+ iam_policy_validator-1.10.1.dist-info/licenses/LICENSE,sha256=AMnbFTBDcK4_MITe2wiQBkj0vg-jjBBhsc43ydC7tt4,1098
96
+ iam_policy_validator-1.10.1.dist-info/RECORD,,
@@ -3,7 +3,7 @@
3
3
  This file is the single source of truth for the package version.
4
4
  """
5
5
 
6
- __version__ = "1.9.0"
6
+ __version__ = "1.10.1"
7
7
  # Parse version, handling pre-release suffixes like -rc, -alpha, -beta
8
8
  _version_base = __version__.split("-")[0] # Remove pre-release suffix if present
9
9
  __version_info__ = tuple(int(part) for part in _version_base.split("."))
@@ -302,12 +302,17 @@ Examples:
302
302
  from iam_validator.core.config.config_loader import ConfigLoader
303
303
  from iam_validator.core.pr_commenter import PRCommenter
304
304
 
305
- # Load config to get fail_on_severity setting
305
+ # Load config to get fail_on_severity and severity_labels settings
306
306
  config = ConfigLoader.load_config(config_path)
307
307
  fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
308
+ severity_labels = config.get_setting("severity_labels", {})
308
309
 
309
310
  async with GitHubIntegration() as github:
310
- commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
311
+ commenter = PRCommenter(
312
+ github,
313
+ fail_on_severities=fail_on_severities,
314
+ severity_labels=severity_labels,
315
+ )
311
316
  success = await commenter.post_findings_to_pr(
312
317
  report,
313
318
  create_review=getattr(args, "github_review", False),
@@ -426,12 +431,17 @@ Examples:
426
431
  from iam_validator.core.config.config_loader import ConfigLoader
427
432
  from iam_validator.core.pr_commenter import PRCommenter
428
433
 
429
- # Load config to get fail_on_severity setting
434
+ # Load config to get fail_on_severity and severity_labels settings
430
435
  config = ConfigLoader.load_config(config_path)
431
436
  fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
437
+ severity_labels = config.get_setting("severity_labels", {})
432
438
 
433
439
  async with GitHubIntegration() as github:
434
- commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
440
+ commenter = PRCommenter(
441
+ github,
442
+ fail_on_severities=fail_on_severities,
443
+ severity_labels=severity_labels,
444
+ )
435
445
  success = await commenter.post_findings_to_pr(
436
446
  report,
437
447
  create_review=False, # Already posted per-file reviews in streaming mode
@@ -280,9 +280,12 @@ class ServiceValidator:
280
280
  "- `aws:RequestedRegion`\n"
281
281
  "- `aws:SourceIp`\n"
282
282
  "- `aws:SourceVpce`\n"
283
- "- `aws:UserAgent`\n"
283
+ "- `aws:ResourceOrgID`\n"
284
+ "- `aws:PrincipalOrgID`\n"
285
+ "- `aws:SourceAccount`\n"
286
+ "- `aws:PrincipalAccount`\n"
284
287
  "- `aws:CurrentTime`\n"
285
- "- `aws:SecureTransport`\n"
288
+ "- `aws:ResourceAccount`\n"
286
289
  "- `aws:PrincipalArn`\n"
287
290
  "- And many others"
288
291
  )
@@ -75,6 +75,16 @@ DEFAULT_CONFIG = {
75
75
  # IAM Validity: error, warning, info
76
76
  # Security: critical, high, medium, low
77
77
  "fail_on_severity": list(constants.HIGH_SEVERITY_LEVELS),
78
+ # GitHub PR label mapping based on severity findings
79
+ # When issues with these severities are found, apply the corresponding labels
80
+ # If no issues with these severities exist, remove the labels if present
81
+ # Supports both single labels and lists of labels per severity
82
+ # Examples:
83
+ # Single label per severity: {"error": "iam-validity-error", "critical": "security-critical"}
84
+ # Multiple labels per severity: {"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-review"]}
85
+ # Mixed: {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
86
+ # Default: {} (disabled)
87
+ "severity_labels": {},
78
88
  },
79
89
  # ========================================================================
80
90
  # AWS IAM Validation Checks (17 checks total)
@@ -0,0 +1,197 @@
1
+ """Label Manager for GitHub PR Labels based on Severity Findings.
2
+
3
+ This module manages GitHub PR labels based on IAM policy validation severity findings.
4
+ When validation finds issues with specific severities, it applies corresponding labels.
5
+ When those severities are not found, it removes the labels if present.
6
+ """
7
+
8
+ import logging
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from iam_validator.core.models import PolicyValidationResult, ValidationReport
13
+ from iam_validator.integrations.github_integration import GitHubIntegration
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class LabelManager:
19
+ """Manages GitHub PR labels based on severity findings."""
20
+
21
+ def __init__(
22
+ self,
23
+ github: "GitHubIntegration",
24
+ severity_labels: dict[str, str | list[str]] | None = None,
25
+ ):
26
+ """Initialize label manager.
27
+
28
+ Args:
29
+ github: GitHubIntegration instance for API calls
30
+ severity_labels: Mapping of severity levels to label name(s)
31
+ Supports both single labels and lists of labels per severity.
32
+ Examples:
33
+ - Single label per severity:
34
+ {"error": "iam-validity-error", "critical": "security-critical"}
35
+ - Multiple labels per severity:
36
+ {"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-security-review"]}
37
+ - Mixed:
38
+ {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
39
+ """
40
+ self.github = github
41
+ self.severity_labels = severity_labels or {}
42
+
43
+ def is_enabled(self) -> bool:
44
+ """Check if label management is enabled.
45
+
46
+ Returns:
47
+ True if severity_labels is configured and GitHub is configured
48
+ """
49
+ return bool(self.severity_labels) and self.github.is_configured()
50
+
51
+ def _get_severities_in_results(self, results: list["PolicyValidationResult"]) -> set[str]:
52
+ """Extract all severity levels found in validation results.
53
+
54
+ Args:
55
+ results: List of PolicyValidationResult objects
56
+
57
+ Returns:
58
+ Set of severity levels found (e.g., {"error", "critical", "high"})
59
+ """
60
+ severities = set()
61
+ for result in results:
62
+ for issue in result.issues:
63
+ severities.add(issue.severity)
64
+ return severities
65
+
66
+ def _get_severities_in_report(self, report: "ValidationReport") -> set[str]:
67
+ """Extract all severity levels found in validation report.
68
+
69
+ Args:
70
+ report: ValidationReport object
71
+
72
+ Returns:
73
+ Set of severity levels found (e.g., {"error", "critical", "high"})
74
+ """
75
+ return self._get_severities_in_results(report.results)
76
+
77
+ def _determine_labels_to_apply(self, found_severities: set[str]) -> set[str]:
78
+ """Determine which labels should be applied based on found severities.
79
+
80
+ Args:
81
+ found_severities: Set of severity levels found in validation
82
+
83
+ Returns:
84
+ Set of label names to apply
85
+ """
86
+ labels_to_apply = set()
87
+ for severity, labels in self.severity_labels.items():
88
+ if severity in found_severities:
89
+ # Support both single labels and lists of labels
90
+ if isinstance(labels, list):
91
+ labels_to_apply.update(labels)
92
+ else:
93
+ labels_to_apply.add(labels)
94
+ return labels_to_apply
95
+
96
+ def _determine_labels_to_remove(self, found_severities: set[str]) -> set[str]:
97
+ """Determine which labels should be removed based on missing severities.
98
+
99
+ Args:
100
+ found_severities: Set of severity levels found in validation
101
+
102
+ Returns:
103
+ Set of label names to remove
104
+ """
105
+ labels_to_remove = set()
106
+ for severity, labels in self.severity_labels.items():
107
+ if severity not in found_severities:
108
+ # Support both single labels and lists of labels
109
+ if isinstance(labels, list):
110
+ labels_to_remove.update(labels)
111
+ else:
112
+ labels_to_remove.add(labels)
113
+ return labels_to_remove
114
+
115
+ async def manage_labels_from_results(
116
+ self, results: list["PolicyValidationResult"]
117
+ ) -> tuple[bool, int, int]:
118
+ """Manage PR labels based on validation results.
119
+
120
+ This method will:
121
+ 1. Determine which severity levels are present in the results
122
+ 2. Add labels for severities that are found
123
+ 3. Remove labels for severities that are not found
124
+
125
+ Args:
126
+ results: List of PolicyValidationResult objects
127
+
128
+ Returns:
129
+ Tuple of (success, labels_added, labels_removed)
130
+ """
131
+ if not self.is_enabled():
132
+ logger.debug("Label management not enabled (no severity_labels configured)")
133
+ return (True, 0, 0)
134
+
135
+ # Get all severities found in results
136
+ found_severities = self._get_severities_in_results(results)
137
+ logger.debug(f"Found severities in results: {found_severities}")
138
+
139
+ # Determine which labels to apply/remove
140
+ labels_to_apply = self._determine_labels_to_apply(found_severities)
141
+ labels_to_remove = self._determine_labels_to_remove(found_severities)
142
+
143
+ logger.debug(f"Labels to apply: {labels_to_apply}")
144
+ logger.debug(f"Labels to remove: {labels_to_remove}")
145
+
146
+ # Get current labels on PR
147
+ current_labels = set(await self.github.get_labels())
148
+ logger.debug(f"Current PR labels: {current_labels}")
149
+
150
+ # Filter: only add labels that aren't already present
151
+ labels_to_add = labels_to_apply - current_labels
152
+
153
+ # Filter: only remove labels that are currently present
154
+ labels_to_actually_remove = labels_to_remove & current_labels
155
+
156
+ success = True
157
+ added_count = 0
158
+ removed_count = 0
159
+
160
+ # Add new labels
161
+ if labels_to_add:
162
+ logger.info(f"Adding labels to PR: {labels_to_add}")
163
+ if await self.github.add_labels(list(labels_to_add)):
164
+ added_count = len(labels_to_add)
165
+ else:
166
+ logger.error("Failed to add labels to PR")
167
+ success = False
168
+
169
+ # Remove old labels
170
+ for label in labels_to_actually_remove:
171
+ logger.info(f"Removing label from PR: {label}")
172
+ if await self.github.remove_label(label):
173
+ removed_count += 1
174
+ else:
175
+ logger.error(f"Failed to remove label: {label}")
176
+ success = False
177
+
178
+ if added_count > 0 or removed_count > 0:
179
+ logger.info(f"Label management complete: added {added_count}, removed {removed_count}")
180
+ else:
181
+ logger.debug("No label changes needed")
182
+
183
+ return (success, added_count, removed_count)
184
+
185
+ async def manage_labels_from_report(self, report: "ValidationReport") -> tuple[bool, int, int]:
186
+ """Manage PR labels based on validation report.
187
+
188
+ This is a convenience method that extracts results from the report
189
+ and calls manage_labels_from_results().
190
+
191
+ Args:
192
+ report: ValidationReport object
193
+
194
+ Returns:
195
+ Tuple of (success, labels_added, labels_removed)
196
+ """
197
+ return await self.manage_labels_from_results(report.results)
@@ -31,7 +31,7 @@ class ServiceInfo(BaseModel):
31
31
  class ActionDetail(BaseModel):
32
32
  """Details about an AWS IAM action."""
33
33
 
34
- model_config = ConfigDict(populate_by_name=True)
34
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
35
35
 
36
36
  name: str = Field(alias="Name")
37
37
  action_condition_keys: list[str] | None = Field(
@@ -45,7 +45,7 @@ class ActionDetail(BaseModel):
45
45
  class ResourceType(BaseModel):
46
46
  """Details about an AWS resource type."""
47
47
 
48
- model_config = ConfigDict(populate_by_name=True)
48
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
49
49
 
50
50
  name: str = Field(alias="Name")
51
51
  arn_formats: list[str] | None = Field(default=None, alias="ARNFormats")
@@ -68,7 +68,7 @@ class ResourceType(BaseModel):
68
68
  class ConditionKey(BaseModel):
69
69
  """Details about an AWS condition key."""
70
70
 
71
- model_config = ConfigDict(populate_by_name=True)
71
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
72
72
 
73
73
  name: str = Field(alias="Name")
74
74
  description: str | None = Field(default=None, alias="Description")
@@ -78,7 +78,7 @@ class ConditionKey(BaseModel):
78
78
  class ServiceDetail(BaseModel):
79
79
  """Detailed information about an AWS service."""
80
80
 
81
- model_config = ConfigDict(populate_by_name=True)
81
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True)
82
82
 
83
83
  name: str = Field(alias="Name")
84
84
  prefix: str | None = None # Not always present in API response
@@ -106,7 +106,7 @@ class ServiceDetail(BaseModel):
106
106
  class Statement(BaseModel):
107
107
  """IAM policy statement."""
108
108
 
109
- model_config = ConfigDict(populate_by_name=True, extra="allow")
109
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, extra="allow")
110
110
 
111
111
  sid: str | None = Field(default=None, alias="Sid")
112
112
  effect: str | None = Field(default=None, alias="Effect")
@@ -136,7 +136,7 @@ class Statement(BaseModel):
136
136
  class IAMPolicy(BaseModel):
137
137
  """IAM policy document."""
138
138
 
139
- model_config = ConfigDict(populate_by_name=True, extra="allow")
139
+ model_config = ConfigDict(validate_by_name=True, validate_by_alias=True, extra="allow")
140
140
 
141
141
  version: str | None = Field(default=None, alias="Version")
142
142
  statement: list[Statement] | None = Field(default=None, alias="Statement")
@@ -13,7 +13,9 @@ from iam_validator.core.constants import (
13
13
  REVIEW_IDENTIFIER,
14
14
  SUMMARY_IDENTIFIER,
15
15
  )
16
+ from iam_validator.core.label_manager import LabelManager
16
17
  from iam_validator.core.models import ValidationIssue, ValidationReport
18
+ from iam_validator.core.report import ReportGenerator
17
19
  from iam_validator.integrations.github_integration import GitHubIntegration, ReviewEvent
18
20
 
19
21
  logger = logging.getLogger(__name__)
@@ -32,6 +34,7 @@ class PRCommenter:
32
34
  github: GitHubIntegration | None = None,
33
35
  cleanup_old_comments: bool = True,
34
36
  fail_on_severities: list[str] | None = None,
37
+ severity_labels: dict[str, str | list[str]] | None = None,
35
38
  ):
36
39
  """Initialize PR commenter.
37
40
 
@@ -40,16 +43,24 @@ class PRCommenter:
40
43
  cleanup_old_comments: Whether to clean up old bot comments before posting new ones
41
44
  fail_on_severities: List of severity levels that should trigger REQUEST_CHANGES
42
45
  (e.g., ["error", "critical", "high"])
46
+ severity_labels: Mapping of severity levels to label name(s) for automatic label management
47
+ Supports both single labels and lists of labels per severity.
48
+ Examples:
49
+ - Single: {"error": "iam-validity-error", "critical": "security-critical"}
50
+ - Multiple: {"error": ["iam-error", "needs-fix"], "critical": ["security-critical", "needs-review"]}
51
+ - Mixed: {"error": "iam-validity-error", "critical": ["security-critical", "needs-review"]}
43
52
  """
44
53
  self.github = github
45
54
  self.cleanup_old_comments = cleanup_old_comments
46
55
  self.fail_on_severities = fail_on_severities or ["error", "critical"]
56
+ self.severity_labels = severity_labels or {}
47
57
 
48
58
  async def post_findings_to_pr(
49
59
  self,
50
60
  report: ValidationReport,
51
61
  create_review: bool = True,
52
62
  add_summary_comment: bool = True,
63
+ manage_labels: bool = True,
53
64
  ) -> bool:
54
65
  """Post validation findings to a PR.
55
66
 
@@ -57,6 +68,7 @@ class PRCommenter:
57
68
  report: Validation report with findings
58
69
  create_review: Whether to create a PR review with line comments
59
70
  add_summary_comment: Whether to add a summary comment
71
+ manage_labels: Whether to manage PR labels based on severity findings
60
72
 
61
73
  Returns:
62
74
  True if successful, False otherwise
@@ -81,8 +93,6 @@ class PRCommenter:
81
93
 
82
94
  # Post summary comment (potentially as multiple parts)
83
95
  if add_summary_comment:
84
- from iam_validator.core.report import ReportGenerator
85
-
86
96
  generator = ReportGenerator()
87
97
  comment_parts = generator.generate_github_comment_parts(report)
88
98
 
@@ -104,6 +114,18 @@ class PRCommenter:
104
114
  logger.error("Failed to post review comments")
105
115
  success = False
106
116
 
117
+ # Manage PR labels based on severity findings
118
+ if manage_labels and self.severity_labels:
119
+ label_manager = LabelManager(self.github, self.severity_labels)
120
+ label_success, added, removed = await label_manager.manage_labels_from_report(report)
121
+
122
+ if not label_success:
123
+ logger.error("Failed to manage PR labels")
124
+ success = False
125
+ else:
126
+ if added > 0 or removed > 0:
127
+ logger.info(f"Label management: added {added}, removed {removed}")
128
+
107
129
  return success
108
130
 
109
131
  async def _post_review_comments(self, report: ValidationReport) -> bool:
@@ -288,7 +310,7 @@ class PRCommenter:
288
310
 
289
311
  return mapping
290
312
 
291
- except Exception as e:
313
+ except Exception as e: # pylint: disable=broad-exception-caught
292
314
  logger.warning(f"Could not parse {policy_file} for line mapping: {e}")
293
315
  return {}
294
316
 
@@ -369,7 +391,7 @@ class PRCommenter:
369
391
 
370
392
  return None
371
393
 
372
- except Exception as e:
394
+ except Exception as e: # pylint: disable=broad-exception-caught
373
395
  logger.debug(f"Could not search {policy_file}: {e}")
374
396
  return None
375
397
 
@@ -398,15 +420,20 @@ async def post_report_to_pr(
398
420
 
399
421
  report = ValidationReport.model_validate(report_data)
400
422
 
401
- # Load config to get fail_on_severity setting
423
+ # Load config to get fail_on_severity and severity_labels settings
402
424
  from iam_validator.core.config.config_loader import ConfigLoader
403
425
 
404
426
  config = ConfigLoader.load_config(config_path)
405
427
  fail_on_severities = config.get_setting("fail_on_severity", ["error", "critical"])
428
+ severity_labels = config.get_setting("severity_labels", {})
406
429
 
407
430
  # Post to PR
408
431
  async with GitHubIntegration() as github:
409
- commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
432
+ commenter = PRCommenter(
433
+ github,
434
+ fail_on_severities=fail_on_severities,
435
+ severity_labels=severity_labels,
436
+ )
410
437
  return await commenter.post_findings_to_pr(
411
438
  report,
412
439
  create_review=create_review,
@@ -419,6 +446,6 @@ async def post_report_to_pr(
419
446
  except json.JSONDecodeError as e:
420
447
  logger.error(f"Invalid JSON in report file: {e}")
421
448
  return False
422
- except Exception as e:
449
+ except Exception as e: # pylint: disable=broad-exception-caught
423
450
  logger.error(f"Failed to post report to PR: {e}")
424
451
  return False
@@ -63,9 +63,13 @@ def normalize_policy(policy: IAMPolicy) -> IAMPolicy:
63
63
  """
64
64
  # Pydantic model already handles this via Field(alias="Statement")
65
65
  # which expects a list, but we can ensure it's always a list
66
- statements: list[Statement] = (
67
- policy.statement if isinstance(policy.statement, list) else [policy.statement]
68
- )
66
+ if policy.statement is None:
67
+ statements: list[Statement] = []
68
+ elif isinstance(policy.statement, list):
69
+ statements = policy.statement
70
+ else:
71
+ # Single statement - wrap in list
72
+ statements = [policy.statement]
69
73
 
70
74
  # Normalize actions and resources in each statement
71
75
  normalized_statements: list[Statement] = []
@@ -118,6 +122,9 @@ def extract_actions(policy: IAMPolicy) -> list[str]:
118
122
  """
119
123
  actions = set()
120
124
 
125
+ if policy.statement is None:
126
+ return []
127
+
121
128
  for stmt in policy.statement:
122
129
  # Handle Action field
123
130
  if stmt.action:
@@ -150,6 +157,9 @@ def extract_resources(policy: IAMPolicy) -> list[str]:
150
157
  """
151
158
  resources = set()
152
159
 
160
+ if policy.statement is None:
161
+ return []
162
+
153
163
  for stmt in policy.statement:
154
164
  # Handle Resource field
155
165
  if stmt.resource:
@@ -181,7 +191,10 @@ def extract_condition_keys(policy: IAMPolicy) -> list[str]:
181
191
  >>> keys = extract_condition_keys(policy)
182
192
  >>> print(f"Policy uses condition keys: {', '.join(keys)}")
183
193
  """
184
- condition_keys = set()
194
+ condition_keys: set[str] = set()
195
+
196
+ if policy.statement is None:
197
+ return []
185
198
 
186
199
  for stmt in policy.statement:
187
200
  if stmt.condition:
@@ -216,6 +229,9 @@ def find_statements_with_action(policy: IAMPolicy, action: str) -> list[Statemen
216
229
 
217
230
  matching_statements = []
218
231
 
232
+ if policy.statement is None:
233
+ return []
234
+
219
235
  for stmt in policy.statement:
220
236
  stmt_actions = stmt.get_actions()
221
237
 
@@ -250,6 +266,9 @@ def find_statements_with_resource(policy: IAMPolicy, resource: str) -> list[Stat
250
266
 
251
267
  matching_statements = []
252
268
 
269
+ if policy.statement is None:
270
+ return []
271
+
253
272
  for stmt in policy.statement:
254
273
  stmt_resources = stmt.get_resources()
255
274
 
@@ -286,7 +305,8 @@ def merge_policies(*policies: IAMPolicy) -> IAMPolicy:
286
305
 
287
306
  all_statements: list[Statement] = []
288
307
  for policy in policies:
289
- all_statements.extend(policy.statement)
308
+ if policy.statement is not None:
309
+ all_statements.extend(policy.statement)
290
310
 
291
311
  # Use capitalized field names (aliases) for Pydantic model construction
292
312
  return IAMPolicy(
@@ -318,8 +338,9 @@ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
318
338
  condition_keys = extract_condition_keys(policy)
319
339
 
320
340
  # Count allow vs deny statements
321
- allow_count = sum(1 for s in policy.statement if s.effect.lower() == "allow")
322
- deny_count = sum(1 for s in policy.statement if s.effect.lower() == "deny")
341
+ statements = policy.statement or []
342
+ allow_count = sum(1 for s in statements if s.effect and s.effect.lower() == "allow")
343
+ deny_count = sum(1 for s in statements if s.effect and s.effect.lower() == "deny")
323
344
 
324
345
  # Check for wildcards
325
346
  has_wildcard_actions = any("*" in action for action in actions)
@@ -327,7 +348,7 @@ def get_policy_summary(policy: IAMPolicy) -> dict[str, Any]:
327
348
 
328
349
  return {
329
350
  "version": policy.version,
330
- "statement_count": len(policy.statement),
351
+ "statement_count": len(statements),
331
352
  "allow_statements": allow_count,
332
353
  "deny_statements": deny_count,
333
354
  "action_count": len(actions),
@@ -396,6 +417,8 @@ def is_resource_policy(policy: IAMPolicy) -> bool:
396
417
  >>> if is_resource_policy(policy):
397
418
  ... print("This is an S3 bucket policy or similar")
398
419
  """
420
+ if policy.statement is None:
421
+ return False
399
422
  return any(stmt.principal is not None for stmt in policy.statement)
400
423
 
401
424
 
@@ -414,6 +437,9 @@ def has_public_access(policy: IAMPolicy) -> bool:
414
437
  >>> if has_public_access(policy):
415
438
  ... print("WARNING: This policy allows public access!")
416
439
  """
440
+ if policy.statement is None:
441
+ return False
442
+
417
443
  for stmt in policy.statement:
418
444
  if stmt.principal == "*":
419
445
  return True