Skip to content

Commit 62e07d8

Browse files
author
Ahmed Mustafa
committed
feat: Webhook System & Platform Integrations (Phase 5)
Phase 5 - User Experience (P1): 🔗 Webhook System: - Complete webhook management service - Event types: scan.started, scan.completed, vulnerability.found, etc. - HMAC signature verification (SHA-256) - Retry logic with exponential backoff - Delivery logging and monitoring - Async delivery with aiohttp Features: - Register/unregister webhooks - Subscribe to specific events - Automatic retry on failure (configurable) - Webhook signature generation/verification - Delivery status tracking 📢 Slack Integration: - Rich Block Kit messages - Scan completion notifications - Critical finding alerts - Color-coded severity (red/orange/green) - Interactive buttons with report links 🚨 PagerDuty Integration: - Incident creation for critical findings - Severity levels (critical/error/warning/info) - Custom details in incidents - Events API v2 💬 Discord Integration: - Webhook embeds with rich formatting - Color-coded notifications - Scan completion alerts - Custom fields for findings Integration Points: - Scan workflows → webhooks → external services - Critical findings → immediate alerts - Payment events → notifications - Trial expiration → warnings Files Created: - brain/src/cyper_brain/integrations/webhooks.py - brain/src/cyper_brain/integrations/notifications.py - brain/src/cyper_brain/integrations/__init__.py Platform now integrates with: - Custom webhooks - Slack - PagerDuty - Discord Phase 5: 50% complete
1 parent 9d6df68 commit 62e07d8

3 files changed

