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.

Files changed (56) hide show
  1. iam_policy_validator-1.4.0.dist-info/METADATA +1022 -0
  2. iam_policy_validator-1.4.0.dist-info/RECORD +56 -0
  3. iam_policy_validator-1.4.0.dist-info/WHEEL +4 -0
  4. iam_policy_validator-1.4.0.dist-info/entry_points.txt +2 -0
  5. iam_policy_validator-1.4.0.dist-info/licenses/LICENSE +21 -0
  6. iam_validator/__init__.py +27 -0
  7. iam_validator/__main__.py +11 -0
  8. iam_validator/__version__.py +7 -0
  9. iam_validator/checks/__init__.py +27 -0
  10. iam_validator/checks/action_condition_enforcement.py +727 -0
  11. iam_validator/checks/action_resource_constraint.py +151 -0
  12. iam_validator/checks/action_validation.py +72 -0
  13. iam_validator/checks/condition_key_validation.py +70 -0
  14. iam_validator/checks/policy_size.py +151 -0
  15. iam_validator/checks/policy_type_validation.py +299 -0
  16. iam_validator/checks/principal_validation.py +282 -0
  17. iam_validator/checks/resource_validation.py +108 -0
  18. iam_validator/checks/security_best_practices.py +536 -0
  19. iam_validator/checks/sid_uniqueness.py +170 -0
  20. iam_validator/checks/utils/__init__.py +1 -0
  21. iam_validator/checks/utils/policy_level_checks.py +143 -0
  22. iam_validator/checks/utils/sensitive_action_matcher.py +252 -0
  23. iam_validator/checks/utils/wildcard_expansion.py +87 -0
  24. iam_validator/commands/__init__.py +25 -0
  25. iam_validator/commands/analyze.py +434 -0
  26. iam_validator/commands/base.py +48 -0
  27. iam_validator/commands/cache.py +392 -0
  28. iam_validator/commands/download_services.py +260 -0
  29. iam_validator/commands/post_to_pr.py +86 -0
  30. iam_validator/commands/validate.py +539 -0
  31. iam_validator/core/__init__.py +14 -0
  32. iam_validator/core/access_analyzer.py +666 -0
  33. iam_validator/core/access_analyzer_report.py +643 -0
  34. iam_validator/core/aws_fetcher.py +880 -0
  35. iam_validator/core/aws_global_conditions.py +137 -0
  36. iam_validator/core/check_registry.py +469 -0
  37. iam_validator/core/cli.py +134 -0
  38. iam_validator/core/config_loader.py +452 -0
  39. iam_validator/core/defaults.py +393 -0
  40. iam_validator/core/formatters/__init__.py +27 -0
  41. iam_validator/core/formatters/base.py +147 -0
  42. iam_validator/core/formatters/console.py +59 -0
  43. iam_validator/core/formatters/csv.py +170 -0
  44. iam_validator/core/formatters/enhanced.py +434 -0
  45. iam_validator/core/formatters/html.py +672 -0
  46. iam_validator/core/formatters/json.py +33 -0
  47. iam_validator/core/formatters/markdown.py +63 -0
  48. iam_validator/core/formatters/sarif.py +187 -0
  49. iam_validator/core/models.py +298 -0
  50. iam_validator/core/policy_checks.py +656 -0
  51. iam_validator/core/policy_loader.py +396 -0
  52. iam_validator/core/pr_commenter.py +338 -0
  53. iam_validator/core/report.py +859 -0
  54. iam_validator/integrations/__init__.py +28 -0
  55. iam_validator/integrations/github_integration.py +795 -0
  56. iam_validator/integrations/ms_teams.py +442 -0
