Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support to Zendesk #28

Merged
merged 9 commits into from
Oct 12, 2024
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 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.
n0s1 ([pronunciation](https://en.wiktionary.org/wiki/nosy#Pronunciation)) is a secret scanner for Slack, Jira, Confluence, Asana, Wrike, Linear and Zendesk. 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 @@ -29,6 +29,7 @@ These secrets are identified by comparing them against an adaptable configuratio
* [Asana](https://asana.com)
* [Wrike](https://www.wrike.com)
* [Linear](https://linear.app/)
* [Zendesk](https://www.zendesk.com/)

### Install
```bash
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ jira
pyyaml
atlassian-python-api
asana==3.2.2
zenpy
WrikePy
BeautifulSoup4
slack_sdk
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def get_version():
setup(
name="n0s1",
version=get_version(),
description="Secret Scanner for Slack, Jira, Confluence, Asana, Wrike and Linear. Prevent credential leaks with n0s1.",
description="Secret Scanner for Slack, Jira, Confluence, Asana, Wrike, Linear and Zendesk. Prevent credential leaks with n0s1.",
long_description=long_description,
long_description_content_type="text/markdown",
url="https://spark1.us/n0s1",
Expand All @@ -46,8 +46,9 @@ def get_version():
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
], # Classifiers help users find your project by categorizing it https://pypi.org/classifiers/
keywords="security, cybersecurity, scanner, secret scanner, secret leak, data leak, Slack, Jira, Confluence, Asana, Wrike, Linear, security scanner, data loss prevention",
keywords="security, cybersecurity, scanner, secret scanner, secret leak, data leak, Slack, Jira, Confluence, Asana, Wrike, Linear, Zendesk, security scanner, data loss prevention",
package_dir={"": "src"},
packages=find_packages(where="src"),
python_requires=">=3.9, <4",
Expand Down
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.23"
__version__ = "1.0.24"
4 changes: 4 additions & 0 deletions src/n0s1/controllers/platform_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,15 @@ def get_platform(self, platform):
from . import confluence_controller as confluence_controller
from . import linear_controller as linear_controller
from . import asana_controller as asana_controller
from . import zendesk_controller as zendesk_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.zendesk_controller as zendesk_controller
import n0s1.controllers.wrike_controller as wrike_controller
import n0s1.controllers.slack_controller as slack_controller

Expand All @@ -44,6 +46,8 @@ def get_platform(self, platform):
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("zendesk", zendesk_controller.ZendeskController)
factory.register_platform("zendesk_scan", zendesk_controller.ZendeskController)
factory.register_platform("wrike", wrike_controller.WrikeController)
factory.register_platform("wrike_scan", wrike_controller.WrikeController)
factory.register_platform("slack", slack_controller.SlackController)
Expand Down
76 changes: 76 additions & 0 deletions src/n0s1/controllers/zendesk_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import logging


try:
from . import hollow_controller as hollow_controller
except Exception:
import n0s1.controllers.hollow_controller as hollow_controller


class ZendeskController(hollow_controller.HollowController):
def __init__(self):
super().__init__()
self._client = None

def set_config(self, config):
super().set_config(config)
from zenpy import Zenpy
SERVER = self._config.get("server", "")
EMAIL = self._config.get("email", "")
TOKEN = self._config.get("token", "")
creds = {
"email": EMAIL,
"token": TOKEN,
"subdomain": SERVER
}
self._client = Zenpy(**creds)
return self.is_connected()

def get_name(self):
return "Zendesk"

def is_connected(self):
if self._client:
if user := self._client.users.me():
self.log_message(f"Logged to {self.get_name()} as {user} - {user.email}")
return True
else:
self.log_message(f"Unable to connect to {self.get_name()}. Check your credentials.", logging.ERROR)
return False
return False

def get_data(self, include_comments=False, limit=None):
if not self._client:
return {}

try:
# Fetch all tickets (paginated)
tickets = self._client.tickets()
for ticket in tickets:
self.log_message(f"Scanning Zendesk Ticket ID: {ticket.id}, Subject: {ticket.subject}, Status: {ticket.status}, Created: {ticket.created_at}")
comments = []
title = ticket.subject
ticket_id = ticket.id
description = ticket.description
tmp = ticket.url
tmp = tmp.replace(f"/{ticket.api.api_prefix}/", "/agent/")
blupants marked this conversation as resolved.
Show resolved Hide resolved
tmp = tmp.replace(f"/{ticket_id}.json", f"/{ticket_id}")
url = tmp
if include_comments:
if cs := self._client.tickets.comments(ticket_id):
for c in cs:
c_body = c.body
comments.append(c_body)
ticket = self.pack_data(title, description, comments, url, ticket_id)
yield ticket
except Exception as e:
message = str(e) + f" client.get_data()"
self.log_message(message, logging.WARNING)

def post_comment(self, issue, comment):
if not self._client:
return False
from zenpy.lib.api_objects import Ticket, Comment
c = Comment(body=comment, public=True)
ticket = Ticket(id=issue, comment=c)
return self._client.tickets.update(ticket)
41 changes: 40 additions & 1 deletion src/n0s1/n0s1.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ def init_argparse() -> argparse.ArgumentParser:
install_path = os.path.dirname(os.path.abspath(__file__))
parser = argparse.ArgumentParser(
prog="n0s1",
description="""Secret scanner for Slack, Jira, Confluence, Asana, Wrike and Linear.
description="""Secret scanner for Slack, Jira, Confluence, Asana, Wrike, Zendesk and Linear.
""",
)

Expand Down Expand Up @@ -207,6 +207,31 @@ def init_argparse() -> argparse.ArgumentParser:
help="Asana API key. Ref: https://developers.asana.com/docs/personal-access-token#generating-a-pat"
)

zendesk_scan_parser = subparsers.add_parser(
"zendesk_scan", help="Scan Zendesk tickets", parents=[parent_parser]
)
zendesk_scan_parser.add_argument(
"--server",
dest="server",
nargs="?",
type=str,
help="Zendesk server subdomain."
)
zendesk_scan_parser.add_argument(
"--email",
dest="email",
nargs="?",
type=str,
help="Zendesk user email."
)
zendesk_scan_parser.add_argument(
"--api-key",
dest="api_key",
nargs="?",
type=str,
help="Zendesk API key. Ref: https://developer.zendesk.com/api-reference/integration-services/connections/api_key_connections"
)

wrike_scan_parser = subparsers.add_parser(
"wrike_scan", help="Scan Wrike tasks", parents=[parent_parser]
)
Expand Down Expand Up @@ -558,6 +583,20 @@ def main(callback=None):
TOKEN = args.api_key
controller_config["token"] = TOKEN

elif command == "zendesk_scan":
SERVER = os.getenv("ZENDESK_SERVER")
EMAIL = os.getenv("ZENDESK_EMAIL")
TOKEN = os.getenv("ZENDESK_TOKEN")
if args.server and len(args.server) > 0:
SERVER = args.server
if args.email and len(args.email) > 0:
EMAIL = args.email
if args.api_key and len(args.api_key) > 0:
TOKEN = args.api_key
controller_config["server"] = SERVER
controller_config["email"] = EMAIL
controller_config["token"] = TOKEN

elif command == "wrike_scan":
TOKEN = os.getenv("WRIKE_TOKEN")
if args.api_key and len(args.api_key) > 0:
Expand Down