Skip to content

Commit

Permalink
Merge pull request #23 from spark1security/ms/slack-support
Browse files Browse the repository at this point in the history
Added support to scan secret leaks in Slack messages
  • Loading branch information
blupants authored Jul 17, 2024
2 parents 9ec710d + cf06540 commit 081ed19
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 37 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@


# n0s1 - Secret Scanner
n0s1 ([pronunciation](https://en.wiktionary.org/wiki/nosy#Pronunciation)) is a secret scanner for Jira, Confluence, Asana, Wrike and Linear.app. It scans all tickets/items/issues within the chosen platform in search of any leaked secrets in the titles, bodies, and comments. It is open-source and it can be easily extended to support scanning many others Project Management and Issue Tracker platforms.
n0s1 ([pronunciation](https://en.wiktionary.org/wiki/nosy#Pronunciation)) is a secret scanner for Slack, Jira, Confluence, Asana, Wrike and Linear. It scans all channels/tickets/items/issues within the chosen platform in search of any leaked secrets in the titles, bodies, messages and comments. It is open-source and it can be easily extended to support scanning many others ticketing and messaging platforms.

These secrets are identified by comparing them against an adaptable configuration file named [regex.yaml](https://github.com/spark1security/n0s1/blob/main/src/n0s1/config/regex.yaml). Alternative TOML format is also supported: [regex.toml](https://github.com/spark1security/n0s1/blob/main/src/n0s1/config/regex.toml). The scanner specifically looks for sensitive information, which includes:
* Github Personal Access Tokens
Expand All @@ -23,6 +23,7 @@ These secrets are identified by comparing them against an adaptable configuratio
* npm access tokens

### Currently supported target platforms:
* [Slack](https://slack.com)
* [Jira](https://www.atlassian.com/software/jira)
* [Confluence](https://www.atlassian.com/software/confluence)
* [Asana](https://asana.com)
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pyyaml
atlassian-python-api
asana==3.2.2
WrikePy
BeautifulSoup4
BeautifulSoup4
slack_sdk
2 changes: 1 addition & 1 deletion src/n0s1/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "1.0.19"
__version__ = "1.0.20"
2 changes: 1 addition & 1 deletion src/n0s1/controllers/asana_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import n0s1.controllers.hollow_controller as hollow_controller


class AsanaControler(hollow_controller.HollowController):
class AsanaController(hollow_controller.HollowController):
def __init__(self):
super().__init__()
self._client = None
Expand Down
2 changes: 1 addition & 1 deletion src/n0s1/controllers/confluence_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import n0s1.controllers.hollow_controller as hollow_controller


class ConfluenceControler(hollow_controller.HollowController):
class ConfluenceController(hollow_controller.HollowController):
def __init__(self):
super().__init__()
self._client = None
Expand Down
2 changes: 1 addition & 1 deletion src/n0s1/controllers/jira_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import n0s1.controllers.hollow_controller as hollow_controller


class JiraControler(hollow_controller.HollowController):
class JiraController(hollow_controller.HollowController):
def __init__(self):
super().__init__()
self._client = None
Expand Down
2 changes: 1 addition & 1 deletion src/n0s1/controllers/linear_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import n0s1.clients.linear_graphql_client as linear_graphql_client


class LinearControler(hollow_controller.HollowController):
class LinearController(hollow_controller.HollowController):
def __init__(self):
super().__init__()
self._client = None
Expand Down
28 changes: 16 additions & 12 deletions src/n0s1/controllers/platform_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,21 +26,25 @@ def get_platform(self, platform):
from . import linear_controller as linear_controller
from . import asana_controller as asana_controller
from . import wrike_controller as wrike_controller
from . import slack_controller as slack_controller
except Exception:
import n0s1.controllers.jira_controller as jira_controller
import n0s1.controllers.confluence_controller as confluence_controller
import n0s1.controllers.linear_controller as linear_controller
import n0s1.controllers.asana_controller as asana_controller
import n0s1.controllers.wrike_controller as wrike_controller

factory.register_platform("", jira_controller.JiraControler)
factory.register_platform("jira", jira_controller.JiraControler)
factory.register_platform("jira_scan", jira_controller.JiraControler)
factory.register_platform("confluence", confluence_controller.ConfluenceControler)
factory.register_platform("confluence_scan", confluence_controller.ConfluenceControler)
factory.register_platform("linear", linear_controller.LinearControler)
factory.register_platform("linear_scan", linear_controller.LinearControler)
factory.register_platform("asana", asana_controller.AsanaControler)
factory.register_platform("asana_scan", asana_controller.AsanaControler)
factory.register_platform("wrike", wrike_controller.WrikeControler)
factory.register_platform("wrike_scan", wrike_controller.WrikeControler)
import n0s1.controllers.slack_controller as slack_controller

factory.register_platform("", jira_controller.JiraController)
factory.register_platform("jira", jira_controller.JiraController)
factory.register_platform("jira_scan", jira_controller.JiraController)
factory.register_platform("confluence", confluence_controller.ConfluenceController)
factory.register_platform("confluence_scan", confluence_controller.ConfluenceController)
factory.register_platform("linear", linear_controller.LinearController)
factory.register_platform("linear_scan", linear_controller.LinearController)
factory.register_platform("asana", asana_controller.AsanaController)
factory.register_platform("asana_scan", asana_controller.AsanaController)
factory.register_platform("wrike", wrike_controller.WrikeController)
factory.register_platform("wrike_scan", wrike_controller.WrikeController)
factory.register_platform("slack", slack_controller.SlackController)
factory.register_platform("slack_scan", slack_controller.SlackController)
173 changes: 173 additions & 0 deletions src/n0s1/controllers/slack_controller.py
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 "", ""
2 changes: 1 addition & 1 deletion src/n0s1/controllers/wrike_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
except Exception:
import n0s1.controllers.hollow_controller as hollow_controller

class WrikeControler(hollow_controller.HollowController):
class WrikeController(hollow_controller.HollowController):
def __init__(self):
super().__init__()
self._client = None
Expand Down
Loading

0 comments on commit 081ed19

Please sign in to comment.