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,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)
|