diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 6f15b22..131c639 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -1,7 +1,7 @@ name: Linting on: [push, pull_request] jobs: - lint: + lint: # Run per push for internal contributers. This isn't possible for forked pull requests, # so we'll need to run on PR events for external contributers. # String comparison below is case insensitive. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e6c658..f8b1796 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,11 @@ repos: - repo: https://github.com/phantomcyber/dev-cicd-tools - rev: v1.13 + rev: v1.17 hooks: - id: org-hook - id: package-app-dependencies - repo: https://github.com/Yelp/detect-secrets - rev: v1.2.0 + rev: v1.4.0 hooks: - id: detect-secrets args: ['--no-verify'] diff --git a/LICENSE b/LICENSE index a74307f..c3eb7da 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2023 Splunk Inc. + Copyright (c) ZeroFox, 2024 Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 02fa60b..606f2d2 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,180 @@ -# Splunk> Phantom +[comment]: # "Auto-generated SOAR connector documentation" +# ZeroFox -Welcome to the open-source repository for Splunk> Phantom's zerofoxalerts App. +Publisher: ZeroFox +Connector Version: 1.0.1 +Product Vendor: ZeroFox +Product Name: ZeroFox +Product Version Supported (regex): ".\*" +Minimum Product Version: 5.5.0 -Please have a look at our [Contributing Guide](https://github.com/Splunk-SOAR-Apps/.github/blob/main/.github/CONTRIBUTING.md) if you are interested in contributing, raising issues, or learning more about open-source Phantom apps. +ZeroFox Alerts for Splunk SOAR -## Legal and License +[comment]: # File: manual_readme_content.md +[comment]: # +[comment]: # Copyright (c) ZeroFox, 2024 +[comment]: # +[comment]: # Licensed under the Apache License, Version 2.0 (the "License"); +[comment]: # you may not use this file except in compliance with the License. +[comment]: # You may obtain a copy of the License at +[comment]: # +[comment]: # http://www.apache.org/licenses/LICENSE-2.0 +[comment]: # +[comment]: # Unless required by applicable law or agreed to in writing, software distributed under +[comment]: # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +[comment]: # either express or implied. See the License for the specific language governing permissions +[comment]: # and limitations under the License. -This Phantom App is licensed under the Apache 2.0 license. Please see our [Contributing Guide](https://github.com/Splunk-SOAR-Apps/.github/blob/main/.github/CONTRIBUTING.md#legal-notice) for further details. +### Configuration Variables +The below configuration variables are required for this Connector to operate. These variables are specified when configuring a ZeroFox asset in SOAR. + +VARIABLE | REQUIRED | TYPE | DESCRIPTION +-------- | -------- | ---- | ----------- +**zerofox_api_token** | required | password | ZeroFox API Token +**username** | required | string | Your ZeroFOX platform username or email address +**reviewed** | optional | boolean | Only poll reviewed alerts +**history_days_interval** | required | string | Initial historical alert poll interval (in days) +**verify_server_cert** | optional | boolean | Verify Sever Certificate + +### Supported Actions +[test connectivity](#action-test-connectivity) - Validate the asset configuration for connectivity using supplied configuration +[on poll](#action-on-poll) - Callback action for the on_poll ingest functionality +[take action](#action-take-action) - Take action on a ZeroFox an alert +[tag alert](#action-tag-alert) - Add or remove a tag to a ZeroFox alert +[threat submission](#action-threat-submission) - Add a manual threat to ZeroFox +[lookup alert](#action-lookup-alert) - Retrieve a single alert and it's details, identified by its unique integer identifier + +## action: 'test connectivity' +Validate the asset configuration for connectivity using supplied configuration + +Type: **test** +Read only: **True** + +#### Action Parameters +No parameters are required for this action + +#### Action Output +No Output + +## action: 'on poll' +Callback action for the on_poll ingest functionality + +Type: **ingest** +Read only: **True** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**container_id** | optional | Container IDs to limit the ingestion to | string | +**start_time** | optional | Start of time range, in epoch time (milliseconds) | numeric | +**end_time** | optional | End of time range, in epoch time (milliseconds) | numeric | +**container_count** | optional | Maximum number of container records to query for | numeric | +**artifact_count** | optional | Maximum number of artifact records to query for | numeric | + +#### Action Output +No Output + +## action: 'take action' +Take action on a ZeroFox an alert + +Type: **generic** +Read only: **False** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**alert_id** | required | ZeroFox Alert ID | numeric | +**alert_action** | required | The action to take | string | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.alert_action | string | | +action_result.parameter.alert_id | numeric | | +action_result.data | string | | +action_result.summary | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | + +## action: 'tag alert' +Add or remove a tag to a ZeroFox alert + +Type: **generic** +Read only: **False** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**alert_id** | required | ZeroFox Alert ID | numeric | +**alert_tag** | required | Tag | string | +**tag_action** | required | Tag action: add or remove | string | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.alert_id | numeric | | +action_result.parameter.alert_tag | string | | +action_result.parameter.tag_action | string | | +action_result.data | string | | +action_result.summary | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | + +## action: 'threat submission' +Add a manual threat to ZeroFox + +Type: **generic** +Read only: **False** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**source** | required | Source URL | string | +**alert_type** | required | Alert Type | string | +**violation** | required | Violation | string | +**asset_id** | required | The ZeroFox Asset ID to associate the threat | numeric | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.alert_type | string | | +action_result.parameter.asset_id | numeric | | +action_result.parameter.source | string | | +action_result.parameter.violation | string | | +action_result.data.\*.alert_id | numeric | | +action_result.summary | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | + +## action: 'lookup alert' +Retrieve a single alert and it's details, identified by its unique integer identifier + +Type: **investigate** +Read only: **True** + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**alert_id** | required | ZeroFox Alert ID | numeric | + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.status | string | | success failed +action_result.parameter.alert_id | numeric | | +action_result.data.\*.alert.alert_type | string | | +action_result.data.\*.alert.network | string | | +action_result.data.\*.alert.offending_content_url | string | | +action_result.data.\*.alert.rule_name | string | | +action_result.data.\*.alert.status | string | | +action_result.data.\*.alert.timestamp | string | | +action_result.summary | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..fa5b8dc --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +# File: __init__.py +# +# Copyright (c) ZeroFox, 2024 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions +# and limitations under the License. diff --git a/logo_zerofox.svg b/logo_zerofox.svg new file mode 100644 index 0000000..f7a0023 --- /dev/null +++ b/logo_zerofox.svg @@ -0,0 +1 @@ + zerofox-logo-black diff --git a/logo_zerofox_dark.svg b/logo_zerofox_dark.svg new file mode 100644 index 0000000..2002c11 --- /dev/null +++ b/logo_zerofox_dark.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +THREAT FEED + diff --git a/manual_readme_content.md b/manual_readme_content.md new file mode 100644 index 0000000..1036032 --- /dev/null +++ b/manual_readme_content.md @@ -0,0 +1,14 @@ +[comment]: # File: manual_readme_content.md +[comment]: # +[comment]: # Copyright (c) ZeroFox, 2024 +[comment]: # +[comment]: # Licensed under the Apache License, Version 2.0 (the "License"); +[comment]: # you may not use this file except in compliance with the License. +[comment]: # You may obtain a copy of the License at +[comment]: # +[comment]: # http://www.apache.org/licenses/LICENSE-2.0 +[comment]: # +[comment]: # Unless required by applicable law or agreed to in writing, software distributed under +[comment]: # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +[comment]: # either express or implied. See the License for the specific language governing permissions +[comment]: # and limitations under the License. \ No newline at end of file diff --git a/release_notes/1.0.1.md b/release_notes/1.0.1.md new file mode 100644 index 0000000..d959ed1 --- /dev/null +++ b/release_notes/1.0.1.md @@ -0,0 +1 @@ +* Initial Release \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..c4644ad --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[flake8] +max-line-length = 145 +max-complexity = 28 +extend-ignore = F403,E128,E126,E111,E121,E127,E731,E201,E202,F405,E722,D,W292 + +[isort] +line_length = 145 diff --git a/zerofox.json b/zerofox.json new file mode 100644 index 0000000..1044d5d --- /dev/null +++ b/zerofox.json @@ -0,0 +1,496 @@ +{ + "appid": "015d60bf-fe28-4eeb-b726-161855707d7a", + "name": "ZeroFox", + "description": "ZeroFox Alerts for Splunk SOAR", + "type": "information", + "product_vendor": "ZeroFox", + "logo": "logo_zerofox.svg", + "logo_dark": "logo_zerofox_dark.svg", + "product_name": "ZeroFox", + "python_version": "3", + "product_version_regex": ".*", + "publisher": "ZeroFox", + "contributors": [ + { + "name": "Diego Ramirez" + }, + { + "name": "Felipe Garrido" + } + ], + "license": "Copyright (c) ZeroFox, 2024", + "app_version": "1.0.1", + "utctime_updated": "2023-07-26T17:04:21.523369Z", + "package_name": "phantom_zerofox", + "main_module": "zerofox_connector.py", + "min_phantom_version": "5.5.0", + "fips_compliant": false, + "app_wizard_version": "1.0.0", + "pip_dependencies": {}, + "configuration": { + "zerofox_api_token": { + "description": "ZeroFox API Token", + "data_type": "password", + "required": true, + "order": 0 + }, + "username": { + "description": "Your ZeroFOX platform username or email address", + "data_type": "string", + "required": true, + "order": 1 + }, + "reviewed": { + "description": "Only poll reviewed alerts", + "data_type": "boolean", + "default": true, + "order": 2 + }, + "history_days_interval": { + "description": "Initial historical alert poll interval (in days)", + "data_type": "string", + "required": true, + "order": 3 + }, + "verify_server_cert": { + "description": "Verify Sever Certificate", + "data_type": "boolean", + "default": true, + "order": 4 + } + }, + "actions": [ + { + "action": "test connectivity", + "identifier": "test_connectivity", + "description": "Validate the asset configuration for connectivity using supplied configuration", + "type": "test", + "read_only": true, + "parameters": {}, + "output": [], + "versions": "EQ(*)" + }, + { + "action": "on poll", + "identifier": "on_poll", + "description": "Callback action for the on_poll ingest functionality", + "type": "ingest", + "read_only": true, + "parameters": { + "container_id": { + "description": "Container IDs to limit the ingestion to", + "data_type": "string", + "order": 0 + }, + "start_time": { + "description": "Start of time range, in epoch time (milliseconds)", + "data_type": "numeric", + "order": 1 + }, + "end_time": { + "description": "End of time range, in epoch time (milliseconds)", + "data_type": "numeric", + "order": 2 + }, + "container_count": { + "description": "Maximum number of container records to query for", + "data_type": "numeric", + "order": 3 + }, + "artifact_count": { + "description": "Maximum number of artifact records to query for", + "data_type": "numeric", + "order": 4 + } + }, + "output": [], + "versions": "EQ(*)" + }, + { + "action": "take action", + "identifier": "take_alert_action", + "description": "Take action on a ZeroFox an alert", + "type": "generic", + "read_only": false, + "parameters": { + "alert_id": { + "description": "ZeroFox Alert ID", + "data_type": "numeric", + "required": true, + "order": 0 + }, + "alert_action": { + "data_type": "string", + "order": 1, + "description": "The action to take", + "value_list": [ + "close", + "escalate", + "request_takedown", + "mark_not_helpful" + ], + "default": "close", + "required": true + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "Status", + "column_order": 2, + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.alert_action", + "data_type": "string", + "column_name": "Alert Action", + "column_order": 1 + }, + { + "data_path": "action_result.parameter.alert_id", + "data_type": "numeric", + "column_name": "Alert ID", + "column_order": 0 + }, + { + "data_path": "action_result.data", + "data_type": "string" + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string", + "column_order": 3, + "column_name": "Message" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "column_order": 4, + "column_name": "Total Objects" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "column_order": 5, + "column_name": "Total Objects Successful" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "tag alert", + "identifier": "modify_alert_tag", + "description": "Add or remove a tag to a ZeroFox alert", + "type": "generic", + "read_only": false, + "parameters": { + "alert_id": { + "description": "ZeroFox Alert ID", + "data_type": "numeric", + "required": true, + "order": 0 + }, + "alert_tag": { + "data_type": "string", + "order": 1, + "description": "Tag", + "required": true + }, + "tag_action": { + "data_type": "string", + "order": 2, + "description": "Tag action: add or remove", + "value_list": [ + "add", + "remove" + ], + "default": "add", + "required": true + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "Status", + "column_order": 3, + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.alert_id", + "data_type": "numeric", + "column_name": "Alert ID", + "column_order": 0 + }, + { + "data_path": "action_result.parameter.alert_tag", + "data_type": "string", + "column_name": "Alert Action", + "column_order": 1 + }, + { + "data_path": "action_result.parameter.tag_action", + "data_type": "string", + "column_name": "Tag Action", + "column_order": 2 + }, + { + "data_path": "action_result.data", + "data_type": "string" + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string", + "column_order": 4, + "column_name": "Message" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "column_order": 5, + "column_name": "Total Objects" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "column_order": 6, + "column_name": "Total Objects Successful" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "threat submission", + "identifier": "threat_submit", + "description": "Add a manual threat to ZeroFox", + "type": "generic", + "read_only": false, + "parameters": { + "source": { + "description": "Source URL", + "data_type": "string", + "required": true, + "order": 0 + }, + "alert_type": { + "description": "Alert Type", + "data_type": "string", + "required": true, + "value_list": [ + "email", + "ip", + "domain", + "url", + "phone", + "mail_exchange", + "page_content", + "account" + ], + "order": 1 + }, + "violation": { + "description": "Violation", + "data_type": "string", + "required": true, + "value_list": [ + "phishing", + "malware", + "rogue_app", + "impersonation", + "trademark", + "copyright", + "private_data", + "fraud", + "other" + ], + "order": 2 + }, + "asset_id": { + "description": "The ZeroFox Asset ID to associate the threat", + "data_type": "numeric", + "required": true, + "order": 3 + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "Status", + "column_order": 1, + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.alert_type", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.asset_id", + "data_type": "numeric" + }, + { + "data_path": "action_result.parameter.source", + "data_type": "string" + }, + { + "data_path": "action_result.parameter.violation", + "data_type": "string" + }, + { + "data_path": "action_result.data.*.alert_id", + "data_type": "numeric", + "column_name": "Alert ID", + "column_order": 0 + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string", + "column_order": 2, + "column_name": "Message" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "column_order": 3, + "column_name": "Total Objects" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "column_order": 4, + "column_name": "Total Objects Successful" + } + ], + "render": { + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "lookup alert", + "identifier": "get_alert_by_id", + "description": "Retrieve a single alert and it's details, identified by its unique integer identifier", + "type": "investigate", + "read_only": true, + "parameters": { + "alert_id": { + "description": "ZeroFox Alert ID", + "data_type": "numeric", + "required": true, + "order": 0 + } + }, + "output": [ + { + "data_path": "action_result.status", + "data_type": "string", + "column_order": 7, + "column_name": "Status", + "example_values": [ + "success", + "failed" + ] + }, + { + "data_path": "action_result.parameter.alert_id", + "data_type": "numeric", + "column_name": "Alert ID", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.alert.alert_type", + "data_type": "string", + "column_name": "Type", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.alert.network", + "data_type": "string", + "column_name": "Network", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.alert.offending_content_url", + "data_type": "string", + "column_name": "URL", + "column_order": 5 + }, + { + "data_path": "action_result.data.*.alert.rule_name", + "data_type": "string", + "column_name": "Rule", + "column_order": 4 + }, + { + "data_path": "action_result.data.*.alert.status", + "data_type": "string", + "column_name": "Status", + "column_order": 1 + }, + { + "data_path": "action_result.data.*.alert.timestamp", + "data_type": "string", + "column_name": "Timestamp", + "column_order": 6 + }, + { + "data_path": "action_result.summary", + "data_type": "string" + }, + { + "data_path": "action_result.message", + "data_type": "string", + "column_order": 8, + "column_name": "Message" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric", + "column_order": 9, + "column_name": "Total Objects" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric", + "column_order": 10, + "column_name": "Total Objects Successful" + } + ], + "render": { + "width": 12, + "title": "ZeroFox Alert Details", + "type": "table", + "height": 5 + }, + "versions": "EQ(*)" + } + ] +} diff --git a/zerofox_connector.py b/zerofox_connector.py new file mode 100644 index 0000000..fdb19c2 --- /dev/null +++ b/zerofox_connector.py @@ -0,0 +1,1032 @@ +# File: zerofox_connector.py +# +# Copyright (c) ZeroFox, 2024 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions +# and limitations under the License. + +import json +import sys +from datetime import datetime, timedelta + +# Phantom App imports +import phantom.app as phantom +import requests +from bs4 import BeautifulSoup +from phantom.action_result import ActionResult +from phantom.base_connector import BaseConnector + +from zerofox_consts import ZEROFOX_API_URL + + +class RetVal(tuple): + def __new__(cls, val1, val2=None): + return tuple.__new__(RetVal, (val1, val2)) + + +class AlertMapper: + def _phantom_severity_transform(self, severity): + """ + Map ZeroFOX severity to Phantom severity. + + :param severity: ZeroFOX Severity: 5, 4, 3, 2, 1 + :return: Phantom Severity: high, medium, low + """ + return {5: "high", 4: "high", 3: "medium", 2: "low", 1: "low"}.get(severity) + + def build_artifact(self, container_id, alert): + """ + Artifacts are JSON objects that are stored in a container. + Artifacts are objects that are associated with a container and serve as + corroboration or evidence related to the container. Much like the + container schema, the artifact schema has a common header that can be + operated on, and also contains a Common Event Format (CEF) body and + raw data body to store elements that can be accessed by Splunk Phantom + playbooks as shown in the following code. The fields in the code are + defined in the table immediately following the code: + + { + "id": 1, + "version": 1, + "name": "test", + "label": "event", + "source_data_identifier": "140a7ae0-9da5-4ee2-b06c-64faa313e94a", + "create_time": "2016-01-18T19:26:39.053087Z", + "start_time": "2016-01-18T19:26:39.058797Z", + "end_time": null, + "severity": "low", + "type": null, + "kill_chain": null, + "hash": "EXAMPLEHASH", + "cef": { + "sourceAddress": "1.1.1.1" + }, + "container": 1, + "description": null, + "tags": [""], + "data": {} + } + + Create an artifact from a ZeroFOX alert. + + :param container_id: int + :param alert: ZeroFOX alert + :return: dict + """ + now = datetime.now() + + try: + perp_name = alert["perpetrator"]["name"] + except KeyError: + perp_name = "Concealed Perpetrator" + + try: + perp_content = alert["perpetrator"]["name"] + except KeyError: + perp_content = None + + artifact = dict() + artifact["container_id"] = container_id + artifact["label"] = "alert" + artifact["name"] = alert["rule_name"] + artifact["description"] = alert["offending_content_url"] + artifact["severity"] = self._phantom_severity_transform(alert["severity"]) + artifact["label"] = "event" + artifact["type"] = alert["network"] + artifact["tags"] = [alert["network"]] + artifact["start_time"] = now.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + artifact["source_data_identifier"] = alert["id"] + artifact["run_automation"] = False + + # get screenshot from metadata + try: + alert_metadata = alert["metadata"] + except KeyError: + alert_metadata = None + + if alert_metadata: + try: + m_data = json.loads(alert_metadata) + screenshot_url = m_data["alert_modal"]["screenshot"] + except KeyError: + screenshot_url = None + + artifact["cef"] = dict() + artifact["cef"]["alert_id"] = alert["id"] + artifact["cef"][ + "zerofox_url" + ] = f"https://cloud.zerofox.com/alerts/{alert['id']}" + artifact["cef"]["alert_type"] = alert["alert_type"] + artifact["cef"]["offending_content_url"] = alert["offending_content_url"] + artifact["cef"]["screenshot_url"] = screenshot_url + artifact["cef"]["entity"] = alert["entity"]["name"] + artifact["cef"]["perpetrator_name"] = perp_name + artifact["cef"]["perpetrator_url"] = alert["perpetrator"]["url"] + artifact["cef"]["perpetrator_type"] = alert["perpetrator"]["type"] + artifact["cef"]["perpetrator_content"] = perp_content + artifact["cef"]["rule_name"] = alert["rule_name"] + artifact["cef"]["rule_id"] = alert["rule_id"] + artifact["cef"]["notes"] = alert["notes"] + artifact["cef"]["reviewed"] = alert["reviewed"] + artifact["cef"]["escalated"] = alert["escalated"] + + return artifact + + def prepare_alert_container(self, alert): + """ + The contents of the container header and associated container data are + exposed to the Splunk Phantom platform as JSON objects. Playbooks + operate on these elements in order to make decisions and apply logic. + The following code shows an example of the container schema, and the + table immediately after the code defines the fields present in the code: + + { + "id": 107, + "version": "1", + "label": "incident", + "name": "my_test_incident", + "source_data_identifier": "64c2a9a4-d6ef-4da8-ad6f-982d785f14b2", + "description": "this is my test incident", + "status": "open", + "sensitivity": "amber", + "severity": "medium", + "create_time": "2016-01-16 07:18:46.631897+00", + "start_time": "2016-01-16 07:18:46.636966+00", + "end_time": "", + "due_time": "2016-01-16 19:18:00+00", + "close_time": "", + "kill_chain": "", + "owner": "admin", + "hash": "EXAMPLEHASH", + "tags": [""], + "asset_name": "", + "artifact_update_time": "2016-01-16 07:18:46.631875+00", + "container_update_time": "2016-01-16 07:19:12.359376+00", + "ingest_app_id": "", + "data": {}, + "artifact_count": 8 + } + + Create a container from ZeroFOX alert. + """ + + container = dict() + + container["label"] = self._container_label + container["name"] = "ZeroFOX Alert: {}".format(alert["rule_name"]) + container["description"] = "{}, {}".format( + alert["network"].title().replace("_", " "), alert["alert_type"] + ) + container["sensitivity"] = "white" + container["custom_fields"] = dict() + container["custom_fields"]["alert_type"] = str(alert["alert_type"]) + container["custom_fields"][ + "alert_url" + ] = f"https://cloud.zerofox.com/alerts/{alert['id']}" + + container["severity"] = self._phantom_severity_transform(alert["severity"]) + container["source_data_identifier"] = alert["id"] + container["asset_name"] = alert["entity"]["name"] + container["tags"] = alert["tags"] + date_time_obj = datetime.strptime(alert["timestamp"], "%Y-%m-%dT%H:%M:%S+00:00") + container["start_time"] = date_time_obj.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + container["ingest_app_id"] = self.get_app_id() + + return container + + +class ZeroFoxClient: + def __init__(self, token, username) -> None: + self.token = token + self.username = username + + def _get_app_headers(self): + return { + "Authorization": f"Token {self.token}", + "Content-Type": "application/json", + "zf-source": "splunk", + } + + def fetch_alerts(self): + pass + + +class ZerofoxAlertsConnector(BaseConnector): + def __init__(self): + # Call the BaseConnectors init first + super(ZerofoxAlertsConnector, self).__init__() + + self._state = None + + # Variable to hold a base_url in case the app makes REST calls + # Do note that the app json defines the asset config, so please + # modify this as you deem fit. + self._base_url = ZEROFOX_API_URL + self.mapper = AlertMapper() + + def _get_app_headers(self): + return { + "Authorization": f"Token {self._api_key}", + "Content-Type": "application/json", + "zf-source": "splunk", + } + + def _process_empty_response(self, response, action_result): + if response.status_code == 200: + return RetVal(phantom.APP_SUCCESS, {}) + + return RetVal( + action_result.set_status( + phantom.APP_ERROR, "Empty response and no information in the header" + ), + None, + ) + + def _process_html_response(self, response, action_result): + # An html response, treat it like an error + status_code = response.status_code + + try: + soup = BeautifulSoup(response.text, "html.parser") + error_text = soup.text + split_lines = error_text.split("\n") + split_lines = [x.strip() for x in split_lines if x.strip()] + error_text = "\n".join(split_lines) + except: + error_text = "Cannot parse error details" + + message = f"Status Code: {status_code}. Data from server:\n{error_text}\n" + + message = message.replace("{", "{{").replace("}", "}}") + + return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) + + def _process_json_response(self, r, action_result): + # Try a json parse + try: + resp_json = r.json() + except Exception as e: + return RetVal( + action_result.set_status( + phantom.APP_ERROR, + "Unable to parse JSON response. Error: {0}".format(str(e)), + ), + None, + ) + + # Please specify the status codes here + if 200 <= r.status_code < 399: + return RetVal(phantom.APP_SUCCESS, resp_json) + + # You should process the error returned in the json + message = "Error from server. Status Code: {0} Data from server: {1}".format( + r.status_code, r.text.replace("{", "{{").replace("}", "}}") + ) + + return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) + + def _process_response(self, r, action_result): + # store the r_text in debug data, it will get dumped in the logs if the action fails + if hasattr(action_result, "add_debug_data"): + action_result.add_debug_data({"r_status_code": r.status_code}) + action_result.add_debug_data({"r_text": r.text}) + action_result.add_debug_data({"r_headers": r.headers}) + + # Process a json response + if "json" in r.headers.get("Content-Type", ""): + return self._process_json_response(r, action_result) + + # Process an HTML response, Do this no matter what the api talks. + # There is a high chance of a PROXY in between phantom and the rest of + # world, in case of errors, PROXY's return HTML, this function parses + # the error and adds it to the action_result. + if "html" in r.headers.get("Content-Type", ""): + return self._process_html_response(r, action_result) + + # it's not content-type that is to be parsed, handle an empty response + if not r.text: + return self._process_empty_response(r, action_result) + + # everything else is actually an error at this point + message = f"Can't process response from server. Status Code: {r.status_code}\ + Data from server: {r.text.replace('{', '{{').replace('}', '}}')}" + return RetVal(action_result.set_status(phantom.APP_ERROR, message), None) + + def _make_rest_call(self, endpoint, action_result, method="get", **kwargs): + # **kwargs can be any additional parameters that requests.request accepts + + config = self.get_config() + + resp_json = None + + try: + request_func = getattr(requests, method) + except AttributeError: + return RetVal( + action_result.set_status( + phantom.APP_ERROR, f"Invalid method: {method}" + ), + resp_json, + ) + + # Create a URL to connect to + if "https://api.zerofox.com" in endpoint: + url = endpoint + else: + url = self._base_url + endpoint + + try: + r = request_func( + url, verify=config.get("verify_server_cert", False), **kwargs + ) + except Exception as e: + return RetVal( + action_result.set_status( + phantom.APP_ERROR, + "Error Connecting to server. Details: {0}".format(str(e)), + ), + resp_json, + ) + + return self._process_response(r, action_result) + + def _test_connectivity(self, param): + self.save_progress("Checking ZeroFOX API Credentials...") + self.save_progress(f"token={self._api_key}") + + action_result = self.add_action_result(ActionResult(dict(param))) + + headers = self._get_app_headers() + + endpoint = "/1.0/users/me/" + url = ZEROFOX_API_URL + endpoint + + ret_val, _ = self._make_rest_call( + url, action_result, params=None, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + self.save_progress("Test Connectivity Failed.") + return action_result.get_status() + + # Return success + self.save_progress("Test Connectivity Passed") + return action_result.set_status(phantom.APP_SUCCESS) + + def _phantom_daterange(self, param): + """ + Extract Phantom start time and end time as datetime objects. + Divide by 1000 to resolve milliseconds. + + :param param: dict + :return: start_time, end_time + """ + try: + start_time_param = float(param.get("start_time")) + end_time_param = float(param.get("end_time")) + except TypeError: + self.error_print("start time or end time not specified") + return None, None + + return datetime.fromtimestamp( + start_time_param / 1000.0 + ), datetime.fromtimestamp(end_time_param / 1000.0) + + def _save_alert(self, alert): + self.debug_print("----------------------------------------") + self.debug_print("PREPARE ALERT CONTAINER") + self.debug_print("----------------------------------------") + + container = self.mapper.prepare_alert_container(alert) + self.debug_print(f"container: {container}") + + status, message, container_id = self.save_container(container) + + if status == phantom.APP_SUCCESS and message != "Duplicate container found": + alert_artifacts = self.mapper.build_artifact(container_id, alert) + self.save_artifact(alert_artifacts) + self.save_progress("Created the alert `successfully`") + return status, message, container_id + else: + return status, message, container_id + + def _on_poll(self, param): + # Implement the handler here + # use self.save_progress(...) to send progress messages back to the platform + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + + self.debug_print(f"Param: {param}") + + # Add an action result object to self (BaseConnector) to represent the action for this param + action_result = self.add_action_result(ActionResult(dict(param))) + + self.debug_print("----------------------------------------") + self.debug_print("ON POLL CONNECTOR") + self.debug_print("----------------------------------------") + + start_time, end_time = self._phantom_daterange(param) + + if start_time is None or end_time is None: + action_result.set_status( + phantom.APP_ERROR, status_message="start time or end time not specified" + ) + + else: + self.save_progress("Start to create alerts") + self.save_progress(f"incident interval_days: {self._history_days_interval}") + + history_date = datetime.utcnow() - timedelta( + int(self._history_days_interval) + ) + + # reformat date to use with last_modified_min_date + interval_startdate = history_date.strftime("%Y-%m-%d %H:%M:%S") + + self.save_progress(f"incident interval_startdate: {interval_startdate}") + + alert_types = [] + alert_types.append({"type": "ALL", "subTypes": "ALL"}) + + self.debug_print("----------------------------------------") + self.debug_print("Get All Alerts") + self.debug_print("----------------------------------------") + + # check if we have a last_checked + self.debug_print(f"self._state: {self._state}") + self.debug_print(f"self._state type: {type(self._state)}") + + try: + last_checked_alert_time = self._state["last_polled"] + last_checked_alert_time = last_checked_alert_time.strftime( + "%Y-%m-%d %H:%M:%S" + ) + except: + last_checked_alert_time = interval_startdate + + self.debug_print( + "last_checked_alert_time: {}".format(last_checked_alert_time) + ) + + if self.is_poll_now(): + self.debug_print("POLL NOW") + self.debug_print(param.get("artifact_count", 0)) + + alert_params = { + "status": "open,escalated,investigation_completed", + "limit": "%s" % str(param.get("artifact_count", 0)), + "last_modified_min_date": "%s" % str(last_checked_alert_time), + } + + else: + self.debug_print("NORMAL POLL") + + alert_params = { + "status": "open,escalated,investigation_completed", + "last_modified_min_date": "%s" % str(last_checked_alert_time), + } + + if self._reviewed: + alert_params["reviewed"] = "true" + + # build call + endpoint = "/1.0/alerts/" + + headers = self._get_app_headers() + + self.debug_print(f"token={self._api_key}") + self.debug_print(f"params={alert_params}") + + # make rest call + ret_val, response = self._make_rest_call( + endpoint, action_result, params=alert_params, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + return action_result.get_status() + + self.debug_print("----------------------------------------") + + if self.is_poll_now(): + alert_total = param.get("artifact_count", 0) + else: + alert_total = response["count"] + + self.debug_print(f"count: {alert_total}") + self.debug_print(f"num_pages: {response['num_pages']}") + self.debug_print(f"next_url: {response['next']}") + self.debug_print("----------------------------------------") + + num_processed = 0 + next_url = response["next"] + + self.debug_print("parsing through list of alerts...") + + for alert in response["alerts"]: + alert_id = alert["id"] + + self.debug_print("alert_id: {}".format(alert_id)) + + # create container + status, message, container_id = self._save_alert(alert) + + if status == phantom.APP_SUCCESS: + num_processed += 1 + self.save_progress( + f"ZeroFOX Alert {alert_id} ingested ({num_processed} of {alert_total})" + ) + else: + self.error_print(f"Did not ingest alert {alert_id}") + action_result.set_status(phantom.APP_ERROR, message) + self.add_action_result(action_result) + return action_result.get_status() + + # dont continue to get more than max if polling now + if not self.is_poll_now(): + while next_url: + self.debug_print("next_url: {}".format(next_url)) + + alert_params = None + + # make rest call + ret_val, response = self._make_rest_call( + next_url, action_result, params=None, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + return action_result.get_status() + + next_url = response["next"] + + for alert in response["alerts"]: + alert_id = alert["id"] + + self.debug_print(f"alert_id: {alert_id}") + + # create container + status, message, container_id = self._save_alert(alert) + + if status == phantom.APP_SUCCESS: + num_processed += 1 + self.save_progress( + f"ZeroFOX Alert {alert_id} ingested ({num_processed} of {alert_total})" + ) + else: + self.error_print(f"Did not ingest alert {alert_id}") + action_result.set_status(phantom.APP_ERROR, message) + self.add_action_result(action_result) + return action_result.get_status() + + # set state + if not self.is_poll_now() and alert_total > 0: + state_time = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S") + self.debug_print("updating _state to {}".format(state_time)) + + self._state["last_polled"] = state_time + self.debug_print(f"saved state: {self._state}") + + self.save_progress("Ingesting ZeroFOX Alerts Completed.") + + if num_processed != alert_total: + action_result.set_status( + phantom.APP_ERROR, + status_message="Did not receive all the alerts from ZeroFOX", + ) + else: + action_result.set_status(phantom.APP_SUCCESS) + + self.debug_print("*** ENDING ***") + self.debug_print(f"updating to {action_result.get_status()}") + + self.add_action_result(action_result) + + return action_result.set_status(phantom.APP_SUCCESS) + + def _get_alert_by_id(self, param): + self.debug_print("----------------------------------------") + self.debug_print("get_alert_by_id") + self.debug_print("----------------------------------------") + self.debug_print(f"Param: {param}") + + action_result = ActionResult(dict(param)) + self.add_action_result(action_result) + self.debug_print( + "Initial action_result dictionary: {}".format(action_result.get_dict()) + ) + + alert_id = param.get("alert_id", 0.0) + + try: + if isinstance(alert_id, float): + return action_result.set_status( + phantom.APP_ERROR, + "Please provide a valid integer value in the 'alert_id' parameter", + ) + alert_id = int(alert_id) + except: + return action_result.set_status( + phantom.APP_ERROR, + "Please provide a valid integer value in the 'alert_id' parameter", + ) + + if alert_id < 0: + return action_result.set_status( + phantom.APP_ERROR, + "Please provide a valid non-negative integer value in the 'alert_id' parameter", + ) + + endpoint = f"/1.0/alerts/{alert_id}/" + + headers = self._get_app_headers() + + self.debug_print(f"token={self._api_key}") + + # make rest call + ret_val, response = self._make_rest_call( + endpoint, action_result, params=None, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + return action_result.get_status() + + # Add the response into the data section + action_result.add_data(response) + + self.debug_print(f"response={response}") + + # Add a dictionary that is made up of the most important values from data into the summary + summary = action_result.update_summary({}) + summary["alert_found"] = True + summary["alert_type"] = response["alert"]["alert_type"] + + self.debug_print("Updating the action_result summary.") + action_result.update_summary(summary) + + # Return success, no need to set the message, only the status + self.save_progress("Get Alert Passed") + + return action_result.set_status(phantom.APP_SUCCESS) + + def _modify_alert_tag(self, param): + # Implement the handler here + # use self.save_progress(...) to send progress messages back to the platform + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + + self.debug_print(f"Param: {param}") + + # Add an action result object to self (BaseConnector) to represent the action for this param + action_result = self.add_action_result(ActionResult(dict(param))) + + alert_id = param.get("alert_id") + alert_tag = param.get("alert_tag") + tag_action = param.get("tag_action", "add") + + self.save_progress(f"Adding tag {alert_tag} to alert {alert_id}") + + endpoint = "/1.0/alerttagchangeset/" + + headers = self._get_app_headers() + changes = {"alert": alert_id} + if tag_action == "add": + changes["added"] = [alert_tag] + changes["removed"] = [] + else: + changes["added"] = [] + changes["removed"] = [alert_tag] + + params = {"changes": [changes]} + + self.debug_print(f"token={self._api_key}") + self.debug_print(f"params={params}") + + # make rest call + ret_val, response = self._make_rest_call( + endpoint, action_result, method="post", json=params, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + return action_result.get_status() + + if phantom.is_fail(ret_val): + action_result.set_status( + phantom.APP_ERROR, + f"Error adding tag {alert_tag} on alert for: {alert_id}", + ) + self.debug_print( + f"Interim action_result dictionary after adding FAILURE status: {action_result.get_dict()}" + ) + summary = action_result.update_summary({}) + summary["status"] = "failed" + return action_result.set_status(phantom.APP_ERROR) + + # Add the response into the data section + action_result.add_data(response) + + # Add a dictionary that is made up of the most important values from data into the summary + summary = action_result.update_summary({}) + summary["num_alerts"] = 1 + summary["status"] = "success" + + # Return success, no need to set the message, only the status + self.save_progress("Alert Tag Passed") + + self.debug_print( + "-------------------------------------------------------------" + ) + self.debug_print("%s response: %s" % (self._banner, response)) + self.debug_print( + "-------------------------------------------------------------" + ) + + return action_result.set_status(phantom.APP_SUCCESS) + + def _threat_submit(self, param): + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + + self.debug_print(f"Param: {param}") + + action_result = self.add_action_result(ActionResult(dict(param))) + + source = param.get("source") + alert_type = param.get("alert_type") + violation = param.get("violation") + asset_id = param.get("asset_id") + + self.save_progress(f"Threat submission {source} to asset {asset_id}") + + endpoint = "/2.0/threat_submit/" + + headers = self._get_app_headers() + + params = { + "source": source, + "alert_type": alert_type, + "violation": violation, + "entity_id": asset_id, + } + + self.debug_print(f"token={self._api_key}") + self.debug_print(f"params={params}") + + # make rest call + ret_val, response = self._make_rest_call( + endpoint, action_result, method="post", json=params, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + return action_result.get_status() + + if phantom.is_fail(ret_val): + action_result.set_status( + phantom.APP_ERROR, + f"Error adding threat {source} on entity for: {asset_id}", + ) + self.debug_print( + f"Interim action_result dictionary after adding FAILURE status: {action_result.get_dict()}" + ) + summary = action_result.update_summary({}) + summary["status"] = "failed" + return action_result.set_status(phantom.APP_ERROR) + + # Add the response into the data section + action_result.add_data(response) + + self.debug_print(f"threat_response={response}") + # self.debug_print('threat_alert={}'.format(response['alert_id'])) + + # Add a dictionary that is made up of the most important values from data into the summary + summary = action_result.update_summary({}) + summary["num_alerts"] = 1 + summary["status"] = "success" + summary["alert_id"] = response["alert_id"] + + # Return success, no need to set the message, only the status + self.save_progress("Threat Submit Passed") + + self.debug_print( + "-------------------------------------------------------------" + ) + self.debug_print("%s response: %s" % (self._banner, response)) + self.debug_print( + "-------------------------------------------------------------" + ) + + return action_result.set_status(phantom.APP_SUCCESS) + + def _take_alert_action(self, param): + # Implement the handler here + # use self.save_progress(...) to send progress messages back to the platform + self.save_progress(f"In action handler for: {self.get_action_identifier()}") + + self.debug_print(f"Param: {param}") + + # Add an action result object to self (BaseConnector) to represent the action for this param + action_result = self.add_action_result(ActionResult(dict(param))) + + alert_id = param.get("alert_id") + alert_action = param.get("alert_action", "close") + + self.save_progress(f"Issuing {alert_action} on alert {alert_id}") + endpoint = f"/1.0/alerts/{alert_id}/{alert_action}/" + + headers = self._get_app_headers() + + params = {"actor": "%s" % self._actor} + + self.debug_print(f"token={self._api_key}") + self.debug_print(f"params={params}") + + # make rest call + ret_val, response = self._make_rest_call( + endpoint, action_result, method="post", json=params, headers=headers + ) + + if phantom.is_fail(ret_val): + # the call to the 3rd party device or service failed, action result should contain all the error details + # for now the return is commented out, but after implementation, return from here + return action_result.get_status() + + if phantom.is_fail(ret_val): + action_result.set_status( + phantom.APP_ERROR, + f"Error taking {alert_action} action on alert data for: {alert_id}", + ) + self.debug_print( + f"Interim action_result dictionary after adding FAILURE status: {action_result.get_dict()}" + ) + summary = action_result.update_summary({}) + summary["status"] = "failed" + return action_result.set_status(phantom.APP_ERROR) + + # Add the response into the data section + action_result.add_data(response) + + # Add a dictionary that is made up of the most important values from data into the summary + summary = action_result.update_summary({}) + summary["num_alerts"] = 1 + summary["status"] = "success" + + # Return success, no need to set the message, only the status + self.save_progress("Alert Action Passed") + + self.debug_print( + "-------------------------------------------------------------" + ) + self.debug_print("%s response: %s" % (self._banner, response)) + self.debug_print( + "-------------------------------------------------------------" + ) + + return action_result.set_status(phantom.APP_SUCCESS) + + def handle_action(self, param): + ret_val = phantom.APP_SUCCESS + + action_id = self.get_action_identifier() + + self.debug_print("action_id", self.get_action_identifier()) + self.debug_print(f"Ingesting handle action in: {param}") + + if action_id == "test_connectivity": + ret_val = self._test_connectivity(param) + + elif action_id == "get_alert_by_id": + ret_val = self._get_alert_by_id(param) + + elif action_id == "take_alert_action": + ret_val = self._take_alert_action(param) + + elif action_id == "modify_alert_tag": + ret_val = self._modify_alert_tag(param) + + elif action_id == "threat_submit": + ret_val = self._threat_submit(param) + + elif action_id == "on_poll": + ret_val = self._on_poll(param) + + return ret_val + + def initialize(self): + # Load the state in initialize, use it to store data + # that needs to be accessed across actions + self._state = self.load_state() + + # get the asset config + config = self.get_config() + + """ + # Access values in asset config by the name + + # Required values can be accessed directly + required_config_name = config['required_config_name'] + + # Optional values should use the .get() function + optional_config_name = config.get('optional_config_name') + """ + + self._base_url = ZEROFOX_API_URL + self._api_key = config.get("zerofox_api_token") + self._history_days_interval = config.get("history_days_interval") + self._reviewed = config.get("reviewed") + self._container_label = config["ingest"]["container_label"] + self._actor = config.get("username") + self._banner = "ZeroFOX Alerts Connector" + self.zf_client = ZeroFoxClient( + token=config.get("zerofox_api_token"), username=config.get("username") + ) + + return phantom.APP_SUCCESS + + def finalize(self): + # Save the state, this data is saved across actions and app upgrades + self.save_state(self._state) + return phantom.APP_SUCCESS + + +if __name__ == "__main__": + import argparse + + import pudb + + pudb.set_trace() + + argparser = argparse.ArgumentParser() + + argparser.add_argument("input_test_json", help="Input Test JSON file") + argparser.add_argument("-u", "--username", help="username", required=False) + argparser.add_argument("-p", "--password", help="password", required=False) + argparser.add_argument('-v', '--verify', action='store_true', help='verify', required=False, default=False) + + args = argparser.parse_args() + session_id = None + + username = args.username + password = args.password + verify = args.verify + + if username is not None and password is None: + # User specified a username but not a password, so ask + import getpass + + password = getpass.getpass("Password: ") + + if username and password: + try: + login_url = ZerofoxAlertsConnector._get_phantom_base_url() + "/login" + + print("Accessing the Login page") + r = requests.get(login_url, verify=verify) + csrftoken = r.cookies["csrftoken"] + + data = dict() + data["username"] = username + data["password"] = password + data["csrfmiddlewaretoken"] = csrftoken + + headers = dict() + headers["Cookie"] = f"csrftoken={csrftoken}" + headers["Referer"] = login_url + + print("Logging into Platform to get the session id") + r2 = requests.post(login_url, verify=verify, data=data, headers=headers) + session_id = r2.cookies["sessionid"] + except Exception as e: + print(f"Unable to get session id from the platform. Error: {e}") + sys.exit(1) + + with open(args.input_test_json) as f: + in_json = f.read() + in_json = json.loads(in_json) + print(json.dumps(in_json, indent=4)) + + connector = ZerofoxAlertsConnector() + connector.print_progress_message = True + + if session_id is not None: + in_json["user_session_token"] = session_id + connector._set_csrf_info(csrftoken, headers["Referer"]) + + ret_val = connector._handle_action(json.dumps(in_json), None) + print(json.dumps(json.loads(ret_val), indent=4)) + + sys.exit(0) diff --git a/zerofox_consts.py b/zerofox_consts.py new file mode 100644 index 0000000..65c5ac8 --- /dev/null +++ b/zerofox_consts.py @@ -0,0 +1,18 @@ +# File: zerofox_consts.py +# +# Copyright (c) ZeroFox, 2024 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +# either express or implied. See the License for the specific language governing permissions +# and limitations under the License. + +# Define your constants here + +ZEROFOX_API_URL = 'https://api.zerofox.com'