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/.gitignore b/.gitignore new file mode 100644 index 0000000..1d74e21 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e6c658..9f55448 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.23 hooks: - id: org-hook - id: package-app-dependencies - repo: https://github.com/Yelp/detect-secrets - rev: v1.2.0 + rev: v1.5.0 hooks: - id: detect-secrets - args: ['--no-verify'] + args: ['--no-verify', '--exclude-files', '^gsgmail.json$'] diff --git a/LICENSE b/LICENSE index 81a1474..ec0af9e 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2024 Splunk Inc. + Copyright (c) 2024 Splunk Inc. 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 e65c33f..1cf1ebb 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,138 @@ -# Splunk> Phantom +[comment]: # "Auto-generated SOAR connector documentation" +# Cisco Talos Intelligence -Welcome to the open-source repository for Splunk> Phantom's talosintelligencev2 App. +Publisher: Splunk +Connector Version: 1.0.1 +Product Vendor: Cisco +Product Name: Talos Intelligence +Product Version Supported (regex): ".\*" +Minimum Product Version: 6.3.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. +This app provides investigative actions for Cisco Talos Intelligence -## Legal and License +[comment]: # " File: README.md" +[comment]: # "Copyright (c) 2024 Splunk Inc." +[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." +[comment]: # "" +## Cisco Talos Intelligence license for Splunk SOAR (Cloud) -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. +The Cisco Talos Intelligence license is included with your Splunk SOAR (Cloud) license. + +## Overview + +This app uses the Cisco Talos API that specializes in identifying, analyzing, and mitigating cybersecurity threats. + +For additional details, see the [Cisco Talos Intelligence article](https://docs.splunk.com/Documentation/SOAR/current/Playbook/Talos) in the Splunk SOAR documentation. + +**Note:** The Cisco Talos Intelligence asset is already configured in your Splunk SOAR (Cloud) deployment. + +### Supported Actions +[test connectivity](#action-test-connectivity) - Validate the asset configuration for connectivity using supplied configuration +[ip reputation](#action-ip-reputation) - Look up Cisco Talos threat intelligence for a given IP address +[domain reputation](#action-domain-reputation) - Look up Cisco Talos threat intelligence for a given domain +[url reputation](#action-url-reputation) - Look up Cisco Talos threat intelligence for a given URL + +## action: 'test connectivity' +Validate the asset configuration for connectivity using supplied configuration + +Type: **test** +Read only: **True** + +Action uses the URS API to get a list of the AUP categories used to classify website content. + +#### Action Parameters +No parameters are required for this action + +#### Action Output +No Output + +## action: 'ip reputation' +Look up Cisco Talos threat intelligence for a given IP address + +Type: **investigate** +Read only: **True** + +Provides information on an IP address's reputation, so you can take appropriate action against untrusted or unwanted resources. + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**ip** | required | IP to query | string | `ip` `ipv6` + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.parameter.ip | string | `ip` `ipv6` | +action_result.status | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | +action_result.data.\*.Observable | string | | +action_result.data.\*.Threat_Level | string | | +action_result.data.\*.Threat_Categories | string | | +action_result.data.\*.AUP | string | | +action_result.summary.message | string | | 72.163.4.185 has a Favorable threat level + +## action: 'domain reputation' +Look up Cisco Talos threat intelligence for a given domain + +Type: **investigate** +Read only: **True** + +Provides information on a domain's reputation, so you can take appropriate action against untrusted or unwanted resources. + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**domain** | required | Domain to query | string | `domain` + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.parameter.domain | string | `domain` | +action_result.status | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | +action_result.data.\*.Observable | string | | +action_result.data.\*.Threat_Level | string | | +action_result.data.\*.Threat_Categories | string | | +action_result.data.\*.AUP | string | | +action_result.summary.message | string | | splunk.com has a Favorable threat level + +## action: 'url reputation' +Look up Cisco Talos threat intelligence for a given URL + +Type: **investigate** +Read only: **True** + +Provides information on a URL's reputation, so you can take appropriate action against untrusted or unwanted resources. + +#### Action Parameters +PARAMETER | REQUIRED | DESCRIPTION | TYPE | CONTAINS +--------- | -------- | ----------- | ---- | -------- +**url** | required | URL to query | string | `url` + +#### Action Output +DATA PATH | TYPE | CONTAINS | EXAMPLE VALUES +--------- | ---- | -------- | -------------- +action_result.parameter.url | string | `url` | +action_result.status | string | | +action_result.message | string | | +summary.total_objects | numeric | | +summary.total_objects_successful | numeric | | +action_result.data.\*.Observable | string | | +action_result.data.\*.Threat_Level | string | | +action_result.data.\*.Threat_Categories | string | | +action_result.data.\*.AUP | string | | +action_result.summary.message | string | | https://splunk.com has a Favorable threat level \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..300a6cd --- /dev/null +++ b/__init__.py @@ -0,0 +1,14 @@ +# File: __init__.py +# +# Copyright (c) 2024 Splunk Inc. +# +# 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/ciscotalosintelligence.json b/ciscotalosintelligence.json new file mode 100644 index 0000000..0de42ac --- /dev/null +++ b/ciscotalosintelligence.json @@ -0,0 +1,406 @@ +{ + "appid": "7c653487-22c8-4ec1-bca0-16a8b1513c86", + "name": "Cisco Talos Intelligence", + "description": "This app provides investigative actions for Cisco Talos Intelligence", + "type": "information", + "product_vendor": "Cisco", + "logo": "ciscotalosintelligence.svg", + "logo_dark": "ciscotalosintelligence_dark.svg", + "product_name": "Talos Intelligence", + "python_version": "3", + "latest_tested_versions": [ + "Cloud, October 30, 2024" + ], + "product_version_regex": ".*", + "publisher": "Splunk", + "license": "Copyright (c) 2024 Splunk Inc.", + "app_version": "1.0.1", + "utctime_updated": "2024-06-21T18:40:03.685771Z", + "package_name": "phantom_ciscotalosintelligence", + "fips_compliant": false, + "main_module": "ciscotalosintelligence_connector.py", + "min_phantom_version": "6.3.0", + "app_wizard_version": "1.0.0", + "pip39_dependencies": { + "wheel": [ + { + "module": "anyio", + "input_file": "wheels/py3/anyio-4.6.2.post1-py3-none-any.whl" + }, + { + "module": "beautifulsoup4", + "input_file": "wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl" + }, + { + "module": "cffi", + "input_file": "wheels/py39/cffi-1.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + { + "module": "cryptography", + "input_file": "wheels/py39/cryptography-42.0.5-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + { + "module": "exceptiongroup", + "input_file": "wheels/py3/exceptiongroup-1.2.2-py3-none-any.whl" + }, + { + "module": "h11", + "input_file": "wheels/py3/h11-0.14.0-py3-none-any.whl" + }, + { + "module": "h2", + "input_file": "wheels/py3/h2-4.1.0-py3-none-any.whl" + }, + { + "module": "hpack", + "input_file": "wheels/py3/hpack-4.0.0-py3-none-any.whl" + }, + { + "module": "httpcore", + "input_file": "wheels/py3/httpcore-1.0.6-py3-none-any.whl" + }, + { + "module": "httpx", + "input_file": "wheels/py3/httpx-0.27.2-py3-none-any.whl" + }, + { + "module": "hyperframe", + "input_file": "wheels/py3/hyperframe-6.0.1-py3-none-any.whl" + }, + { + "module": "pycparser", + "input_file": "wheels/py3/pycparser-2.22-py3-none-any.whl" + }, + { + "module": "pydantic", + "input_file": "wheels/py39/pydantic-1.10.13-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl" + }, + { + "module": "sniffio", + "input_file": "wheels/py3/sniffio-1.3.1-py3-none-any.whl" + }, + { + "module": "typing_extensions", + "input_file": "wheels/py3/typing_extensions-4.12.2-py3-none-any.whl" + } + ] + }, + "configuration": { + "base_url": { + "data_type": "string", + "order": 0, + "description": "Base URL provided by Talos.", + "default": "https://soar-api.talos.cisco.com", + "required": true, + "name": "base_url", + "id": 0, + "visibility": [] + }, + "certificate": { + "data_type": "password", + "order": 1, + "description": "Certificate contents to authenticate with Talos.", + "required": true, + "name": "certificate", + "id": 1, + "visibility": [] + }, + "key": { + "data_type": "password", + "order": 2, + "description": "Private key to authenticate with Talos.", + "required": true, + "name": "key", + "id": 2, + "visibility": [] + }, + "verify_server_cert": { + "description": "Verify server certificate.", + "data_type": "boolean", + "default": false, + "order": 3, + "visibility": [] + } + }, + "actions": [ + { + "action": "test connectivity", + "identifier": "test_connectivity", + "description": "Validate the asset configuration for connectivity using supplied configuration", + "verbose": "Action uses the URS API to get a list of the AUP categories used to classify website content.", + "type": "test", + "read_only": true, + "parameters": {}, + "output": [], + "versions": "EQ(*)" + }, + { + "action": "ip reputation", + "identifier": "ip_reputation", + "description": "Look up Cisco Talos threat intelligence for a given IP address", + "verbose": "Provides information on an IP address's reputation, so you can take appropriate action against untrusted or unwanted resources.", + "type": "investigate", + "read_only": true, + "parameters": { + "ip": { + "description": "IP to query", + "data_type": "string", + "required": true, + "primary": true, + "contains": [ + "ip", + "ipv6" + ], + "value_list": [], + "default": "", + "order": 0, + "name": "ip" + } + }, + "output": [ + { + "data_path": "action_result.parameter.ip", + "data_type": "string", + "contains": [ + "ip", + "ipv6" + ] + }, + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "status", + "column_order": 1 + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.Observable", + "data_type": "string", + "column_name": "Observable", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.Threat_Level", + "data_type": "string", + "column_name": "threat level", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.Threat_Categories", + "data_type": "string", + "column_name": "threat categories", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.AUP", + "data_type": "string", + "column_name": "Acceptable Use Policy Categories", + "column_order": 4 + }, + { + "data_path": "action_result.summary.message", + "data_type": "string", + "example_values": [ + "72.163.4.185 has a Favorable threat level" + ] + } + ], + "render": { + "type": "table", + "title": "IP Reputation Results" + }, + "versions": "EQ(*)" + }, + { + "action": "domain reputation", + "identifier": "domain_reputation", + "description": "Look up Cisco Talos threat intelligence for a given domain", + "verbose": "Provides information on a domain's reputation, so you can take appropriate action against untrusted or unwanted resources.", + "type": "investigate", + "read_only": true, + "parameters": { + "domain": { + "description": "Domain to query", + "data_type": "string", + "required": true, + "primary": true, + "contains": [ + "domain" + ], + "value_list": [], + "default": "", + "order": 0, + "name": "domain" + } + }, + "output": [ + { + "data_path": "action_result.parameter.domain", + "data_type": "string", + "contains": [ + "domain" + ] + }, + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "status", + "column_order": 1 + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.Observable", + "data_type": "string", + "column_name": "Observable", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.Threat_Level", + "data_type": "string", + "column_name": "threat level", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.Threat_Categories", + "data_type": "string", + "column_name": "threat categories", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.AUP", + "data_type": "string", + "column_name": "Acceptable Use Policy Categories", + "column_order": 4 + }, + { + "data_path": "action_result.summary.message", + "data_type": "string", + "example_values": [ + "splunk.com has a Favorable threat level" + ] + } + ], + "render": { + "title": "Domain Reputation Results", + "type": "table" + }, + "versions": "EQ(*)" + }, + { + "action": "url reputation", + "identifier": "url_reputation", + "description": "Look up Cisco Talos threat intelligence for a given URL", + "verbose": "Provides information on a URL's reputation, so you can take appropriate action against untrusted or unwanted resources.", + "type": "investigate", + "read_only": true, + "parameters": { + "url": { + "description": "URL to query", + "data_type": "string", + "required": true, + "primary": true, + "contains": [ + "url" + ], + "value_list": [], + "default": "", + "order": 0, + "name": "url" + } + }, + "output": [ + { + "data_path": "action_result.parameter.url", + "data_type": "string", + "contains": [ + "url" + ] + }, + { + "data_path": "action_result.status", + "data_type": "string", + "column_name": "status", + "column_order": 1 + }, + { + "data_path": "action_result.message", + "data_type": "string" + }, + { + "data_path": "summary.total_objects", + "data_type": "numeric" + }, + { + "data_path": "summary.total_objects_successful", + "data_type": "numeric" + }, + { + "data_path": "action_result.data.*.Observable", + "data_type": "string", + "column_name": "Observable", + "column_order": 0 + }, + { + "data_path": "action_result.data.*.Threat_Level", + "data_type": "string", + "column_name": "threat level", + "column_order": 2 + }, + { + "data_path": "action_result.data.*.Threat_Categories", + "data_type": "string", + "column_name": "threat categories", + "column_order": 3 + }, + { + "data_path": "action_result.data.*.AUP", + "data_type": "string", + "column_name": "Acceptable Use Policy Categories", + "column_order": 4 + }, + { + "data_path": "action_result.summary.message", + "data_type": "string", + "example_values": [ + "https://splunk.com has a Favorable threat level" + ] + } + ], + "render": { + "title": "URL Reputation Results", + "type": "table" + }, + "versions": "EQ(*)" + } + ], + "copied_from_id": 190, + "copied_from_version": "1.0.0", + "directory": "ciscotalosintelligence_7c653487-22c8-4ec1-bca0-16a8b1513c86", + "version": 1, + "appname": "-", + "executable": "spawn3", + "disabled": false, + "custom_made": true +} diff --git a/ciscotalosintelligence.svg b/ciscotalosintelligence.svg new file mode 100644 index 0000000..34751d7 --- /dev/null +++ b/ciscotalosintelligence.svg @@ -0,0 +1 @@ + diff --git a/ciscotalosintelligence_connector.py b/ciscotalosintelligence_connector.py new file mode 100644 index 0000000..669b4c1 --- /dev/null +++ b/ciscotalosintelligence_connector.py @@ -0,0 +1,567 @@ +# File: ciscotalosintelligence_connector.py +# +# Copyright (c) 2024 Splunk Inc. +# +# 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 ipaddress +import json +import os +import random +import re +import tempfile +import textwrap +import time +from datetime import datetime +from urllib.parse import urlparse + +import httpx + +# Phantom App imports +import phantom.app as phantom +import requests +from bs4 import BeautifulSoup +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from phantom.action_result import ActionResult +from phantom.base_connector import BaseConnector +from phantom_common.install_info import is_dev_env + +from ciscotalosintelligence_consts import * + + +class RetVal(tuple): + def __new__(cls, val1, val2=None): + return tuple.__new__(RetVal, (val1, val2)) + + +class TalosIntelligenceConnector(BaseConnector): + + def __init__(self): + super(TalosIntelligenceConnector, self).__init__() + + self._state = None + + self._base_url = None + self._cert = None + self._key = None + + self._appinfo = None + self._catalog_id = 2 + + 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 = "Status Code: {0}. Data from server:\n{1}\n".format(status_code, error_text) + + 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, retry=3): + # 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}) + + retryable_error_codes = {2, 4, 8, 9, 13, 14} + + if retry < MAX_REQUEST_RETRIES: + if r.headers.get("grpc-status", 0) in retryable_error_codes: + err_msg = r.headers.get("grpc-message", "Error") + return ( + action_result.set_status( + phantom.APP_ERROR, f"Got retryable grpc-status of {r.headers['grpc-status']} with message {err_msg}" + ), + r, + ) + + if r.status_code == 503: + return action_result.set_status(phantom.APP_ERROR, "Got retryable http status code {0}".format(r.status_code)), r + + # Process each 'Content-Type' of response separately + + # 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 = "Can't process response 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 _make_rest_call(self, retry, endpoint, action_result, method="get", **kwargs): + # **kwargs can be any additional parameters that requests.request accepts + + config = self.get_config() + + resp_json = None + + # Create a URL to connect to + url = self._base_url + endpoint + + delay = 0.25 + for i in range(MAX_CONNECTION_RETIRIES): + try: + request_func = getattr(self.client, method) + + r = request_func(url, **kwargs) + break + except Exception as e: + self.debug_print(f"Retrying to establish connection to the server for the {i + 1} time") + jittered_delay = random.uniform(delay * 0.9, delay * 1.1) + time.sleep(jittered_delay) + delay = min(delay * 2, 256) + + with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix="test") as temp_file: + cert_string = f"-----BEGIN CERTIFICATE-----\n{self._cert}\n-----END CERTIFICATE-----" + cert = f"{cert_string}\n-----BEGIN RSA PRIVATE KEY-----\n{self._key}\n-----END RSA PRIVATE KEY-----\n" + temp_file.write(cert) + temp_file.seek(0) # Move the file pointer to the beginning for reading + temp_file_path = temp_file.name # Get the name of the temporary file + self.client = httpx.Client( + http2=True, verify=config.get("verify_server_cert", False), cert=temp_file_path, timeout=MAX_REQUEST_TIMEOUT + ) + + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + + if i == MAX_CONNECTION_RETIRIES - 1: + 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, retry) + + def _make_rest_call_helper(self, *args, **kwargs): + request_delay = 0.25 + max_processing_time = time.time() + MAX_REQUEST_TIMEOUT + for i in range(MAX_REQUEST_RETRIES + 1): + if time.time() > max_processing_time: + action_result = args[1] + return action_result.set_status(phantom.APP_ERROR, f"Max request timeout of {MAX_REQUEST_TIMEOUT}s exceeded"), None + + ret_val, response = self._make_rest_call(i, *args, **kwargs) + if phantom.is_fail(ret_val) and response: + time.sleep(request_delay) + request_delay *= 2 + else: + break + + return ret_val, response + + def _handle_test_connectivity(self, param): + action_result = self.add_action_result(ActionResult(dict(param))) + self.save_progress("Connecting to endpoint") + + prev_perf_testing_val = self._appinfo["perf_testing"] + self._appinfo["perf_testing"] = True + + payload = {"urls": [{"raw_url": "cisco.com"}], "app_info": self._appinfo} + ret_val, response = self._make_rest_call_helper(ENDPOINT_QUERY_REPUTATION_V3, action_result, method="post", json=payload) + + self._appinfo["perf_testing"] = prev_perf_testing_val + + if phantom.is_fail(ret_val): + self.save_progress("Test Connectivity Failed.") + return action_result.get_status() + + self.save_progress("Received Metadata") + self.save_progress("Test Connectivity Passed") + + self._state = {} + return action_result.set_status(phantom.APP_SUCCESS) + + def format_ip_type(self, ip_addr): + if isinstance(ip_addr, ipaddress.IPv4Address): + return {"ipv4_addr": int(ip_addr)} + elif isinstance(ip_addr, ipaddress.IPv6Address): + return {"ipv6_addr": ip_addr.packed.hex()} + else: + raise Exception(f"{ip_addr} is not valid") + + def _handle_ip_reputation(self, param): + self.save_progress("In action handler for: {0}".format(self.get_action_identifier())) + action_result = self.add_action_result(ActionResult(dict(param))) + + ip = param["ip"] + + try: + ip_addr = ipaddress.ip_address(ip) + ip_request = self.format_ip_type(ip_addr) + except Exception as exc: + return action_result.set_status(phantom.APP_ERROR, f"Please provide a valid IP Address. Error: {exc}") + + payload = {"urls": {"endpoint": [ip_request]}, "app_info": self._appinfo} + + ret_val = self._query_reputation(action_result, payload, ip) + if phantom.is_fail(ret_val): + return action_result.get_status() + + summary = action_result.update_summary({}) + threat_level = action_result.get_data()[0]["Threat_Level"] + summary["Message"] = f"{ip} has a {threat_level} threat level" + + return action_result.set_status(phantom.APP_SUCCESS) + + def _is_valid_domain(self, domain): + regex = r"^(?!-)([A-Za-z0-9-]{1,63}(? self._state["taxonomy_version"]: + taxonomy_ret_val, taxonomy = self._fetch_taxonomy(action_result, allow_cache=False) + + if phantom.is_fail(ret_val) or "results" not in response: + return action_result.get_status() + + threat_level = "" + threat_categories = {} + aup_categories = {} + + for result in response["results"]: + for url_result in result["results"]: + for tag in url_result["context_tags"]: + tax_id = str(tag["taxonomy_id"]) + entry_id = str(tag["taxonomy_entry_id"]) + + if tax_id not in taxonomy["taxonomies"]: + continue + + if not taxonomy["taxonomies"][tax_id]["is_avail"]: + continue + + category = taxonomy["taxonomies"][tax_id]["name"]["en-us"]["text"] + name = taxonomy["taxonomies"][tax_id]["entries"][entry_id]["name"]["en-us"]["text"] + description = taxonomy["taxonomies"][tax_id]["entries"][entry_id]["description"]["en-us"]["text"] + + if category == "Threat Levels": + threat_level = name + elif category == "Threat Categories": + threat_categories[name] = description + elif category == "Acceptable Use Policy Categories": + aup_categories[name] = description + + output = {} + output["Observable"] = observable + output["Threat_Level"] = threat_level + output["Threat_Categories"] = ", ".join(list(threat_categories.keys())) + output["AUP"] = ", ".join(list(aup_categories.keys())) + + action_result.add_data(output) + + return phantom.APP_SUCCESS + + def _fetch_taxonomy(self, action_result, allow_cache=True): + + payload = {"app_info": self._appinfo} + + if "taxonomy" in self._state and allow_cache: + return 1, self._state["taxonomy"] + + ret_val, response = self._make_rest_call_helper(ENDPOINT_QUERY_TAXONOMIES, action_result, method="post", json=payload) + self.debug_print("fetching taxonomy") + if phantom.is_fail(ret_val): + return action_result.get_status() + + taxonomy = response["catalogs"][str(self._catalog_id)] + + self._state = {"taxonomy": taxonomy, "taxonomy_version": response["version"]} + + return ret_val, taxonomy + + 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()) + + if action_id == "ip_reputation": + ret_val = self._handle_ip_reputation(param) + + if action_id == "domain_reputation": + ret_val = self._handle_domain_reputation(param) + + if action_id == "url_reputation": + ret_val = self._handle_url_reputation(param) + + if action_id == "test_connectivity": + ret_val = self._handle_test_connectivity(param) + + return ret_val + + def check_certificate_expiry(self, cert): + not_before = cert.not_valid_before + not_after = cert.not_valid_after + now = datetime.utcnow() + return not_before <= now <= not_after + + def fetch_crls(self, cert): + try: + crl_distribution_points = cert.extensions.get_extension_for_oid(x509.ExtensionOID.CRL_DISTRIBUTION_POINTS).value + + crl_urls = [] + + for point in crl_distribution_points: + for general_name in point.full_name: + if isinstance(general_name, x509.DNSName): + crl_urls.append(f"http://{general_name.value}") + elif isinstance(general_name, x509.UniformResourceIdentifier): + crl_urls.append(general_name.value) + + return crl_urls + except x509.ExtensionNotFound: + self.debug_print("CRL Distribution Points extension not found in the certificate.") + return [] + + 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() + + def insert_newlines(string, every=64): + lines = [] + for i in range(0, len(string), every): + lines.append(string[i : i + every]) + + return "\n".join(lines) + + self._base_url = config["base_url"] + self._cert = insert_newlines(config["certificate"]) + self._key = insert_newlines(config["key"]) + + cert_string = f"-----BEGIN CERTIFICATE-----\n{textwrap.fill(self._cert, 64)}\n-----END CERTIFICATE-----" + cert_pem_data = cert_string.encode("utf-8") + try: + cert = x509.load_pem_x509_certificate(cert_pem_data, default_backend()) + except Exception as e: + self.debug_print(f"Error when loadig cert {e}") + return phantom.APP_ERROR + + is_valid = self.check_certificate_expiry(cert) + if not is_valid: + self.debug_print("Certificate is expired. Please use a valid cert") + return phantom.APP_ERROR + + self._appinfo = { + "product_family": "splunk", + "product_id": "soar", + "device_id": self.get_product_installation_id(), + "product_version": self.get_app_json()["app_version"], + "perf_testing": False, + } + if is_dev_env: + self._appinfo["perf_testing"] = True + + with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix="test") as temp_file: + cert = f"{cert_string}\n-----BEGIN RSA PRIVATE KEY-----\n{textwrap.fill(self._key, 64)}\n-----END RSA PRIVATE KEY-----\n" + + temp_file.write(cert) + temp_file.seek(0) # Move the file pointer to the beginning for reading + temp_file_path = temp_file.name # Get the name of the temporary file + + # exceptions shouldn't really be thrown here because most network related disconnections will happen when a request is sent + try: + self.client = httpx.Client( + http2=True, verify=config.get("verify_server_cert", False), cert=temp_file_path, timeout=MAX_REQUEST_TIMEOUT + ) + except Exception as e: + self.debug_print(f"Could not connect to server because of {e}") + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + return phantom.APP_ERROR + + if os.path.exists(temp_file_path): + os.remove(temp_file_path) + return phantom.APP_SUCCESS + + def finalize(self): + self.save_state(self._state) + return phantom.APP_SUCCESS + + +def main(): + import argparse + + 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) + + args = argparser.parse_args() + session_id = None + + username = args.username + password = args.password + + 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 = TalosIntelligenceConnector._get_phantom_base_url() + "/login" + + print("Accessing the Login page") + r = requests.get(login_url, verify=False) + csrftoken = r.cookies["csrftoken"] + + data = dict() + data["username"] = username + data["password"] = password + data["csrfmiddlewaretoken"] = csrftoken + + headers = dict() + headers["Cookie"] = "csrftoken=" + csrftoken + headers["Referer"] = login_url + + print("Logging into Platform to get the session id") + r2 = requests.post(login_url, verify=False, data=data, headers=headers) + session_id = r2.cookies["sessionid"] + except Exception as e: + print("Unable to get session id from the platform. Error: " + str(e)) + 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 = TalosIntelligenceConnector() + 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)) + + exit(0) + + +if __name__ == "__main__": + main() diff --git a/ciscotalosintelligence_consts.py b/ciscotalosintelligence_consts.py new file mode 100644 index 0000000..6288cea --- /dev/null +++ b/ciscotalosintelligence_consts.py @@ -0,0 +1,24 @@ +# File: ciscotalosintelligence_consts.py +# +# Copyright (c) 2024 Splunk Inc. +# +# 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 +ENDPOINT_QUERY_REPUTATION_V3 = "/Talos.Service.URS/QueryReputationV3" +ENDPOINT_QUERY_TAXONOMIES = "/Talos.Service.TTS/QueryTaxonomyCatalogs" +MAX_CONNECTION_RETIRIES = 10 +MAX_REQUEST_RETRIES = 2 +MAX_REQUEST_TIMEOUT = 5 diff --git a/ciscotalosintelligence_dark.svg b/ciscotalosintelligence_dark.svg new file mode 100644 index 0000000..e3726a7 --- /dev/null +++ b/ciscotalosintelligence_dark.svg @@ -0,0 +1 @@ + diff --git a/exclude_files.txt b/exclude_files.txt new file mode 100644 index 0000000..c1c9f4d --- /dev/null +++ b/exclude_files.txt @@ -0,0 +1 @@ +.git* diff --git a/manual_readme_content.md b/manual_readme_content.md new file mode 100644 index 0000000..da744f0 --- /dev/null +++ b/manual_readme_content.md @@ -0,0 +1,25 @@ +[comment]: # " File: README.md" +[comment]: # "Copyright (c) 2024 Splunk Inc." +[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." +[comment]: # "" +## Cisco Talos Intelligence license for Splunk SOAR (Cloud) + +The Cisco Talos Intelligence license is included with your Splunk SOAR (Cloud) license. + +## Overview + +This app uses the Cisco Talos API that specializes in identifying, analyzing, and mitigating cybersecurity threats. + +For additional details, see the [Cisco Talos Intelligence article](https://docs.splunk.com/Documentation/SOAR/current/Playbook/Talos) in the Splunk SOAR documentation. + +**Note:** The Cisco Talos Intelligence asset is already configured in your Splunk SOAR (Cloud) deployment. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4c594fc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[tool.black] +line-length = 145 +target-version = ['py39'] +verbose = true + +[tool.isort] +line_length = 145 +profile = "black" diff --git a/release_notes/1.0.1.md b/release_notes/1.0.1.md new file mode 100644 index 0000000..ccf3ca9 --- /dev/null +++ b/release_notes/1.0.1.md @@ -0,0 +1,5 @@ +* Inital release of the Cisco Talos Intelligence app +* New ip_reputation action that accepts either an IPv4 or IPv6 address and gets threat intelligence data from Talos [PAPP-34630] +* New domain_reputation action that accepts a domain and gets threat intelligence data from Talos [PAPP-34630] +* New durl_reputation action that accepts an url and gets threat intelligence data from Talos [PAPP-34630] +* Error handling regarding action retries and connection management [PAPP-34631] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0abd6cc --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +beautifulsoup4==4.9.1 +cryptography==42.0.5 +httpx[http2]==0.27.2 diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..720a141 --- /dev/null +++ b/tox.ini @@ -0,0 +1,4 @@ +[flake8] +max-line-length = 145 +max-complexity = 28 +extend-ignore = F403,E128,E126,E121,E127,E731,E201,E202,E203,E701,F405,E722,D,W503 diff --git a/wheels/py3/anyio-4.6.2.post1-py3-none-any.whl b/wheels/py3/anyio-4.6.2.post1-py3-none-any.whl new file mode 100644 index 0000000..d65274a Binary files /dev/null and b/wheels/py3/anyio-4.6.2.post1-py3-none-any.whl differ diff --git a/wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl b/wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl new file mode 100644 index 0000000..080b2f8 Binary files /dev/null and b/wheels/py3/beautifulsoup4-4.9.1-py3-none-any.whl differ diff --git a/wheels/py3/exceptiongroup-1.2.2-py3-none-any.whl b/wheels/py3/exceptiongroup-1.2.2-py3-none-any.whl new file mode 100644 index 0000000..91439e5 Binary files /dev/null and b/wheels/py3/exceptiongroup-1.2.2-py3-none-any.whl differ diff --git a/wheels/py3/h11-0.14.0-py3-none-any.whl b/wheels/py3/h11-0.14.0-py3-none-any.whl new file mode 100644 index 0000000..a02c8de Binary files /dev/null and b/wheels/py3/h11-0.14.0-py3-none-any.whl differ diff --git a/wheels/py3/h2-4.1.0-py3-none-any.whl b/wheels/py3/h2-4.1.0-py3-none-any.whl new file mode 100644 index 0000000..47646f6 Binary files /dev/null and b/wheels/py3/h2-4.1.0-py3-none-any.whl differ diff --git a/wheels/py3/hpack-4.0.0-py3-none-any.whl b/wheels/py3/hpack-4.0.0-py3-none-any.whl new file mode 100644 index 0000000..e23f43a Binary files /dev/null and b/wheels/py3/hpack-4.0.0-py3-none-any.whl differ diff --git a/wheels/py3/httpcore-1.0.5-py3-none-any.whl b/wheels/py3/httpcore-1.0.5-py3-none-any.whl new file mode 100644 index 0000000..8064269 Binary files /dev/null and b/wheels/py3/httpcore-1.0.5-py3-none-any.whl differ diff --git a/wheels/py3/httpcore-1.0.6-py3-none-any.whl b/wheels/py3/httpcore-1.0.6-py3-none-any.whl new file mode 100644 index 0000000..7eccea0 Binary files /dev/null and b/wheels/py3/httpcore-1.0.6-py3-none-any.whl differ diff --git a/wheels/py3/httpx-0.27.2-py3-none-any.whl b/wheels/py3/httpx-0.27.2-py3-none-any.whl new file mode 100644 index 0000000..65d9980 Binary files /dev/null and b/wheels/py3/httpx-0.27.2-py3-none-any.whl differ diff --git a/wheels/py3/hyperframe-6.0.1-py3-none-any.whl b/wheels/py3/hyperframe-6.0.1-py3-none-any.whl new file mode 100644 index 0000000..2fd0d67 Binary files /dev/null and b/wheels/py3/hyperframe-6.0.1-py3-none-any.whl differ diff --git a/wheels/py3/pycparser-2.22-py3-none-any.whl b/wheels/py3/pycparser-2.22-py3-none-any.whl new file mode 100644 index 0000000..b478187 Binary files /dev/null and b/wheels/py3/pycparser-2.22-py3-none-any.whl differ diff --git a/wheels/py3/sniffio-1.3.1-py3-none-any.whl b/wheels/py3/sniffio-1.3.1-py3-none-any.whl new file mode 100644 index 0000000..04f44e4 Binary files /dev/null and b/wheels/py3/sniffio-1.3.1-py3-none-any.whl differ diff --git a/wheels/py3/typing_extensions-4.12.2-py3-none-any.whl b/wheels/py3/typing_extensions-4.12.2-py3-none-any.whl new file mode 100644 index 0000000..f6cc799 Binary files /dev/null and b/wheels/py3/typing_extensions-4.12.2-py3-none-any.whl differ diff --git a/wheels/py39/cffi-1.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/py39/cffi-1.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..3833652 Binary files /dev/null and b/wheels/py39/cffi-1.16.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/py39/cryptography-42.0.5-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/py39/cryptography-42.0.5-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..95420e5 Binary files /dev/null and b/wheels/py39/cryptography-42.0.5-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ diff --git a/wheels/py39/pydantic-1.10.13-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl b/wheels/py39/pydantic-1.10.13-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl new file mode 100644 index 0000000..128553c Binary files /dev/null and b/wheels/py39/pydantic-1.10.13-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl differ