diff --git a/README.md b/README.md index 4d76412..e5c175d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/requirements.txt b/requirements.txt index 0ea1763..631251a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ jira pyyaml atlassian-python-api asana==3.2.2 +zenpy WrikePy BeautifulSoup4 slack_sdk \ No newline at end of file diff --git a/setup.py b/setup.py index fd1b735..144048a 100644 --- a/setup.py +++ b/setup.py @@ -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", @@ -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", diff --git a/src/n0s1/__init__.py b/src/n0s1/__init__.py index 89e4f67..0018828 100644 --- a/src/n0s1/__init__.py +++ b/src/n0s1/__init__.py @@ -1 +1 @@ -__version__ = "1.0.23" +__version__ = "1.0.24" diff --git a/src/n0s1/controllers/platform_controller.py b/src/n0s1/controllers/platform_controller.py index 609acc5..c02a939 100644 --- a/src/n0s1/controllers/platform_controller.py +++ b/src/n0s1/controllers/platform_controller.py @@ -25,6 +25,7 @@ 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: @@ -32,6 +33,7 @@ def get_platform(self, platform): 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 @@ -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) diff --git a/src/n0s1/controllers/zendesk_controller.py b/src/n0s1/controllers/zendesk_controller.py new file mode 100644 index 0000000..a6aec12 --- /dev/null +++ b/src/n0s1/controllers/zendesk_controller.py @@ -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: + server = self._config.get("server", "") + # 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 + url = ticket.url + if len(server): + url = f"https://{server}.zendesk.com/agent/tickets/{ticket_id}" + 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) diff --git a/src/n0s1/n0s1.py b/src/n0s1/n0s1.py index 0114241..9b145bd 100755 --- a/src/n0s1/n0s1.py +++ b/src/n0s1/n0s1.py @@ -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. """, ) @@ -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] ) @@ -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: