-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #23 from spark1security/ms/slack-support
Added support to scan secret leaks in Slack messages
- Loading branch information
Showing
11 changed files
with
236 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,5 @@ pyyaml | |
atlassian-python-api | ||
asana==3.2.2 | ||
WrikePy | ||
BeautifulSoup4 | ||
BeautifulSoup4 | ||
slack_sdk |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
__version__ = "1.0.19" | ||
__version__ = "1.0.20" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import datetime | ||
import logging | ||
import re | ||
import time | ||
|
||
try: | ||
from . import hollow_controller as hollow_controller | ||
except Exception: | ||
import n0s1.controllers.hollow_controller as hollow_controller | ||
|
||
|
||
class SlackController(hollow_controller.HollowController): | ||
def __init__(self): | ||
super().__init__() | ||
self._client = None | ||
|
||
def set_config(self, config): | ||
from slack_sdk import WebClient | ||
from slack_sdk.errors import SlackApiError | ||
TOKEN = config.get("token", "") | ||
self._client = WebClient(token=TOKEN) | ||
return self.is_connected() | ||
|
||
def get_name(self): | ||
return "Slack" | ||
|
||
def is_connected(self): | ||
if user := self._client.auth_test(): | ||
self.log_message(f"Logged to Slack as {user}") | ||
else: | ||
self.log_message(f"Unable to connect to Slack. Check your credentials.", logging.ERROR) | ||
return False | ||
return True | ||
|
||
def get_data(self, include_coments=False, limit=None): | ||
max_day_range = 365 * 100 | ||
range_days = 1 | ||
now = datetime.datetime.now() | ||
|
||
# Slack query by timestamp works like "greater than >" and "less than <" operators as opposed to ">=" and "<=". | ||
# If you want to pull messages from 2024-07-14 you have to provide the following query: after:2024-07-13 before:2024-07-15 | ||
# Notice that the messages from the starting date (after:2024-07-13) and the end date (before:2024-07-15) are not included to the query results | ||
end_day = now + datetime.timedelta(days=1) | ||
start_day = now - datetime.timedelta(days=range_days) | ||
|
||
start_day_str = start_day.strftime("%Y-%m-%d") | ||
end_day_str = end_day.strftime("%Y-%m-%d") | ||
|
||
query = f"after:{start_day_str} before:{end_day_str}" | ||
days_counter = 0 | ||
while days_counter < max_day_range: | ||
messages = self.run_slack_query(query) | ||
for m in messages: | ||
len_messages = len(m) | ||
if len_messages <= 0: | ||
range_days = range_days * 2 | ||
else: | ||
range_days = 1 | ||
for item in m: | ||
message = item.get("text", "") | ||
iid = item.get("iid", "") | ||
url = item.get("permalink", "") | ||
ticket = self.pack_data(message, item, url, iid) | ||
yield ticket | ||
|
||
end_day = start_day + datetime.timedelta(days=1) | ||
start_day = start_day - datetime.timedelta(days=range_days) | ||
start_day_str = start_day.strftime("%Y-%m-%d") | ||
end_day_str = end_day.strftime("%Y-%m-%d") | ||
query = f"after:{start_day_str} before:{end_day_str}" | ||
days_counter += range_days | ||
|
||
def post_comment(self, issue, comment): | ||
from slack_sdk.errors import SlackApiError | ||
try: | ||
channel_id, thread_ts = self.extract_channel_id_and_ts(issue) | ||
if comment and len(comment) > 0 and len(channel_id) > 0 and len(thread_ts) > 0: | ||
response = self._client.chat_postMessage( | ||
channel=channel_id, | ||
text=comment, | ||
thread_ts=thread_ts, | ||
unfurl_links=False | ||
) | ||
|
||
self.log_message(f"Message sent successfully") | ||
response_ts = response.get("ts", "") | ||
self.log_message(f"Thread Timestamp: {response_ts}") | ||
|
||
except SlackApiError as e: | ||
error_message = e.response.get("error", "") | ||
self.log_message(f"Error sending message: {error_message}", logging.ERROR) | ||
|
||
def pack_data(self, message, raw_data, url, iid): | ||
channel_id = raw_data.get("channel", {}).get("id", "") | ||
channel_name = raw_data.get("channel", {}).get("name", "") | ||
is_channel = raw_data.get("channel", {}).get("is_channel", "") | ||
timestamp = raw_data.get("ts", "") | ||
slack_type = raw_data.get("type", "") | ||
ticket_data = { | ||
"ticket": { | ||
"message": { | ||
"name": "message", | ||
"data": message, | ||
"data_type": "str" | ||
}, | ||
}, | ||
"url": url, | ||
"issue_id": url, | ||
"raw_data": { | ||
"iid": iid, | ||
"channel_name": channel_name, | ||
"channel_id": channel_id, | ||
"is_channel": is_channel, | ||
"timestamp": timestamp, | ||
"slack_type": slack_type | ||
} | ||
} | ||
return ticket_data | ||
|
||
def search_with_rate_limit(self, query, sort, cursor): | ||
from slack_sdk.errors import SlackApiError | ||
response = None | ||
try: | ||
response = self._client.search_messages(query=query, sort=sort, cursor=cursor) | ||
except SlackApiError as ex: | ||
message = str(ex) + f" client.search_messages()" | ||
self.log_message(message, logging.WARNING) | ||
retry_after = ex.response.headers.get("Retry-After", "") | ||
if len(retry_after) <= 0: | ||
retry_after = ex.response.headers.get("retry-after", "") | ||
if len(retry_after) > 0: | ||
retry_after = int(retry_after) | ||
else: | ||
retry_after = 30 | ||
retry_after += 5 | ||
self.log_message(f"Rate limit reached! Retrying after [{retry_after}] seconds...", logging.WARNING) | ||
time.sleep(retry_after) | ||
response = self.search_with_rate_limit(query, sort, cursor) | ||
|
||
except Exception as ex: | ||
message = str(ex) + f" client.search_messages()" | ||
self.log_message(message, logging.ERROR) | ||
|
||
return response | ||
|
||
def run_slack_query(self, query): | ||
cursor = "" | ||
self.log_message(f"Scanning Slack messages: [{query}]...") | ||
time.sleep(0.2) | ||
if response := self.search_with_rate_limit(query=query, sort="timestamp", cursor="*"): | ||
messages = response.get("messages", {}).get("matches", []) | ||
cursor = response.get("messages", {}).get("pagination", {}).get("next_cursor", "") | ||
yield messages | ||
|
||
while len(cursor) > 0: | ||
cursor = "" | ||
time.sleep(0.2) | ||
if response := self.search_with_rate_limit(query=query, sort="timestamp", cursor=cursor): | ||
messages = response.get("messages", {}).get("matches", []) | ||
cursor = response.get("messages", {}).get("pagination", {}).get("next_cursor", "") | ||
yield messages | ||
|
||
def extract_channel_id_and_ts(self, link): | ||
# Extract the channel ID and message timestamp from the link | ||
match = re.search(r'archives/([^/]+)/p(\d+)', link) | ||
if match: | ||
channel_id = match.group(1) | ||
message_ts = f"{match.group(2)[:10]}.{match.group(2)[10:]}" | ||
return channel_id, message_ts | ||
else: | ||
self.log_message("Invalid Slack link format", logging.ERROR) | ||
|
||
return "", "" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.