@@ -0,0 +1,442 @@
1
+ """Microsoft Teams Integration for IAM Policy Validator.
2
+
3
+ This module provides functionality to send notifications to MS Teams channels
4
+ via incoming webhooks, including validation reports and alerts.
5
+ """
6
+
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+ from typing import Any
11
+
12
+ import httpx
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class MessageType(str, Enum):
18
+ """MS Teams message types."""
19
+
20
+ INFO = "info"
21
+ SUCCESS = "success"
22
+ WARNING = "warning"
23
+ ERROR = "error"
24
+
25
+
26
+ class CardTheme(str, Enum):
27
+ """MS Teams adaptive card accent colors."""
28
+
29
+ DEFAULT = "default"
30
+ DARK = "dark"
31
+ LIGHT = "light"
32
+ ACCENT = "accent"
33
+ ATTENTION = "attention" # Red for errors
34
+ GOOD = "good" # Green for success
35
+ WARNING = "warning" # Yellow for warnings
36
+
37
+
38
+ @dataclass
39
+ class TeamsMessage:
40
+ """Represents a message to send to MS Teams."""
41
+
42
+ title: str
43
+ message: str
44
+ message_type: MessageType = MessageType.INFO
45
+ facts: list[dict[str, str]] | None = None
46
+ actions: list[dict[str, Any]] | None = None
47
+ sections: list[dict[str, Any]] | None = None
48
+
49
+
50
+ class MSTeamsIntegration:
51
+ """Handles Microsoft Teams notifications via webhooks.
52
+
53
+ This class provides methods to:
54
+ - Send simple text notifications
55
+ - Send adaptive cards with rich formatting
56
+ - Send validation reports as formatted messages
57
+ - Send alerts for critical findings
58
+ """
59
+
60
+ def __init__(self, webhook_url: str | None = None):
61
+ """Initialize MS Teams integration.
62
+
63
+ Args:
64
+ webhook_url: MS Teams incoming webhook URL
65
+ """
66
+ self.webhook_url = self._validate_webhook_url(webhook_url)
67
+ self._client: httpx.AsyncClient | None = None
68
+
69
+ def _validate_webhook_url(self, webhook_url: str | None) -> str | None:
70
+ """Validate and sanitize webhook URL.
71
+
72
+ Args:
73
+ webhook_url: Webhook URL to validate
74
+
75
+ Returns:
76
+ Validated URL or None
77
+ """
78
+ if webhook_url is None:
79
+ return None
80
+
81
+ if not isinstance(webhook_url, str) or not webhook_url.strip():
82
+ logger.warning("Invalid webhook URL provided (empty or non-string)")
83
+ return None
84
+
85
+ webhook_url = webhook_url.strip()
86
+
87
+ # Must be HTTPS for security
88
+ if not webhook_url.startswith("https://"):
89
+ logger.warning(
90
+ f"Webhook URL must use HTTPS: {webhook_url[:50]}... "
91
+ "(MS Teams webhooks require HTTPS)"
92
+ )
93
+ return None
94
+
95
+ # Basic URL validation - should contain office.com or webhook.office365.com
96
+ if "webhook.office" not in webhook_url.lower():
97
+ logger.warning(
98
+ f"Webhook URL doesn't appear to be a valid MS Teams webhook: {webhook_url[:50]}..."
99
+ )
100
+ # Still allow it, but warn
101
+
102
+ # Length check to prevent extremely long URLs
103
+ if len(webhook_url) > 2048:
104
+ logger.warning(
105
+ f"Webhook URL is unusually long ({len(webhook_url)} chars), may be invalid"
106
+ )
107
+ return None
108
+
109
+ # Ensure only ASCII characters
110
+ if not webhook_url.isascii():
111
+ logger.warning("Webhook URL contains non-ASCII characters")
112
+ return None
113
+
114
+ return webhook_url
115
+
116
+ async def __aenter__(self) -> "MSTeamsIntegration":
117
+ """Async context manager entry."""
118
+ self._client = httpx.AsyncClient(timeout=httpx.Timeout(30.0))
119
+ return self
120
+
121
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
122
+ """Async context manager exit."""
123
+ del exc_type, exc_val, exc_tb # Unused
124
+ if self._client:
125
+ await self._client.aclose()
126
+ self._client = None
127
+
128
+ def is_configured(self) -> bool:
129
+ """Check if MS Teams integration is configured.
130
+
131
+ Returns:
132
+ True if webhook URL is set
133
+ """
134
+ return bool(self.webhook_url)
135
+
136
+ def _get_card_color(self, message_type: MessageType) -> str:
137
+ """Get the accent color for the card based on message type."""
138
+ color_mapping = {
139
+ MessageType.INFO: "0078D4", # Blue
140
+ MessageType.SUCCESS: "107C10", # Green
141
+ MessageType.WARNING: "FFB900", # Yellow
142
+ MessageType.ERROR: "D83B01", # Red
143
+ }
144
+ return color_mapping.get(message_type, "0078D4")
145
+
146
+ def _create_adaptive_card(self, message: TeamsMessage) -> dict[str, Any]:
147
+ """Create an adaptive card for MS Teams.
148
+
149
+ Args:
150
+ message: TeamsMessage object with content
151
+
152
+ Returns:
153
+ Adaptive card payload
154
+ """
155
+ card = {
156
+ "type": "message",
157
+ "attachments": [
158
+ {
159
+ "contentType": "application/vnd.microsoft.card.adaptive",
160
+ "contentUrl": None,
161
+ "content": {
162
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
163
+ "type": "AdaptiveCard",
164
+ "version": "1.4",
165
+ "body": [
166
+ {
167
+ "type": "Container",
168
+ "style": "emphasis",
169
+ "items": [
170
+ {
171
+ "type": "TextBlock",
172
+ "text": message.title,
173
+ "weight": "bolder",
174
+ "size": "large",
175
+ "wrap": True,
176
+ }
177
+ ],
178
+ },
179
+ {
180
+ "type": "Container",
181
+ "items": [
182
+ {
183
+ "type": "TextBlock",
184
+ "text": message.message,
185
+ "wrap": True,
186
+ }
187
+ ],
188
+ },
189
+ ],
190
+ },
191
+ }
192
+ ],
193
+ }
194
+
195
+ # Add facts if provided
196
+ if message.facts:
197
+ fact_set = {
198
+ "type": "FactSet",
199
+ "facts": [{"title": f["title"], "value": f["value"]} for f in message.facts],
200
+ }
201
+ card["attachments"][0]["content"]["body"].append(fact_set)
202
+
203
+ # Add custom sections if provided
204
+ if message.sections:
205
+ for section in message.sections:
206
+ card["attachments"][0]["content"]["body"].append(section)
207
+
208
+ # Add actions if provided
209
+ if message.actions:
210
+ card["attachments"][0]["content"]["actions"] = message.actions
211
+
212
+ return card
213
+
214
+ def _create_simple_card(self, title: str, text: str, theme_color: str) -> dict[str, Any]:
215
+ """Create a simple message card (legacy format).
216
+
217
+ Args:
218
+ title: Card title
219
+ text: Card text (markdown supported)
220
+ theme_color: Hex color for the card accent
221
+
222
+ Returns:
223
+ Message card payload
224
+ """
225
+ return {
226
+ "@type": "MessageCard",
227
+ "@context": "https://schema.org/extensions",
228
+ "themeColor": theme_color,
229
+ "title": title,
230
+ "text": text,
231
+ }
232
+
233
+ async def send_message(self, message: TeamsMessage, use_adaptive_card: bool = True) -> bool:
234
+ """Send a message to MS Teams.
235
+
236
+ Args:
237
+ message: TeamsMessage object to send
238
+ use_adaptive_card: If True, use adaptive card format; else use legacy format
239
+
240
+ Returns:
241
+ True if successful, False otherwise
242
+ """
243
+ if not self.is_configured():
244
+ logger.warning("MS Teams integration not configured (no webhook URL)")
245
+ return False
246
+
247
+ # Type safety: webhook_url is guaranteed to be str here due to is_configured() check
248
+ if self.webhook_url is None:
249
+ logger.error("Webhook URL is None despite configuration check")
250
+ return False
251
+
252
+ try:
253
+ if use_adaptive_card:
254
+ payload = self._create_adaptive_card(message)
255
+ else:
256
+ color = self._get_card_color(message.message_type)
257
+ payload = self._create_simple_card(message.title, message.message, color)
258
+
259
+ if self._client:
260
+ response = await self._client.post(self.webhook_url, json=payload, timeout=30.0)
261
+ else:
262
+ async with httpx.AsyncClient(timeout=httpx.Timeout(30.0)) as client:
263
+ response = await client.post(self.webhook_url, json=payload)
264
+
265
+ response.raise_for_status()
266
+ logger.info(f"Successfully sent message to MS Teams: {message.title}")
267
+ return True
268
+
269
+ except httpx.HTTPStatusError as e:
270
+ logger.error(f"HTTP error sending to MS Teams: {e.response.status_code}")
271
+ return False
272
+ except Exception as e:
273
+ logger.error(f"Failed to send message to MS Teams: {e}")
274
+ return False
275
+
276
+ async def send_validation_report(
277
+ self,
278
+ report_title: str,
279
+ total_issues: int,
280
+ errors: int,
281
+ warnings: int,
282
+ suggestions: int,
283
+ policy_files: list[str],
284
+ report_url: str | None = None,
285
+ ) -> bool:
286
+ """Send a validation report to MS Teams.
287
+
288
+ Args:
289
+ report_title: Title of the report
290
+ total_issues: Total number of issues found
291
+ errors: Number of errors
292
+ warnings: Number of warnings
293
+ suggestions: Number of suggestions
294
+ policy_files: List of policy files validated
295
+ report_url: Optional URL to full report
296
+
297
+ Returns:
298
+ True if successful, False otherwise
299
+ """
300
+ # Determine message type based on findings
301
+ if errors > 0:
302
+ message_type = MessageType.ERROR
303
+ status = "❌ Validation Failed"
304
+ elif warnings > 0:
305
+ message_type = MessageType.WARNING
306
+ status = "⚠️ Validation Passed with Warnings"
307
+ else:
308
+ message_type = MessageType.SUCCESS
309
+ status = "✅ Validation Passed"
310
+
311
+ facts = [
312
+ {"title": "Status", "value": status},
313
+ {"title": "Total Issues", "value": str(total_issues)},
314
+ {"title": "Errors", "value": str(errors)},
315
+ {"title": "Warnings", "value": str(warnings)},
316
+ {"title": "Suggestions", "value": str(suggestions)},
317
+ {"title": "Files Validated", "value": str(len(policy_files))},
318
+ ]
319
+
320
+ actions = []
321
+ if report_url:
322
+ actions.append(
323
+ {
324
+ "type": "Action.OpenUrl",
325
+ "title": "View Full Report",
326
+ "url": report_url,
327
+ }
328
+ )
329
+
330
+ message = TeamsMessage(
331
+ title=report_title,
332
+ message=f"IAM Policy validation completed for {len(policy_files)} file(s).",
333
+ message_type=message_type,
334
+ facts=facts,
335
+ actions=actions,
336
+ )
337
+
338
+ return await self.send_message(message)
339
+
340
+ async def send_pr_notification(
341
+ self,
342
+ pr_number: int,
343
+ pr_title: str,
344
+ pr_url: str,
345
+ validation_passed: bool,
346
+ issue_summary: dict[str, int],
347
+ ) -> bool:
348
+ """Send a PR validation notification to MS Teams.
349
+
350
+ Args:
351
+ pr_number: Pull request number
352
+ pr_title: Pull request title
353
+ pr_url: URL to the PR
354
+ validation_passed: Whether validation passed
355
+ issue_summary: Dictionary with issue counts by severity
356
+
357
+ Returns:
358
+ True if successful, False otherwise
359
+ """
360
+ if validation_passed:
361
+ message_type = MessageType.SUCCESS
362
+ status = "✅ PR validation passed"
363
+ else:
364
+ message_type = MessageType.ERROR
365
+ status = "❌ PR validation failed"
366
+
367
+ facts = [
368
+ {"title": "PR Number", "value": f"#{pr_number}"},
369
+ {"title": "Status", "value": status},
370
+ ]
371
+
372
+ # Add issue counts
373
+ for severity, count in issue_summary.items():
374
+ if count > 0:
375
+ facts.append({"title": severity.capitalize(), "value": str(count)})
376
+
377
+ actions = [
378
+ {
379
+ "type": "Action.OpenUrl",
380
+ "title": "View Pull Request",
381
+ "url": pr_url,
382
+ }
383
+ ]
384
+
385
+ message = TeamsMessage(
386
+ title=f"PR #{pr_number}: {pr_title}",
387
+ message="IAM Policy validation completed for pull request.",
388
+ message_type=message_type,
389
+ facts=facts,
390
+ actions=actions,
391
+ )
392
+
393
+ return await self.send_message(message)
394
+
395
+ async def send_alert(
396
+ self,
397
+ title: str,
398
+ message: str,
399
+ severity: MessageType = MessageType.WARNING,
400
+ details: list[str] | None = None,
401
+ ) -> bool:
402
+ """Send an alert notification to MS Teams.
403
+
404
+ Args:
405
+ title: Alert title
406
+ message: Alert message
407
+ severity: Alert severity level
408
+ details: Optional list of detail items
409
+
410
+ Returns:
411
+ True if successful, False otherwise
412
+ """
413
+ sections = []
414
+ if details:
415
+ detail_text = "\n".join([f"• {detail}" for detail in details])
416
+ sections.append(
417
+ {
418
+ "type": "Container",
419
+ "items": [
420
+ {
421
+ "type": "TextBlock",
422
+ "text": "Details:",
423
+ "weight": "bolder",
424
+ },
425
+ {
426
+ "type": "TextBlock",
427
+ "text": detail_text,
428
+ "wrap": True,
429
+ "spacing": "small",
430
+ },
431
+ ],
432
+ }
433
+ )
434
+
435
+ teams_message = TeamsMessage(
436
+ title=title,
437
+ message=message,
438
+ message_type=severity,
439
+ sections=sections,
440
+ )
441
+
442
+ return await self.send_message(teams_message)