Lines changed: 630 additions & 0 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Integrations package
2+
from .webhooks import WebhookService, WebhookEvent, WebhookEndpoint
3+
from .notifications import SlackNotifier, PagerDutyNotifier, DiscordNotifier
4+
5+
__all__ = [
6+
'WebhookService', 'WebhookEvent', 'WebhookEndpoint',
7+
'SlackNotifier', 'PagerDutyNotifier', 'DiscordNotifier'
8+
]
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
"""
2+
Slack Integration for Security Notifications
3+
4+
Sends alerts and scan results to Slack channels.
5+
"""
6+
7+
import logging
8+
import os
9+
from typing import Dict, List, Optional
10+
import aiohttp
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
class SlackNotifier:
16+
"""
17+
Slack notification service
18+
19+
Sends formatted messages to Slack channels via webhooks.
20+
"""
21+
22+
def __init__(self, webhook_url: Optional[str] = None):
23+
"""
24+
Initialize Slack notifier
25+
26+
Args:
27+
webhook_url: Slack webhook URL (or from SLACK_WEBHOOK_URL env)
28+
"""
29+
self.webhook_url = webhook_url or os.getenv("SLACK_WEBHOOK_URL", "")
30+
31+
if not self.webhook_url:
32+
logger.warning("Slack webhook URL not configured")
33+
34+
async def send_message(
35+
self,
36+
text: str,
37+
blocks: Optional[List[Dict]] = None,
38+
channel: Optional[str] = None
39+
):
40+
"""
41+
Send message to Slack
42+
43+
Args:
44+
text: Message text (fallback)
45+
blocks: Slack Block Kit blocks
46+
channel: Override default channel
47+
"""
48+
if not self.webhook_url:
49+
logger.error("Cannot send Slack message: webhook URL not configured")
50+
return
51+
52+
payload = {"text": text}
53+
54+
if blocks:
55+
payload["blocks"] = blocks
56+
57+
if channel:
58+
payload["channel"] = channel
59+
60+
try:
61+
async with aiohttp.ClientSession() as session:
62+
async with session.post(
63+
self.webhook_url,
64+
json=payload,
65+
timeout=aiohttp.ClientTimeout(total=10)
66+
) as response:
67+
if response.status == 200:
68+
logger.info("Slack message sent successfully")
69+
else:
70+
logger.error(f"Slack API error: {response.status}")
71+
72+
except Exception as e:
73+
logger.error(f"Failed to send Slack message: {e}")
74+
75+
async def notify_scan_complete(self, scan_data: Dict):
76+
"""Send scan completion notification"""
77+
target = scan_data.get("target", "Unknown")
78+
findings_count = scan_data.get("findings_count", 0)
79+
critical_count = scan_data.get("critical_count", 0)
80+
81+
# Determine color based on findings
82+
if critical_count > 0:
83+
color = "#d32f2f" # Red
84+
emoji = ":rotating_light:"
85+
elif findings_count > 0:
86+
color = "#ffa726" # Orange
87+
emoji = ":warning:"
88+
else:
89+
color = "#4caf50" # Green
90+
emoji = ":white_check_mark:"
91+
92+
blocks = [
93+
{
94+
"type": "header",
95+
"text": {
96+
"type": "plain_text",
97+
"text": f"{emoji} Scan Complete: {target}"
98+
}
99+
},
100+
{
101+
"type": "section",
102+
"fields": [
103+
{
104+
"type": "mrkdwn",
105+
"text": f"*Total Findings:*\n{findings_count}"
106+
},
107+
{
108+
"type": "mrkdwn",
109+
"text": f"*Critical:*\n{critical_count}"
110+
}
111+
]
112+
}
113+
]
114+
115+
if scan_data.get("report_url"):
116+
blocks.append({
117+
"type": "actions",
118+
"elements": [
119+
{
120+
"type": "button",
121+
"text": {
122+
"type": "plain_text",
123+
"text": "View Report"
124+
},
125+
"url": scan_data["report_url"],
126+
"style": "primary" if critical_count > 0 else "default"
127+
}
128+
]
129+
})
130+
131+
await self.send_message(
132+
text=f"Scan complete for {target}: {findings_count} findings ({critical_count} critical)",
133+
blocks=blocks
134+
)
135+
136+
async def notify_critical_finding(self, finding: Dict):
137+
"""Send critical finding alert"""
138+
title = finding.get("title", "Unknown Vulnerability")
139+
severity = finding.get("severity", "unknown").upper()
140+
cvss_score = finding.get("cvss_score", 0.0)
141+
url = finding.get("url", "N/A")
142+
143+
blocks = [
144+
{
145+
"type": "header",
146+
"text": {
147+
"type": "plain_text",
148+
"text": f":rotating_light: CRITICAL: {title}"
149+
}
150+
},
151+
{
152+
"type": "section",
153+
"fields": [
154+
{
155+
"type": "mrkdwn",
156+
"text": f"*Severity:*\n{severity}"
157+
},
158+
{
159+
"type": "mrkdwn",
160+
"text": f"*CVSS Score:*\n{cvss_score}"
161+
},
162+
{
163+
"type": "mrkdwn",
164+
"text": f"*URL:*\n{url}"
165+
}
166+
]
167+
}
168+
]
169+
170+
await self.send_message(
171+
text=f"CRITICAL: {title} (CVSS {cvss_score})",
172+
blocks=blocks
173+
)
174+
175+
176+
class PagerDutyNotifier:
177+
"""
178+
PagerDuty integration for critical alerts
179+
180+
Creates incidents for critical findings.
181+
"""
182+
183+
def __init__(self, integration_key: Optional[str] = None):
184+
"""
185+
Initialize PagerDuty notifier
186+
187+
Args:
188+
integration_key: PagerDuty integration key
189+
"""
190+
self.integration_key = integration_key or os.getenv("PAGERDUTY_INTEGRATION_KEY", "")
191+
self.api_url = "https://events.pagerduty.com/v2/enqueue"
192+
193+
async def trigger_incident(
194+
self,
195+
summary: str,
196+
severity: str,
197+
details: Dict
198+
):
199+
"""
200+
Trigger PagerDuty incident
201+
202+
Args:
203+
summary: Brief summary
204+
severity: critical, error, warning, info
205+
details: Additional details
206+
"""
207+
if not self.integration_key:
208+
logger.error("PagerDuty integration key not configured")
209+
return
210+
211+
payload = {
212+
"routing_key": self.integration_key,
213+
"event_action": "trigger",
214+
"payload": {
215+
"summary": summary,
216+
"severity": severity,
217+
"source": "CyperSecurity Platform",
218+
"custom_details": details
219+
}
220+
}
221+
222+
try:
223+
async with aiohttp.ClientSession() as session:
224+
async with session.post(
225+
self.api_url,
226+
json=payload,
227+
timeout=aiohttp.ClientTimeout(total=10)
228+
) as response:
229+
if response.status == 202:
230+
logger.info("PagerDuty incident triggered")
231+
else:
232+
logger.error(f"PagerDuty API error: {response.status}")
233+
234+
except Exception as e:
235+
logger.error(f"Failed to trigger PagerDuty incident: {e}")
236+
237+
async def notify_critical_finding(self, finding: Dict):
238+
"""Create PagerDuty incident for critical finding"""
239+
await self.trigger_incident(
240+
summary=f"Critical security finding: {finding.get('title', 'Unknown')}",
241+
severity="critical",
242+
details={
243+
"vulnerability": finding.get("title"),
244+
"cvss_score": finding.get("cvss_score"),
245+
"url": finding.get("url"),
246+
"description": finding.get("description")
247+
}
248+
)
249+
250+
251+
class DiscordNotifier:
252+
"""Discord webhook integration"""
253+
254+
def __init__(self, webhook_url: Optional[str] = None):
255+
"""Initialize Discord notifier"""
256+
self.webhook_url = webhook_url or os.getenv("DISCORD_WEBHOOK_URL", "")
257+
258+
async def send_embed(
259+
self,
260+
title: str,
261+
description: str,
262+
color: int,
263+
fields: Optional[List[Dict]] = None
264+
):
265+
"""Send Discord embed"""
266+
if not self.webhook_url:
267+
logger.error("Discord webhook URL not configured")
268+
return
269+
270+
embed = {
271+
"title": title,
272+
"description": description,
273+
"color": color,
274+
"timestamp": None # Would use datetime.utcnow().isoformat()
275+
}
276+
277+
if fields:
278+
embed["fields"] = fields
279+
280+
payload = {
281+
"embeds": [embed]
282+
}
283+
284+
try:
285+
async with aiohttp.ClientSession() as session:
286+
async with session.post(
287+
self.webhook_url,
288+
json=payload,
289+
timeout=aiohttp.ClientTimeout(total=10)
290+
) as response:
291+
if response.status in [200, 204]:
292+
logger.info("Discord message sent")
293+
else:
294+
logger.error(f"Discord API error: {response.status}")
295+
296+
except Exception as e:
297+
logger.error(f"Failed to send Discord message: {e}")
298+
299+
async def notify_scan_complete(self, scan_data: Dict):
300+
"""Send scan completion to Discord"""
301+
target = scan_data.get("target", "Unknown")
302+
findings_count = scan_data.get("findings_count", 0)
303+
critical_count = scan_data.get("critical_count", 0)
304+
305+
# Color based on severity
306+
if critical_count > 0:
307+
color = 0xFF0000 # Red
308+
elif findings_count > 0:
309+
color = 0xFFA500 # Orange
310+
else:
311+
color = 0x00FF00 # Green
312+
313+
fields = [
314+
{"name": "Target", "value": target, "inline": True},
315+
{"name": "Findings", "value": str(findings_count), "inline": True},
316+
{"name": "Critical", "value": str(critical_count), "inline": True}
317+
]
318+
319+
await self.send_embed(
320+
title="🔍 Scan Complete",
321+
description=f"Security scan finished for **{target}**",
322+
color=color,
323+
fields=fields
324+
)

0 commit comments

Comments
 (0)