Skip to content

Commit b707de0

Browse files
Merge pull request #1369 from TheHive-Project/slackintegration-1
Slack integration - Create Channel / War room
2 parents 5f9d2c8 + fba37c1 commit b707de0

3 files changed

Lines changed: 243 additions & 0 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
{
2+
"name": "Slack_CreateChannel",
3+
"version": "1.0",
4+
"author": "Fabien Bloume, StrangeBee",
5+
"url": "https://github.com/TheHive-Project/Cortex-Analyzers",
6+
"license": "AGPL-V3",
7+
"description": "Creates a Slack channel for a TheHive case, invites participants, and optionally posts a summary.",
8+
"dataTypeList": ["thehive:case"],
9+
"command": "Slack/slack.py",
10+
"baseConfig": "Slack",
11+
"config": {
12+
"service": "createchannel"
13+
},
14+
"configurationItems": [
15+
{
16+
"name": "slack_token",
17+
"description": "Slack Bot Token used for API authentication.",
18+
"type": "string",
19+
"multi": false,
20+
"required": true
21+
},
22+
{
23+
"name": "participants",
24+
"description": "List of email addresses to invite as default participants.",
25+
"type": "string",
26+
"multi": true,
27+
"required": true
28+
},
29+
{
30+
"name": "channel_prefix",
31+
"description": "Prefix to use for the Slack channel name. By default, 'case-'",
32+
"type": "string",
33+
"multi": false,
34+
"required": false,
35+
"defaultValue": "case-"
36+
},
37+
{
38+
"name": "visibility",
39+
"description": "Visibility of the channel: 'private' (default) or 'public'.",
40+
"type": "string",
41+
"multi": false,
42+
"required": false,
43+
"defaultValue": "private"
44+
},
45+
{
46+
"name": "thehive_base_url",
47+
"description": "Base URL of TheHive instance to include a case link.",
48+
"type": "string",
49+
"multi": false,
50+
"required": false
51+
},
52+
{
53+
"name": "post_summary",
54+
"description": "Include a case summary in the initial Slack message.",
55+
"type": "boolean",
56+
"multi": false,
57+
"required": true,
58+
"defaultValue": true
59+
},
60+
{
61+
"name": "post_description",
62+
"description": "Include the case description in the initial Slack summary message.",
63+
"type": "boolean",
64+
"multi": false,
65+
"required": false,
66+
"defaultValue": false
67+
}
68+
],
69+
"registration_required": true,
70+
"subscription_required": false,
71+
"free_subscription": true,
72+
"service_homepage": "https://www.slack.com"
73+
}

responders/Slack/requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
cortexutils
2+
requests
3+
thehive4py>=2.0.0b

