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