iam-policy-validator 1.4.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.
Potentially problematic release.
This version of iam-policy-validator might be problematic. Click here for more details.
- iam_policy_validator-1.4.0.dist-info/METADATA +1022 -0
- iam_policy_validator-1.4.0.dist-info/RECORD +56 -0
- iam_policy_validator-1.4.0.dist-info/WHEEL +4 -0
- iam_policy_validator-1.4.0.dist-info/entry_points.txt +2 -0
- iam_policy_validator-1.4.0.dist-info/licenses/LICENSE +21 -0
- iam_validator/__init__.py +27 -0
- iam_validator/__main__.py +11 -0
- iam_validator/__version__.py +7 -0
- iam_validator/checks/__init__.py +27 -0
- iam_validator/checks/action_condition_enforcement.py +727 -0
- iam_validator/checks/action_resource_constraint.py +151 -0
- iam_validator/checks/action_validation.py +72 -0
- iam_validator/checks/condition_key_validation.py +70 -0
- iam_validator/checks/policy_size.py +151 -0
- iam_validator/checks/policy_type_validation.py +299 -0
- iam_validator/checks/principal_validation.py +282 -0
- iam_validator/checks/resource_validation.py +108 -0
- iam_validator/checks/security_best_practices.py +536 -0
- iam_validator/checks/sid_uniqueness.py +170 -0
- iam_validator/checks/utils/__init__.py +1 -0
- iam_validator/checks/utils/policy_level_checks.py +143 -0
- iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
- iam_validator/checks/utils/wildcard_expansion.py +87 -0
- iam_validator/commands/__init__.py +25 -0
- iam_validator/commands/analyze.py +434 -0
- iam_validator/commands/base.py +48 -0
- iam_validator/commands/cache.py +392 -0
- iam_validator/commands/download_services.py +260 -0
- iam_validator/commands/post_to_pr.py +86 -0
- iam_validator/commands/validate.py +539 -0
- iam_validator/core/__init__.py +14 -0
- iam_validator/core/access_analyzer.py +666 -0
- iam_validator/core/access_analyzer_report.py +643 -0
- iam_validator/core/aws_fetcher.py +880 -0
- iam_validator/core/aws_global_conditions.py +137 -0
- iam_validator/core/check_registry.py +469 -0
- iam_validator/core/cli.py +134 -0
- iam_validator/core/config_loader.py +452 -0
- iam_validator/core/defaults.py +393 -0
- iam_validator/core/formatters/__init__.py +27 -0
- iam_validator/core/formatters/base.py +147 -0
- iam_validator/core/formatters/console.py +59 -0
- iam_validator/core/formatters/csv.py +170 -0
- iam_validator/core/formatters/enhanced.py +434 -0
- iam_validator/core/formatters/html.py +672 -0
- iam_validator/core/formatters/json.py +33 -0
- iam_validator/core/formatters/markdown.py +63 -0
- iam_validator/core/formatters/sarif.py +187 -0
- iam_validator/core/models.py +298 -0
- iam_validator/core/policy_checks.py +656 -0
- iam_validator/core/policy_loader.py +396 -0
- iam_validator/core/pr_commenter.py +338 -0
- iam_validator/core/report.py +859 -0
- iam_validator/integrations/__init__.py +28 -0
- iam_validator/integrations/github_integration.py +795 -0
- iam_validator/integrations/ms_teams.py +442 -0
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"""PR Comment Module.
|
|
2
|
+
|
|
3
|
+
This module handles posting validation findings as PR comments.
|
|
4
|
+
It reads a JSON report and posts line-specific comments to GitHub PRs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from iam_validator.core.models import ValidationIssue, ValidationReport
|
|
12
|
+
from iam_validator.integrations.github_integration import GitHubIntegration, ReviewEvent
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PRCommenter:
|
|
18
|
+
"""Posts validation findings as PR comments."""
|
|
19
|
+
|
|
20
|
+
# Identifier for bot comments (used for cleanup/updates)
|
|
21
|
+
BOT_IDENTIFIER = "🤖 IAM Policy Validator"
|
|
22
|
+
SUMMARY_IDENTIFIER = "<!-- iam-policy-validator-summary -->"
|
|
23
|
+
REVIEW_IDENTIFIER = "<!-- iam-policy-validator-review -->"
|
|
24
|
+
|
|
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
|
+
):
|
|
31
|
+
"""Initialize PR commenter.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
github: GitHubIntegration instance (will create one if None)
|
|
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"])
|
|
38
|
+
"""
|
|
39
|
+
self.github = github
|
|
40
|
+
self.cleanup_old_comments = cleanup_old_comments
|
|
41
|
+
self.fail_on_severities = fail_on_severities or ["error", "critical"]
|
|
42
|
+
|
|
43
|
+
async def post_findings_to_pr(
|
|
44
|
+
self,
|
|
45
|
+
report: ValidationReport,
|
|
46
|
+
create_review: bool = True,
|
|
47
|
+
add_summary_comment: bool = True,
|
|
48
|
+
) -> bool:
|
|
49
|
+
"""Post validation findings to a PR.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
report: Validation report with findings
|
|
53
|
+
create_review: Whether to create a PR review with line comments
|
|
54
|
+
add_summary_comment: Whether to add a summary comment
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
True if successful, False otherwise
|
|
58
|
+
"""
|
|
59
|
+
if self.github is None:
|
|
60
|
+
self.github = GitHubIntegration()
|
|
61
|
+
|
|
62
|
+
if not self.github.is_configured():
|
|
63
|
+
logger.error("GitHub integration not configured")
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
success = True
|
|
67
|
+
|
|
68
|
+
# Clean up old bot comments if enabled
|
|
69
|
+
if self.cleanup_old_comments and create_review:
|
|
70
|
+
logger.info("Cleaning up old review comments from previous runs...")
|
|
71
|
+
await self.github.cleanup_bot_review_comments(self.REVIEW_IDENTIFIER)
|
|
72
|
+
|
|
73
|
+
# Post summary comment (potentially as multiple parts)
|
|
74
|
+
if add_summary_comment:
|
|
75
|
+
from iam_validator.core.report import ReportGenerator
|
|
76
|
+
|
|
77
|
+
generator = ReportGenerator()
|
|
78
|
+
comment_parts = generator.generate_github_comment_parts(report)
|
|
79
|
+
|
|
80
|
+
# Post all parts using the multipart method
|
|
81
|
+
if not await self.github.post_multipart_comments(
|
|
82
|
+
comment_parts, self.SUMMARY_IDENTIFIER
|
|
83
|
+
):
|
|
84
|
+
logger.error("Failed to post summary comment(s)")
|
|
85
|
+
success = False
|
|
86
|
+
else:
|
|
87
|
+
if len(comment_parts) > 1:
|
|
88
|
+
logger.info(f"Posted summary in {len(comment_parts)} parts")
|
|
89
|
+
else:
|
|
90
|
+
logger.info("Posted summary comment")
|
|
91
|
+
|
|
92
|
+
# Post line-specific review comments
|
|
93
|
+
if create_review:
|
|
94
|
+
if not await self._post_review_comments(report):
|
|
95
|
+
logger.error("Failed to post review comments")
|
|
96
|
+
success = False
|
|
97
|
+
|
|
98
|
+
return success
|
|
99
|
+
|
|
100
|
+
async def _post_review_comments(self, report: ValidationReport) -> bool:
|
|
101
|
+
"""Post line-specific review comments.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
report: Validation report
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
True if successful, False otherwise
|
|
108
|
+
"""
|
|
109
|
+
if not self.github:
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
# Group issues by file
|
|
113
|
+
comments_by_file: dict[str, list[dict[str, Any]]] = {}
|
|
114
|
+
|
|
115
|
+
for result in report.results:
|
|
116
|
+
if not result.issues:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Try to determine line numbers from the policy file
|
|
120
|
+
line_mapping = self._get_line_mapping(result.policy_file)
|
|
121
|
+
|
|
122
|
+
for issue in result.issues:
|
|
123
|
+
# Determine the line number for this issue
|
|
124
|
+
line_number = self._find_issue_line(issue, result.policy_file, line_mapping)
|
|
125
|
+
|
|
126
|
+
if line_number:
|
|
127
|
+
comment = {
|
|
128
|
+
"path": result.policy_file,
|
|
129
|
+
"line": line_number,
|
|
130
|
+
"body": issue.to_pr_comment(),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if result.policy_file not in comments_by_file:
|
|
134
|
+
comments_by_file[result.policy_file] = []
|
|
135
|
+
comments_by_file[result.policy_file].append(comment)
|
|
136
|
+
|
|
137
|
+
# If no line-specific comments, skip
|
|
138
|
+
if not comments_by_file:
|
|
139
|
+
logger.info("No line-specific comments to post")
|
|
140
|
+
return True
|
|
141
|
+
|
|
142
|
+
# Flatten comments list
|
|
143
|
+
all_comments = []
|
|
144
|
+
for file_comments in comments_by_file.values():
|
|
145
|
+
all_comments.extend(file_comments)
|
|
146
|
+
|
|
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
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Set review event: request changes if any blocking issues, else comment
|
|
156
|
+
event = ReviewEvent.REQUEST_CHANGES if has_blocking_issues else ReviewEvent.COMMENT
|
|
157
|
+
|
|
158
|
+
# Post review with comments (include identifier in review body for cleanup)
|
|
159
|
+
review_body = (
|
|
160
|
+
f"{self.REVIEW_IDENTIFIER}\n\n"
|
|
161
|
+
f"🤖 **IAM Policy Validator**\n\n"
|
|
162
|
+
f"## Validation Results\n\n"
|
|
163
|
+
f"Found {report.total_issues} issues across {report.total_policies} policies.\n"
|
|
164
|
+
f"See inline comments for details."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return await self.github.create_review_with_comments(
|
|
168
|
+
comments=all_comments,
|
|
169
|
+
body=review_body,
|
|
170
|
+
event=event,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _get_line_mapping(self, policy_file: str) -> dict[int, int]:
|
|
174
|
+
"""Get mapping of statement indices to line numbers.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
policy_file: Path to policy file
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Dict mapping statement index to line number
|
|
181
|
+
"""
|
|
182
|
+
try:
|
|
183
|
+
with open(policy_file, encoding="utf-8") as f:
|
|
184
|
+
lines = f.readlines()
|
|
185
|
+
|
|
186
|
+
mapping: dict[int, int] = {}
|
|
187
|
+
statement_count = 0
|
|
188
|
+
in_statement_array = False
|
|
189
|
+
|
|
190
|
+
for line_num, line in enumerate(lines, start=1):
|
|
191
|
+
stripped = line.strip()
|
|
192
|
+
|
|
193
|
+
# Detect "Statement": [ or "Statement" : [
|
|
194
|
+
if '"Statement"' in stripped or "'Statement'" in stripped:
|
|
195
|
+
in_statement_array = True
|
|
196
|
+
continue
|
|
197
|
+
|
|
198
|
+
# Detect statement object start
|
|
199
|
+
if in_statement_array and stripped.startswith("{"):
|
|
200
|
+
mapping[statement_count] = line_num
|
|
201
|
+
statement_count += 1
|
|
202
|
+
|
|
203
|
+
return mapping
|
|
204
|
+
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.warning(f"Could not parse {policy_file} for line mapping: {e}")
|
|
207
|
+
return {}
|
|
208
|
+
|
|
209
|
+
def _find_issue_line(
|
|
210
|
+
self,
|
|
211
|
+
issue: ValidationIssue,
|
|
212
|
+
policy_file: str,
|
|
213
|
+
line_mapping: dict[int, int],
|
|
214
|
+
) -> int | None:
|
|
215
|
+
"""Find the line number for an issue.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
issue: Validation issue
|
|
219
|
+
policy_file: Path to policy file
|
|
220
|
+
line_mapping: Statement index to line number mapping
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Line number or None
|
|
224
|
+
"""
|
|
225
|
+
# If issue has explicit line number, use it
|
|
226
|
+
if issue.line_number:
|
|
227
|
+
return issue.line_number
|
|
228
|
+
|
|
229
|
+
# Otherwise, use statement mapping
|
|
230
|
+
if issue.statement_index in line_mapping:
|
|
231
|
+
return line_mapping[issue.statement_index]
|
|
232
|
+
|
|
233
|
+
# Fallback: try to find specific field in file
|
|
234
|
+
search_term = issue.action or issue.resource or issue.condition_key
|
|
235
|
+
if search_term:
|
|
236
|
+
return self._search_for_field_line(policy_file, issue.statement_index, search_term)
|
|
237
|
+
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
def _search_for_field_line(
|
|
241
|
+
self, policy_file: str, statement_idx: int, search_term: str
|
|
242
|
+
) -> int | None:
|
|
243
|
+
"""Search for a specific field within a statement.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
policy_file: Path to policy file
|
|
247
|
+
statement_idx: Statement index
|
|
248
|
+
search_term: Term to search for
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
Line number or None
|
|
252
|
+
"""
|
|
253
|
+
try:
|
|
254
|
+
with open(policy_file, encoding="utf-8") as f:
|
|
255
|
+
lines = f.readlines()
|
|
256
|
+
|
|
257
|
+
# Find the statement block
|
|
258
|
+
statement_count = 0
|
|
259
|
+
in_statement = False
|
|
260
|
+
brace_depth = 0
|
|
261
|
+
|
|
262
|
+
for line_num, line in enumerate(lines, start=1):
|
|
263
|
+
stripped = line.strip()
|
|
264
|
+
|
|
265
|
+
# Track braces
|
|
266
|
+
brace_depth += stripped.count("{") - stripped.count("}")
|
|
267
|
+
|
|
268
|
+
# Detect statement start
|
|
269
|
+
if not in_statement and stripped.startswith("{") and brace_depth > 0:
|
|
270
|
+
if statement_count == statement_idx:
|
|
271
|
+
in_statement = True
|
|
272
|
+
continue
|
|
273
|
+
statement_count += 1
|
|
274
|
+
|
|
275
|
+
# Search within the statement
|
|
276
|
+
if in_statement:
|
|
277
|
+
if search_term in line:
|
|
278
|
+
return line_num
|
|
279
|
+
|
|
280
|
+
# Exit statement when braces balance
|
|
281
|
+
if brace_depth == 0:
|
|
282
|
+
in_statement = False
|
|
283
|
+
|
|
284
|
+
return None
|
|
285
|
+
|
|
286
|
+
except Exception as e:
|
|
287
|
+
logger.debug(f"Could not search {policy_file}: {e}")
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
async def post_report_to_pr(
|
|
292
|
+
report_file: str,
|
|
293
|
+
create_review: bool = True,
|
|
294
|
+
add_summary: bool = True,
|
|
295
|
+
config_path: str | None = None,
|
|
296
|
+
) -> bool:
|
|
297
|
+
"""Post a JSON report to a PR.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
report_file: Path to JSON report file
|
|
301
|
+
create_review: Whether to create line-specific review
|
|
302
|
+
add_summary: Whether to add summary comment
|
|
303
|
+
config_path: Optional path to config file (to get fail_on_severity)
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
True if successful, False otherwise
|
|
307
|
+
"""
|
|
308
|
+
try:
|
|
309
|
+
# Load report from JSON
|
|
310
|
+
with open(report_file, encoding="utf-8") as f:
|
|
311
|
+
report_data = json.load(f)
|
|
312
|
+
|
|
313
|
+
report = ValidationReport.model_validate(report_data)
|
|
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
|
+
|
|
321
|
+
# Post to PR
|
|
322
|
+
async with GitHubIntegration() as github:
|
|
323
|
+
commenter = PRCommenter(github, fail_on_severities=fail_on_severities)
|
|
324
|
+
return await commenter.post_findings_to_pr(
|
|
325
|
+
report,
|
|
326
|
+
create_review=create_review,
|
|
327
|
+
add_summary_comment=add_summary,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
except FileNotFoundError:
|
|
331
|
+
logger.error(f"Report file not found: {report_file}")
|
|
332
|
+
return False
|
|
333
|
+
except json.JSONDecodeError as e:
|
|
334
|
+
logger.error(f"Invalid JSON in report file: {e}")
|
|
335
|
+
return False
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.error(f"Failed to post report to PR: {e}")
|
|
338
|
+
return False
|