responders/Slack/slack.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env python3
2+
# encoding: utf-8
3+
4+
import json
5+
import requests
6+
from cortexutils.responder import Responder
7+
8+
class Slack(Responder):
9+
10+
def __init__(self):
11+
Responder.__init__(self)
12+
self.slack_token = self.get_param(
13+
"config.slack_token", None, "Missing Slack bot token")
14+
self.participants = self.get_param(
15+
"config.participants", [], "Missing Slack participant emails (list)")
16+
self.channel_prefix = self.get_param(
17+
"config.channel_prefix", "case-", "Missing channel prefix")
18+
self.visibility = self.get_param(
19+
"config.visibility", "private") # 'private' or 'public'
20+
self.thehive_base_url = self.get_param("config.thehive_base_url", None)
21+
self.post_summary = self.get_param("config.post_summary", True)
22+
self.post_description = self.get_param("config.post_description", False)
23+
24+
self.service = self.get_param("config.service")
25+
26+
def find_existing_channel(self, name, headers):
27+
url = "https://slack.com/api/conversations.list"
28+
cursor = None
29+
while True:
30+
params = {
31+
"limit": 200,
32+
"exclude_archived": True,
33+
"types": "public_channel,private_channel"
34+
}
35+
if cursor:
36+
params["cursor"] = cursor
37+
resp = requests.get(url, headers=headers, params=params).json()
38+
for ch in resp.get("channels", []):
39+
if ch["name"] == name:
40+
return ch["id"]
41+
cursor = resp.get("response_metadata", {}).get("next_cursor")
42+
if not cursor:
43+
break
44+
return None
45+
46+
def run(self):
47+
Responder.run(self)
48+
if self.service == "createchannel":
49+
# Gather data from TheHive case
50+
case_id = self.get_param("data.caseId")
51+
case_unique_id = self.get_param("data.id")
52+
title = self.get_param("data.title", "")
53+
owner = self.get_param("data.owner", "")
54+
description = self.get_param("data.description", "")
55+
56+
# Slack channel name must be lowercase, <=80 chars, no spaces, no periods, no commas
57+
channel_name = f"{self.channel_prefix}{case_id}".replace(" ", "-").replace(".", "").replace(",", "").lower()
58+
channel_name = channel_name[:80]
59+
60+
headers = {
61+
"Authorization": f"Bearer {self.slack_token}",
62+
"Content-Type": "application/json"
63+
}
64+
65+
# 1. validate participant emails BEFORE creating the channel
66+
user_ids = []
67+
for email in self.participants:
68+
lookup_url = "https://slack.com/api/users.lookupByEmail"
69+
params = {"email": email}
70+
lookup_resp = requests.get(lookup_url, headers=headers, params=params)
71+
user_data = lookup_resp.json()
72+
if user_data.get("ok"):
73+
user_ids.append(user_data["user"]["id"])
74+
else:
75+
print(f"[WARNING] Could not resolve Slack user for email {email}: {user_data.get('error')}")
76+
if not user_ids:
77+
self.error("No valid Slack users found from participant emails. Aborting channel creation.")
78+
79+
# 2. create channel only after we have valid users
80+
channel_id = self.find_existing_channel(channel_name, headers)
81+
if channel_id:
82+
print(f"Channel '{channel_name}' already exists. Reusing.")
83+
else:
84+
create_url = "https://slack.com/api/conversations.create"
85+
payload = {
86+
"name": channel_name,
87+
"is_private": self.visibility == "private"
88+
}
89+
create_resp = requests.post(create_url, headers=headers, json=payload)
90+
create_data = create_resp.json()
91+
if not create_data.get("ok"):
92+
self.error(f"Slack channel creation failed: {create_data.get('error')}")
93+
channel_id = create_data["channel"]["id"]
94+
95+
96+
# 3. invite users
97+
if user_ids:
98+
invite_url = "https://slack.com/api/conversations.invite"
99+
invite_payload = {
100+
"channel": channel_id,
101+
"users": ",".join(user_ids),
102+
"force": True
103+
104+
}
105+
invite_resp = requests.post(invite_url, headers=headers, json=invite_payload)
106+
invite_data = invite_resp.json()
107+
if not invite_data.get("ok"):
108+
self.error(f"Error inviting users: {invite_data.get('error')}")
109+
110+
# 4. post a summary message
111+
if self.post_summary:
112+
post_url = "https://slack.com/api/chat.postMessage"
113+
114+
def format_severity(score):
115+
return {1: "LOW", 2: "MEDIUM", 3: "HIGH", 4: "CRITICAL"}.get(score, "UNKNOWN")
116+
117+
def format_tlp(level):
118+
return {
119+
-1: "UNKNOWN",
120+
0: "WHITE",
121+
1: "GREEN",
122+
2: "AMBER",
123+
3: "RED"
124+
}.get(level, "UNKNOWN")
125+
126+
severity = format_severity(self.get_param("data.severity", 2))
127+
tlp = format_tlp(self.get_param("data.tlp", 2))
128+
status = self.get_param("data.extendedStatus", "New")
129+
130+
case_link_text = ""
131+
if self.thehive_base_url:
132+
case_web_link = f"{self.thehive_base_url}/cases/{case_unique_id}/details"
133+
case_link_text = f"\n🔗 <{case_web_link}|Open Case in TheHive>"
134+
135+
summary_lines = [
136+
"*🚨 New Slack Channel created from TheHive*",
137+
f"*Case ID:* {case_id} — *{title}*",
138+
f"*Owner:* {owner}",
139+
f"*Severity:* {severity} | *TLP:* {tlp} | *Status:* {status}",
140+
]
141+
142+
if self.post_description and description.strip():
143+
summary_lines.append(f"*Description:*\n{description}")
144+
145+
if case_link_text:
146+
summary_lines.append(case_link_text)
147+
148+
summary = "\n".join(summary_lines)
149+
150+
post_payload = {
151+
"channel": channel_id,
152+
"text": summary
153+
}
154+
post_resp = requests.post(post_url, headers=headers, json=post_payload)
155+
post_data = post_resp.json()
156+
if not post_data.get("ok"):
157+
self.error(f"[ERROR] Failed to post summary message: {post_data.get('error')}")
158+
159+
self.report({
160+
"channel_name": channel_name,
161+
"channel_id": channel_id,
162+
"invited_users": user_ids,
163+
"message": f"Slack channel `{channel_name}` created and users invited."
164+
})
165+
166+
if __name__ == "__main__":
167+
Slack().run()

0 commit comments

Comments
 (0)