diff --git a/README.md b/README.md index ce060452..ab3b361e 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,11 @@ See a quick demo πŸ“Ί of the process to dump a dataset [here](https://www.youtub To contribute a dataset simply create a PR on this repository, for general instructions on creating a PR [see this guide](https://gist.github.com/Chaser324/ce0505fbed06b947d962). +# TOTAL-REPLAY +A lightweight tool helps you make the most of Splunk’s [Security Content](https://github.com/splunk/security_content) metadata, such as detection names, analytic stories, and more, by replaying relevant test event logs or attack data from either the [Splunk Attack Data](https://github.com/splunk/attack_data) or [Splunk Attack Range](https://github.com/splunk/attack_range) projects. + +for more information of this tool, please refer to [TOTAL-REPLAY Guide](total_replay/readme.md) + # Automatically generated Datasets βš™οΈ This project takes advantage of automation to generate datasets using the attack_range. You can see details about this service on this [sub-project folder attack_data_service](https://github.com/splunk/attack_data/tree/master/attack_data_service). diff --git a/bin/replay.py b/bin/replay.py index dd1b0615..e705e9a4 100644 --- a/bin/replay.py +++ b/bin/replay.py @@ -10,7 +10,7 @@ from urllib3 import disable_warnings import yaml from pathlib import Path - +from urllib.parse import urlparse, urlunparse, unquote def load_environment_variables(): """Load required environment variables for Splunk connection.""" @@ -114,6 +114,70 @@ def send_data_to_splunk(file_path, splunk_host, hec_token, event_host_uuid, except Exception as e: print(f":x: Error sending {file_path} to Splunk HEC: {e}") +def parse_old_attack_yml_data_file(yml_file_path, + index_override, + source_override, + sourcetype_override, + host_uuid): + ### handling possible empty inputs + print("Processing old attack data yml file") + if source_override == "" or sourcetype_override == "" or index_override == "": + return None, [], {} + + try: + with open(yml_file_path, 'r') as file: + data = yaml.safe_load(file) + + # Extract required fields + file_id = host_uuid + d = data.get('dataset') + ### if the instance is list + if isinstance(d, list): + dataset_val = d[0] + if isinstance(d, str): + dataset_val = d + + name_value = os.path.basename(dataset_val).split(".")[0] + p = urlparse(dataset_val) + if not p.scheme or not p.netloc: + raise ValueError(f"Unsupported GitHub URL format: {dataset_val}") + + m, path_value = str(p.path).split("master") + ### generate our own datasets data + ### "datasets": [ + ### { + ### "name": "windows-sysmon_creddump", + ### "path": "/datasets/attack_techniques/T1003.001/atomic_red_team/windows-sysmon_creddump.log", + ### "sourcetype": "XmlWinEventLog", + ### "source": "XmlWinEventLog:Microsoft-Windows-Sysmon/Operational" + ### } + ### ] + # Extract required fields + + datasets = [ + { + "name": name_value, + "path": path_value, + "sourcetype":sourcetype_override, + "source":source_override + } + ] + #print(datasets) + # Extract default metadata from YAML file + default_index = index_override + default_source = source_override + default_sourcetype = sourcetype_override + + # Return tuple of (id, datasets_list, default_metadata) + return file_id, datasets, { + 'index': default_index, + 'source': default_source, + 'sourcetype': default_sourcetype + } + + except Exception as e: + print(f"Error parsing {yml_file_path}: {e}") + return None, [], {} def main(): parser = argparse.ArgumentParser( @@ -205,8 +269,12 @@ def main(): file_id, datasets, defaults = parse_data_yml(yml_file) if not file_id or not datasets: - print(f"Skipping {yml_file} - no valid data found") - continue + + file_id, datasets, defaults = parse_old_attack_yml_data_file(yml_file, args.index_override, args.source_override, args.sourcetype_override, args.host_uuid) + + if not file_id or not datasets: + print(f"Skipping {yml_file} - no valid data found") + continue # Use the id from YAML file as host field (unless user provided one) event_host_uuid = args.host_uuid or file_id diff --git a/total_replay/assets/banner.png b/total_replay/assets/banner.png new file mode 100644 index 00000000..0b5bd23d Binary files /dev/null and b/total_replay/assets/banner.png differ diff --git a/total_replay/assets/usage.png b/total_replay/assets/usage.png new file mode 100644 index 00000000..90904ad5 Binary files /dev/null and b/total_replay/assets/usage.png differ diff --git a/total_replay/configuration/config.yml b/total_replay/configuration/config.yml new file mode 100644 index 00000000..91041763 --- /dev/null +++ b/total_replay/configuration/config.yml @@ -0,0 +1,7 @@ +settings: + security_content_detection_path: ~/path/to/your/security_content/detections + attack_data_dir_path: ~/path/to/your/attack_data + output_dir_name : output + cache_replay_yaml_name : cache_replay_data.yml + replayed_yaml_cache_dir_name: replayed_yaml_cache + debug_print: False \ No newline at end of file diff --git a/total_replay/poetry.lock b/total_replay/poetry.lock new file mode 100644 index 00000000..3a3ab6a8 --- /dev/null +++ b/total_replay/poetry.lock @@ -0,0 +1,740 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "ansible-runner" +version = "2.4.2" +description = "\"Consistent Ansible Python API and CLI with container and process isolation runtime capabilities\"" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "ansible_runner-2.4.2-py3-none-any.whl", hash = "sha256:0bde6cb39224770ff49ccdc6027288f6a98f4ed2ea0c64688b31217033221893"}, + {file = "ansible_runner-2.4.2.tar.gz", hash = "sha256:331d4da8d784e5a76aa9356981c0255f4bb1ba640736efe84b0bd7c73a4ca420"}, +] + +[package.dependencies] +packaging = "*" +pexpect = ">=4.5" +python-daemon = "*" +pyyaml = "*" + +[[package]] +name = "certifi" +version = "2025.10.5" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"}, + {file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] + +[[package]] +name = "click" +version = "8.3.0" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"}, + {file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "lockfile" +version = "0.12.2" +description = "Platform-independent file locking module" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "lockfile-0.12.2-py2.py3-none-any.whl", hash = "sha256:6c3cb24f344923d30b2785d5ad75182c8ea7ac1b6171b08657258ec7429d50fa"}, + {file = "lockfile-0.12.2.tar.gz", hash = "sha256:6aed02de03cba24efabcd600b30540140634fc06cfa603822d508d5361e9f799"}, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, + {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins (>=0.5.0)"] +profiling = ["gprof2dot"] +rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "numpy" +version = "2.3.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.11" +groups = ["main"] +files = [ + {file = "numpy-2.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e78aecd2800b32e8347ce49316d3eaf04aed849cd5b38e0af39f829a4e59f5eb"}, + {file = "numpy-2.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fd09cc5d65bda1e79432859c40978010622112e9194e581e3415a3eccc7f43f"}, + {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1b219560ae2c1de48ead517d085bc2d05b9433f8e49d0955c82e8cd37bd7bf36"}, + {file = "numpy-2.3.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:bafa7d87d4c99752d07815ed7a2c0964f8ab311eb8168f41b910bd01d15b6032"}, + {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36dc13af226aeab72b7abad501d370d606326a0029b9f435eacb3b8c94b8a8b7"}, + {file = "numpy-2.3.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7b2f9a18b5ff9824a6af80de4f37f4ec3c2aab05ef08f51c77a093f5b89adda"}, + {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9984bd645a8db6ca15d850ff996856d8762c51a2239225288f08f9050ca240a0"}, + {file = "numpy-2.3.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:64c5825affc76942973a70acf438a8ab618dbd692b84cd5ec40a0a0509edc09a"}, + {file = "numpy-2.3.4-cp311-cp311-win32.whl", hash = "sha256:ed759bf7a70342f7817d88376eb7142fab9fef8320d6019ef87fae05a99874e1"}, + {file = "numpy-2.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:faba246fb30ea2a526c2e9645f61612341de1a83fb1e0c5edf4ddda5a9c10996"}, + {file = "numpy-2.3.4-cp311-cp311-win_arm64.whl", hash = "sha256:4c01835e718bcebe80394fd0ac66c07cbb90147ebbdad3dcecd3f25de2ae7e2c"}, + {file = "numpy-2.3.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ef1b5a3e808bc40827b5fa2c8196151a4c5abe110e1726949d7abddfe5c7ae11"}, + {file = "numpy-2.3.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2f91f496a87235c6aaf6d3f3d89b17dba64996abadccb289f48456cff931ca9"}, + {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:f77e5b3d3da652b474cc80a14084927a5e86a5eccf54ca8ca5cbd697bf7f2667"}, + {file = "numpy-2.3.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8ab1c5f5ee40d6e01cbe96de5863e39b215a4d24e7d007cad56c7184fdf4aeef"}, + {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77b84453f3adcb994ddbd0d1c5d11db2d6bda1a2b7fd5ac5bd4649d6f5dc682e"}, + {file = "numpy-2.3.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4121c5beb58a7f9e6dfdee612cb24f4df5cd4db6e8261d7f4d7450a997a65d6a"}, + {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:65611ecbb00ac9846efe04db15cbe6186f562f6bb7e5e05f077e53a599225d16"}, + {file = "numpy-2.3.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dabc42f9c6577bcc13001b8810d300fe814b4cfbe8a92c873f269484594f9786"}, + {file = "numpy-2.3.4-cp312-cp312-win32.whl", hash = "sha256:a49d797192a8d950ca59ee2d0337a4d804f713bb5c3c50e8db26d49666e351dc"}, + {file = "numpy-2.3.4-cp312-cp312-win_amd64.whl", hash = "sha256:985f1e46358f06c2a09921e8921e2c98168ed4ae12ccd6e5e87a4f1857923f32"}, + {file = "numpy-2.3.4-cp312-cp312-win_arm64.whl", hash = "sha256:4635239814149e06e2cb9db3dd584b2fa64316c96f10656983b8026a82e6e4db"}, + {file = "numpy-2.3.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c090d4860032b857d94144d1a9976b8e36709e40386db289aaf6672de2a81966"}, + {file = "numpy-2.3.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a13fc473b6db0be619e45f11f9e81260f7302f8d180c49a22b6e6120022596b3"}, + {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:3634093d0b428e6c32c3a69b78e554f0cd20ee420dcad5a9f3b2a63762ce4197"}, + {file = "numpy-2.3.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:043885b4f7e6e232d7df4f51ffdef8c36320ee9d5f227b380ea636722c7ed12e"}, + {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ee6a571d1e4f0ea6d5f22d6e5fbd6ed1dc2b18542848e1e7301bd190500c9d7"}, + {file = "numpy-2.3.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc8a63918b04b8571789688b2780ab2b4a33ab44bfe8ccea36d3eba51228c953"}, + {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40cc556d5abbc54aabe2b1ae287042d7bdb80c08edede19f0c0afb36ae586f37"}, + {file = "numpy-2.3.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ecb63014bb7f4ce653f8be7f1df8cbc6093a5a2811211770f6606cc92b5a78fd"}, + {file = "numpy-2.3.4-cp313-cp313-win32.whl", hash = "sha256:e8370eb6925bb8c1c4264fec52b0384b44f675f191df91cbe0140ec9f0955646"}, + {file = "numpy-2.3.4-cp313-cp313-win_amd64.whl", hash = "sha256:56209416e81a7893036eea03abcb91c130643eb14233b2515c90dcac963fe99d"}, + {file = "numpy-2.3.4-cp313-cp313-win_arm64.whl", hash = "sha256:a700a4031bc0fd6936e78a752eefb79092cecad2599ea9c8039c548bc097f9bc"}, + {file = "numpy-2.3.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:86966db35c4040fdca64f0816a1c1dd8dbd027d90fca5a57e00e1ca4cd41b879"}, + {file = "numpy-2.3.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:838f045478638b26c375ee96ea89464d38428c69170360b23a1a50fa4baa3562"}, + {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d7315ed1dab0286adca467377c8381cd748f3dc92235f22a7dfc42745644a96a"}, + {file = "numpy-2.3.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:84f01a4d18b2cc4ade1814a08e5f3c907b079c847051d720fad15ce37aa930b6"}, + {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:817e719a868f0dacde4abdfc5c1910b301877970195db9ab6a5e2c4bd5b121f7"}, + {file = "numpy-2.3.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85e071da78d92a214212cacea81c6da557cab307f2c34b5f85b628e94803f9c0"}, + {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2ec646892819370cf3558f518797f16597b4e4669894a2ba712caccc9da53f1f"}, + {file = "numpy-2.3.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:035796aaaddfe2f9664b9a9372f089cfc88bd795a67bd1bfe15e6e770934cf64"}, + {file = "numpy-2.3.4-cp313-cp313t-win32.whl", hash = "sha256:fea80f4f4cf83b54c3a051f2f727870ee51e22f0248d3114b8e755d160b38cfb"}, + {file = "numpy-2.3.4-cp313-cp313t-win_amd64.whl", hash = "sha256:15eea9f306b98e0be91eb344a94c0e630689ef302e10c2ce5f7e11905c704f9c"}, + {file = "numpy-2.3.4-cp313-cp313t-win_arm64.whl", hash = "sha256:b6c231c9c2fadbae4011ca5e7e83e12dc4a5072f1a1d85a0a7b3ed754d145a40"}, + {file = "numpy-2.3.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81c3e6d8c97295a7360d367f9f8553973651b76907988bb6066376bc2252f24e"}, + {file = "numpy-2.3.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7c26b0b2bf58009ed1f38a641f3db4be8d960a417ca96d14e5b06df1506d41ff"}, + {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:62b2198c438058a20b6704351b35a1d7db881812d8512d67a69c9de1f18ca05f"}, + {file = "numpy-2.3.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:9d729d60f8d53a7361707f4b68a9663c968882dd4f09e0d58c044c8bf5faee7b"}, + {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd0c630cf256b0a7fd9d0a11c9413b42fef5101219ce6ed5a09624f5a65392c7"}, + {file = "numpy-2.3.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5e081bc082825f8b139f9e9fe42942cb4054524598aaeb177ff476cc76d09d2"}, + {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:15fb27364ed84114438fff8aaf998c9e19adbeba08c0b75409f8c452a8692c52"}, + {file = "numpy-2.3.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:85d9fb2d8cd998c84d13a79a09cc0c1091648e848e4e6249b0ccd7f6b487fa26"}, + {file = "numpy-2.3.4-cp314-cp314-win32.whl", hash = "sha256:e73d63fd04e3a9d6bc187f5455d81abfad05660b212c8804bf3b407e984cd2bc"}, + {file = "numpy-2.3.4-cp314-cp314-win_amd64.whl", hash = "sha256:3da3491cee49cf16157e70f607c03a217ea6647b1cea4819c4f48e53d49139b9"}, + {file = "numpy-2.3.4-cp314-cp314-win_arm64.whl", hash = "sha256:6d9cd732068e8288dbe2717177320723ccec4fb064123f0caf9bbd90ab5be868"}, + {file = "numpy-2.3.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:22758999b256b595cf0b1d102b133bb61866ba5ceecf15f759623b64c020c9ec"}, + {file = "numpy-2.3.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9cb177bc55b010b19798dc5497d540dea67fd13a8d9e882b2dae71de0cf09eb3"}, + {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0f2bcc76f1e05e5ab58893407c63d90b2029908fa41f9f1cc51eecce936c3365"}, + {file = "numpy-2.3.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:8dc20bde86802df2ed8397a08d793da0ad7a5fd4ea3ac85d757bf5dd4ad7c252"}, + {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5e199c087e2aa71c8f9ce1cb7a8e10677dc12457e7cc1be4798632da37c3e86e"}, + {file = "numpy-2.3.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85597b2d25ddf655495e2363fe044b0ae999b75bc4d630dc0d886484b03a5eb0"}, + {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04a69abe45b49c5955923cf2c407843d1c85013b424ae8a560bba16c92fe44a0"}, + {file = "numpy-2.3.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e1708fac43ef8b419c975926ce1eaf793b0c13b7356cfab6ab0dc34c0a02ac0f"}, + {file = "numpy-2.3.4-cp314-cp314t-win32.whl", hash = "sha256:863e3b5f4d9915aaf1b8ec79ae560ad21f0b8d5e3adc31e73126491bb86dee1d"}, + {file = "numpy-2.3.4-cp314-cp314t-win_amd64.whl", hash = "sha256:962064de37b9aef801d33bc579690f8bfe6c5e70e29b61783f60bcba838a14d6"}, + {file = "numpy-2.3.4-cp314-cp314t-win_arm64.whl", hash = "sha256:8b5a9a39c45d852b62693d9b3f3e0fe052541f804296ff401a72a1b60edafb29"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:6e274603039f924c0fe5cb73438fa9246699c78a6df1bd3decef9ae592ae1c05"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d149aee5c72176d9ddbc6803aef9c0f6d2ceeea7626574fc68518da5476fa346"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:6d34ed9db9e6395bb6cd33286035f73a59b058169733a9db9f85e650b88df37e"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:fdebe771ca06bb8d6abce84e51dca9f7921fe6ad34a0c914541b063e9a68928b"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:957e92defe6c08211eb77902253b14fe5b480ebc5112bc741fd5e9cd0608f847"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13b9062e4f5c7ee5c7e5be96f29ba71bc5a37fed3d1d77c37390ae00724d296d"}, + {file = "numpy-2.3.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:81b3a59793523e552c4a96109dde028aa4448ae06ccac5a76ff6532a85558a7f"}, + {file = "numpy-2.3.4.tar.gz", hash = "sha256:a7d018bfedb375a8d979ac758b120ba846a7fe764911a64465fd87b8729f4a6a"}, +] + +[[package]] +name = "packaging" +version = "25.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] + +[[package]] +name = "pandas" +version = "2.3.3" +description = "Powerful data structures for data analysis, time series, and statistics" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c"}, + {file = "pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1"}, + {file = "pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250"}, + {file = "pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4"}, + {file = "pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523"}, + {file = "pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66"}, + {file = "pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791"}, + {file = "pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151"}, + {file = "pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53"}, + {file = "pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908"}, + {file = "pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98"}, + {file = "pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084"}, + {file = "pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713"}, + {file = "pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d"}, + {file = "pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c"}, + {file = "pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493"}, + {file = "pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5"}, + {file = "pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78"}, + {file = "pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86"}, + {file = "pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0"}, + {file = "pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c"}, + {file = "pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6"}, + {file = "pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3"}, + {file = "pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec"}, + {file = "pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450"}, + {file = "pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788"}, + {file = "pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c503ba5216814e295f40711470446bc3fd00f0faea8a086cbc688808e26f92a2"}, + {file = "pandas-2.3.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a637c5cdfa04b6d6e2ecedcb81fc52ffb0fd78ce2ebccc9ea964df9f658de8c8"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:854d00d556406bffe66a4c0802f334c9ad5a96b4f1f868adf036a21b11ef13ff"}, + {file = "pandas-2.3.3-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf1f8a81d04ca90e32a0aceb819d34dbd378a98bf923b6398b9a3ec0bf44de29"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:23ebd657a4d38268c7dfbdf089fbc31ea709d82e4923c5ffd4fbd5747133ce73"}, + {file = "pandas-2.3.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5554c929ccc317d41a5e3d1234f3be588248e61f08a74dd17c9eabb535777dc9"}, + {file = "pandas-2.3.3-cp39-cp39-win_amd64.whl", hash = "sha256:d3e28b3e83862ccf4d85ff19cf8c20b2ae7e503881711ff2d534dc8f761131aa"}, + {file = "pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b"}, +] + +[package.dependencies] +numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} +python-dateutil = ">=2.8.2" +pytz = ">=2020.1" +tzdata = ">=2022.7" + +[package.extras] +all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] +aws = ["s3fs (>=2022.11.0)"] +clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] +compression = ["zstandard (>=0.19.0)"] +computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] +consortium-standard = ["dataframe-api-compat (>=0.1.7)"] +excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] +feather = ["pyarrow (>=10.0.1)"] +fss = ["fsspec (>=2022.11.0)"] +gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] +hdf5 = ["tables (>=3.8.0)"] +html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] +mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] +output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] +parquet = ["pyarrow (>=10.0.1)"] +performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] +plot = ["matplotlib (>=3.6.3)"] +postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] +pyarrow = ["pyarrow (>=10.0.1)"] +spss = ["pyreadstat (>=1.2.0)"] +sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] +test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] +xml = ["lxml (>=4.9.2)"] + +[[package]] +name = "pexpect" +version = "4.9.0" +description = "Pexpect allows easy control of interactive console applications." +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, + {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, +] + +[package.dependencies] +ptyprocess = ">=0.5" + +[[package]] +name = "ptyprocess" +version = "0.7.0" +description = "Run a subprocess in a pseudo terminal" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, + {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, +] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "python-daemon" +version = "3.1.2" +description = "Library to implement a well-behaved Unix daemon process." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "python_daemon-3.1.2-py3-none-any.whl", hash = "sha256:b906833cef63502994ad48e2eab213259ed9bb18d54fa8774dcba2ff7864cec6"}, + {file = "python_daemon-3.1.2.tar.gz", hash = "sha256:f7b04335adc473de877f5117e26d5f1142f4c9f7cd765408f0877757be5afbf4"}, +] + +[package.dependencies] +lockfile = ">=0.10" + +[package.extras] +build = ["build", "changelog-chug", "docutils", "python-daemon[doc]", "wheel"] +devel = ["python-daemon[dist,test]"] +dist = ["python-daemon[build]", "twine"] +static-analysis = ["isort (>=5.13,<6.0)", "pip-check", "pycodestyle (>=2.12,<3.0)", "pydocstyle (>=6.3,<7.0)", "pyupgrade (>=3.17,<4.0)"] +test = ["coverage", "python-daemon[build,static-analysis]", "testscenarios (>=0.4)", "testtools"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2025.2" +description = "World timezone definitions, modern and historical" +optional = false +python-versions = "*" +groups = ["main"] +files = [ + {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, + {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "requests" +version = "2.32.5" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, + {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset_normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "14.2.0" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.8.0" +groups = ["main"] +files = [ + {file = "rich-14.2.0-py3-none-any.whl", hash = "sha256:76bc51fe2e57d2b1be1f96c524b890b816e334ab4c1e45888799bfaab0021edd"}, + {file = "rich-14.2.0.tar.gz", hash = "sha256:73ff50c7c0c1c77c8243079283f4edb376f0f6442433aecb8ce7e6d0b92d1fe4"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + +[[package]] +name = "typer" +version = "0.20.0" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a"}, + {file = "typer-0.20.0.tar.gz", hash = "sha256:1aaf6494031793e4876fb0bacfa6a912b551cf43c1e63c800df8b1a866720c37"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "tzdata" +version = "2025.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +groups = ["main"] +files = [ + {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, + {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, +] + +[[package]] +name = "urllib3" +version = "2.5.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.13" +content-hash = "438fb875fc4a98920779388968b0ec93cae8aa51de6918102b2856ee427a0fe0" diff --git a/total_replay/pyproject.toml b/total_replay/pyproject.toml new file mode 100644 index 00000000..51694eb7 --- /dev/null +++ b/total_replay/pyproject.toml @@ -0,0 +1,24 @@ +[project] +name = "total-replay" +version = "0.1.0" +description = "Attack Data Replay Amplifier" +authors = [ + {name = "Teoderick Contreras",email = "tcontreras@splunk.com"} +] +license = {text = "Apache License"} +requires-python = ">=3.13" +dependencies = [ + "rich (>=14.2.0,<15.0.0)", + "typer (>=0.20.0,<0.21.0)", + "pyyaml (>=6.0.3,<7.0.0)", + "requests (>=2.32.5,<3.0.0)", + "urllib3 (>=2.5.0,<3.0.0)", + "pandas (>=2.3.3,<3.0.0)", + "colorama (>=0.4.6,<0.5.0)", + "ansible-runner (>=2.4.2,<3.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/total_replay/readme.md b/total_replay/readme.md new file mode 100644 index 00000000..dd162976 --- /dev/null +++ b/total_replay/readme.md @@ -0,0 +1,174 @@ +# TOTAL-REPLAY + +![TOTAL-REPLAY](assets/banner.png) + +## Description + +This lightweight tool helps you make the most of Splunk’s [Security Content](https://github.com/splunk/security_content) metadata, such as detection names, analytic stories, and more, by replaying relevant test event logs or attack data from either the [Splunk Attack Data](https://github.com/splunk/attack_data) or [Splunk Attack Range](https://github.com/splunk/attack_range) projects. + +## Installation + +### MAC/LINUX: +--- + +1. Clone the Splunk Attack Data github repo. We recommend to follow this steps [Attack Data Getting Started](https://github.com/splunk/attack_data/). + +2. Clone the Splunk Security Content github repo. We recommend to follow this steps [Security Content Getting Started](https://github.com/splunk/security_content). + +3. Install Poetry (if not already installed) +``` +curl -sSL https://install.python-poetry.org/ | python3 - +``` +4. Navigate to your project directory +``` +cd /path/to/your/total-replay-project +``` +5. Create a virtual environment and activate it +``` +poetry shell +``` +6. Install project dependencies +``` +poetry install +``` +7. In total_replay->configuration->config.yml, add the folder path of the Splunk Attack Data repo and the detection folder path in Splunk Security Content. + +``` +settings: + security_content_detection_path: ~/path/to/your/security_content/detections + attack_data_dir_path: ~/path/to/your/attack_data +``` +8. enable the `attack_data_version_on` config setting in total_replay->configuration->config.yml: + + **NOTE: You can only enable either one of the `attack_range_version_on` or `attack_data_version_on` settings of TOTAL-REPLAY** +``` +attack_data_version_on: True +``` + +9. make sure you setup the required environment variables for splunk server connection + + | Environment Variables. | Description | + |----------------------------|-------------------------| + | **SPLUNK_HOST** | SPLUNK HOST IP ADDRESS | + | **SPLUNK_HEC_TOKEN** | SPLUNK SERVER HEC TOKEN | + + you can use the `export` commandline function for adding these environment variables + + ``` + export SPLUNK_HOST= + export SPLUNK_HEC_TOKEN= + ``` + +10. Make sure HEC token is set to "Enabled" in Splunk server (Settings β†’ Data Inputs β†’ HTTP Event Collector). + +11. Confirm the HEC listener port is enabled, typically 8088, using HTTPS. + +12. Update your firewall settings to allow inbound connections on port 8088, otherwise your data sender will not be able to reach Splunk. + +### Windows OS: + +We recommend using the Windows Subsystem for Linux (WSL). You can find a tutorial [here](https://learn.microsoft.com/en-us/windows/wsl/install). After installing WSL, you can follow the steps described in the Linux section. + + +### OPTIONAL: +- You can toggle the `debug_print` configuration setting of TOTAL-REPLAY to disable or enable debug print during execution. + + +## Usage + +![TOTAL-REPLAY-USAGE](assets/usage.png) + +### Features + +A. This tool accepts the following types of metadata as input: + + - **Splunk detection names** + - **MITRE ATT&CK tactic and technique IDs** + - **Splunk detection GUIDs** + - **Analytic stories** + + It then uses these inputs to identify and replay the attack data associated with them. + + Example A - Replay Attack Data via Splunk detection name: + + ``` + python3 total_replay.py -n '7zip CommandLine To SMB Share Path, CMLUA Or CMSTPLUA UAC Bypass' + ``` + + + + +B. For automation, you can also provide a simple .txt file. +For example: + +**test.txt**: + +``` +wsreset_uac_bypass.yml +wscript_or_cscript_suspicious_child_process.yml +windows_user_deletion_via_net.yml +Windows User Disabled Via Net +Windows Chromium Browser No Security Sandbox Process +004e32e2-146d-11ec-a83f-acde48001122 +01d29b48-ff6f-11eb-b81e-acde48001123 +#T1014 +T1589.001 +Amos Stealer +PromptLock +f64579c0-203f-11ec-abcc-acde48001122 +004e32e2-146d-11ec-a83f-acde48001122 +``` + +This file can contain any mix of Security Content metadata you want to replay. +From there, you can choose whether to replay only detection GUIDs, only analytic stories, or all entries using the tool’s greedy replay feature. + +C. TOTAL-REPLAY downloads the required Attack Data each time you execute or replay data during detection testing or development. To help reduce disk space usage, the tool generates a cached .yml file for every downloaded dataset. You can then use the `local_data_path` parameter to replay the cached data, allowing you to avoid downloading the same Attack Data again. + +### Other + +For replaying captured datasets or event logs during detection development or testing outside of the Splunk Security Content or Splunk Attack Data GitHub repositories, we recommend using the built-in replay.py feature provided by either Splunk Attack Range or Attack Data. + +If you have multiple datasets to replay and your metadata matches the format required by TOTAL-REPLAY for caching downloaded datasets, you can recreate that format and use the `local_data_path` feature to replay the data directly from the cache. + +Below is an example of the cached .yml file generated by TOTAL-REPLAY after replaying datasets: + +``` +analytic_story: +- Ransomware +attack_data_link: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/malware/conti/conti_leak/windows-sysmon_7z.log +attack_data_output_file_path: /Users/tecontre/Research/lab/attack_range/total_replay/output/2025-11-18/detection_name_replay_c2ffd320-bcd3-4d88-9d12-57bdd30f6545/01d29b48-ff6f-11eb-b81e-acde48001123/windows-sysmon_7z.log +attack_data_source: XmlWinEventLog:Microsoft-Windows-Sysmon/Operational +attack_data_sourcetype: XmlWinEventLog +description: The following analytic detects the execution of 7z or 7za processes with + command lines pointing to SMB network shares. It leverages data from Endpoint Detection + and Response (EDR) agents, focusing on process names and command-line arguments. + This activity is significant as it may indicate an attempt to archive and exfiltrate + sensitive files to a network share, a technique observed in CONTI LEAK tools. If + confirmed malicious, this behavior could lead to data exfiltration, compromising + sensitive information and potentially aiding further attacks. +id: 01d29b48-ff6f-11eb-b81e-acde48001123 +mitre_attack_id: +- T1560.001 +name: 7zip CommandLine To SMB Share Path + +``` + +## Author + +* [Teoderick Contreras](https://www.linkedin.com/in/teoderickc/) + +## License + +Copyright 2025 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. \ No newline at end of file diff --git a/total_replay/test/test_names.txt b/total_replay/test/test_names.txt new file mode 100644 index 00000000..c3e9d07d --- /dev/null +++ b/total_replay/test/test_names.txt @@ -0,0 +1,13 @@ +PromptLock +Amos Stealer +wscript_or_cscript_suspicious_child_process.yml +wmic_xsl_execution_via_url.yml +windows_windbg_spawning_autoit3.yml +Linux Auditd Change File Owner To Root +f64579c0-203f-11ec-abcc-acde48001122 +004e32e2-146d-11ec-a83f-acde48001122 +01d29b48-ff6f-11eb-b81e-acde48001123 +6bc5243e-ef36-45dc-9b12-f4a6be131159 +453a6b0f-b0ea-48fa-9cf4-20537ffdd22c +T1036.005 +#T1014 diff --git a/total_replay/total_replay.py b/total_replay/total_replay.py new file mode 100644 index 00000000..3f31a393 --- /dev/null +++ b/total_replay/total_replay.py @@ -0,0 +1,182 @@ +""" +author: Teoderick Contreras +description: a simple attack range utility tool designed to replay attack data via several category: + - analytic story + - detection name + - detection guid id + - Mitre attack Technique ID +""" + +import typer +import os +import sys +import yaml +from rich.live import Live +from rich.layout import Layout +from rich.panel import Panel +from time import sleep +import uuid +from colorama import Fore, Back, Style, init +from utility.color_print import ColorPrint + +from utility.utility_helper import UtilityHelper + + +def main(): + + uh = UtilityHelper() + + uh.clear_screen() + + uh.show_banner() + + app = typer.Typer() + + @app.command() + def input_helps( + detection_name: str = typer.Option(None, "--name", "-n", + help="comma-separated list of security content detection name.\n\n "), + technique_id: str = typer.Option(None, "--technique_id", "-tid", + help="comma-separated list of security content detection mitre technique id.\n\n "), + guid: str = typer.Option(None, "--guid", "-g", + help="comma-separated list of security content detection guid.\n\n "), + analytic_story: str = typer.Option(None, "--analytic_story", "-as", + help="comma-separated list of security content analytic story.\n\n "), + local_data_path: str = typer.Option(None, "--local_data_path", "-ld", + help="directory path of the cache directory generated by echo-attack during download of logs.\n\n "), + + file_detection_name: str = typer.Option(None, "--file_detection_name", "-fn", + help="file path that contains security content detection names.\n\n "), + file_detection_tid: str = typer.Option(None, "--file_detection_tid", "-ftid", + help="file path that contains security content detection mitre technique id.\n\n "), + file_detection_guid: str = typer.Option(None, "--file_detection_guid", "-fg", + help="file path that contains security content detection guid.\n\n "), + file_detection_analytic_story: str = typer.Option(None, "--file_detection_analytic_story", "-fas", + help="file path that contains security content detection analytic story.\n\n "), + file_detection_greedy: str = typer.Option(None, "--file_detection_greedy", "-fgr", + help="file path that contains security content detection analytic story, technique ID, guid and detection name.\n\n "), + index_value: str = typer.Option("test", "--index", "-i", + help="Index to replay the attack data. Set this first if you want to use a different index.\n\n ", hidden=True), + ): + + + + ### via console + if detection_name: + ColorPrint.print_yellow_fg("[+][. INFO]: Searching For both Security Content Detection Name and .YML Filename ...") + normalized_list_args = uh.normalized_args_tolist(detection_name) + generated_guid = uuid.uuid4() + marker_uid = "detection_name_replay_" + str(generated_guid) + uh.process_replay_attack_data_by_file_name("file_name", normalized_list_args, index_value, marker_uid) + uh.process_replay_attack_data("name", normalized_list_args, index_value, marker_uid) + + elif guid: + ColorPrint.print_yellow_fg("[+][. INFO]: Searching For Security Content Detection GUID ...") + normalized_list_args = uh.normalized_args_tolist(guid) + generated_guid = uuid.uuid4() + marker_uid = "guid_replay_" + str(generated_guid) + uh.process_replay_attack_data("id", normalized_list_args, index_value, marker_uid) + + elif technique_id: + ColorPrint.print_yellow_fg("[+][. INFO]: Searching For Security Content Detection Mitre ATT&CK Technique ID ...") + normalized_list_args = uh.normalized_args_tolist(technique_id) + generated_guid = uuid.uuid4() + marker_uid = "technique_id_replay_" + str(generated_guid) + uh.process_replay_attack_data("mitre_attack_id", normalized_list_args, index_value, marker_uid) + + elif analytic_story: + ColorPrint.print_yellow_fg("[+][. INFO]: Searching For Security Content Analytic Story ...") + normalized_list_args = uh.normalized_args_tolist(analytic_story) + generated_guid = uuid.uuid4() + marker_uid = "analytic_story_replay_" + str(generated_guid) + uh.process_replay_attack_data("analytic_story", normalized_list_args, index_value, marker_uid) + + ### by file input + elif file_detection_name: + segregate = uh.normalized_file_args(file_detection_name) + generated_guid = uuid.uuid4() + marker_uid = "file_detection_name_replay_" + str(generated_guid) + + if "detection_filename" in segregate: + normalized_list_args = segregate['detection_filename'] + uh.process_replay_attack_data_by_file_name("file_name", normalized_list_args, index_value, marker_uid) + + if "detection_and_analytic_story_name" in segregate: + normalized_list_args = segregate['detection_and_analytic_story_name'] + uh.process_replay_attack_data("name", normalized_list_args, index_value, marker_uid) + + elif file_detection_tid: + segregate = uh.normalized_file_args(file_detection_tid) + + if "mitre_attack_tid" in segregate: + normalized_list_args = segregate['mitre_attack_tid'] + generated_guid = uuid.uuid4() + marker_uid = "file_detection_tid_replay_" + str(generated_guid) + uh.process_replay_attack_data("mitre_attack_id", normalized_list_args, index_value, marker_uid) + + elif file_detection_guid: + segregate = uh.normalized_file_args(file_detection_guid) + + if "guid" in segregate: + normalized_list_args = segregate['guid'] + generated_guid = uuid.uuid4() + marker_uid = "file_detection_guid_replay_" + str(generated_guid) + uh.process_replay_attack_data("id", normalized_list_args, index_value, marker_uid) + + elif file_detection_analytic_story: + + segregate = uh.normalized_file_args(file_detection_analytic_story) + + if "detection_and_analytic_story_name" in segregate: + normalized_list_args = segregate['detection_and_analytic_story_name'] + generated_guid = uuid.uuid4() + marker_uid = "file_detection_analytic_story_replay_" + str(generated_guid) + uh.process_replay_attack_data("analytic_story", normalized_list_args, index_value, marker_uid) + + elif file_detection_greedy: + segregate = uh.normalized_file_args(file_detection_greedy) + generated_guid = uuid.uuid4() + marker_uid = "file_detection_greedy_replay_" + str(generated_guid) + + if "detection_filename" in segregate: + ColorPrint.print_info_fg("[+] greedy replay... [detection_filename]") + normalized_list_args = segregate['detection_filename'] + uh.process_replay_attack_data_by_file_name("file_name", normalized_list_args, index_value, marker_uid) + + if "detection_and_analytic_story_name" in segregate: + ColorPrint.print_info_fg("[+] greedy replay... [detection_and_analytic_story_name]") + normalized_list_args = segregate['detection_and_analytic_story_name'] + uh.process_replay_attack_data("name", normalized_list_args, index_value, marker_uid) + uh.process_replay_attack_data("analytic_story", normalized_list_args, index_value, marker_uid) + + if "mitre_attack_id" in segregate: + ColorPrint.print_info_fg("[+] greedy replay... [mitre_attack_id]") + normalized_list_args = segregate['mitre_attack_id'] + uh.process_replay_attack_data("mitre_attack_id", normalized_list_args, index_value, marker_uid) + + if "guid" in segregate: + ColorPrint.print_info_fg("[+] greedy replay... [mitre_attack_id]") + normalized_list_args = segregate['guid'] + uh.process_replay_attack_data("id", normalized_list_args, index_value, marker_uid) + + + elif local_data_path: + uh.process_local_yaml_cache(local_data_path, index_value) + + + else: + ColorPrint.print_error_fg("[+] [STATUS]: [ERROR] invalid inputs\n") + + ColorPrint.print_success_fg("Thank you... <(^_^)>") + + app() + + + + + return + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/total_replay/utility/color_print.py b/total_replay/utility/color_print.py new file mode 100644 index 00000000..6522693e --- /dev/null +++ b/total_replay/utility/color_print.py @@ -0,0 +1,79 @@ +from colorama import Fore, Back, Style, init +import datetime +init(autoreset=True) + +class ColorPrint: + """ + description: small Utility class for colored printing to the console. tested in macos terminal + by. tccontre18 ~ Br3akp0int + """ + + + @staticmethod + def print_cyan_fg(msg:str)->None: + print(Fore.CYAN + msg) + return + + @staticmethod + def print_red_fg(msg:str)->None: + print(Fore.RED + msg) + return + + @staticmethod + def print_green_fg(msg:str)->None: + print(Fore.GREEN + msg) + return + + @staticmethod + def print_yellow_fg(msg:str)->None: + print(Fore.YELLOW + msg) + return + + @staticmethod + def print_blue_fg(msg:str)->None: + print(Fore.BLUE + msg) + return + + @staticmethod + def print_magenta_fg(msg:str)->None: + print(Fore.MAGENTA + msg) + return + + @staticmethod + def print_bold_style(msg:str)->None: + print(Style.BRIGHT + msg) + return + + @staticmethod + def print_dim_style(msg:str)->None: + print(Style.DIM + msg) + return + + @staticmethod + def print_normal_style(msg:str)->None: + print(Style.NORMAL + msg) + return + + @staticmethod + def print_info_fg(msg:str)->None: + timestamp = f"[ {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ]" + print(Fore.MAGENTA + " "* 3 + timestamp+ "==> " + Style.RESET_ALL + Fore.CYAN + msg) + return + + @staticmethod + def print_warning_fg(msg:str)->None: + timestamp = f"[ {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ]" + print(Fore.MAGENTA + " "* 3 + timestamp+ "==> " + Style.RESET_ALL + Fore.YELLOW + msg) + return + + @staticmethod + def print_error_fg(msg:str)->None: + timestamp = f"[ {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ]" + print(Fore.MAGENTA + " "* 3 + timestamp+ "==> " + Style.RESET_ALL + Fore.RED + msg) + return + + @staticmethod + def print_success_fg(msg:str)->None: + timestamp = f"[ {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ]" + print(Fore.MAGENTA + " "* 3 + timestamp+ "==> " + Style.RESET_ALL + Fore.LIGHTGREEN_EX + msg) + return \ No newline at end of file diff --git a/total_replay/utility/utility_helper.py b/total_replay/utility/utility_helper.py new file mode 100644 index 00000000..552d1671 --- /dev/null +++ b/total_replay/utility/utility_helper.py @@ -0,0 +1,719 @@ +""" +author: Teoderick Contreras +description: a utility class for several helper functions for echo-attack utility tool: + - analytic story + - detection name + - detection guid id + - Mitre attack Technique ID + - detection name list in a file +""" + +import os +import sys +import yaml +import platform +from pathlib import Path +from colorama import Fore, Back, Style, init +import datetime +import json +import requests +import subprocess +import typer +import uuid +import re +import urllib +from collections import defaultdict +from urllib.parse import urlparse, urlunparse, unquote +from urllib3 import disable_warnings +from utility.color_print import ColorPrint +import json + +class UtilityHelper: + + def __init__(self): + self.curdir = os.getcwd() + self.config_file_path = os.path.join(self.curdir, "configuration", "config.yml") + self.home_path = Path.home() + self.processed_attack_data_uuid = [] + self.header_val = "" + return + + def get_header_val(self): + return self.header_val + ############################################# + ### config helper functions + ############################################# + def get_config_file_path(self)->str: + return self.config_file_path + + def load_config(self)->str: + with open(self.get_config_file_path(), "r") as file: + return yaml.safe_load(file) + + def read_config_settings(self, setting_field:str, key_tag="settings")->str: + cfg = self.load_config() + config_field = cfg[key_tag][setting_field] + return config_field + + ############################################# + ### helper functions + ############################################# + def clear_screen(self)->None: + """Clear the console screen based on the operating system.""" + if platform.system() == "Windows": + os.system("cls") ## Windows + else: + os.system("clear") ## macOS/Linux + + return + + def get_time_stamps(self)->str: + """get the current timestamps""" + timestamps = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + return + + def show_banner(self)->None: + + banner = r""" + ________________ + |'-.--._ _________: + | / | __ __\ + | | _ | [\_\= [\_\_ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•— + | |.' '. \...........| β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•—β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•”β• β•šβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ•— + | ( <) ||: β–ˆ β–ˆ β–ˆ β–ˆ :|_ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β•šβ–ˆβ–ˆβ•—β•šβ–ˆβ–ˆβ•— + \ '._.'β–ˆ| :.....: |_(o β–ˆ β–ˆ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ•β•β•β•β•β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β•β•β• β–ˆβ–ˆβ•”β•β•β•β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•”β• + '-\_ \ .------./ β–ˆβ–ˆβ•‘ β•šβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•”β• β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•”β• + _ \ ||.---.|| _ β•šβ•β• β•šβ•β•β•β•β•β• β•šβ•β• β•šβ•β• β•šβ•β•β•šβ•β•β•β•β•β•β• β•šβ•β• β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β• β•šβ•β• + / \ '-._|/|x~~|x | \ + / \ '-._|/|x~~|x | \ by: @tccontre18 - Splunk Threat Research Team [STRT] + (| []=.--[===[()]===[) | β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•β•— + <\_/ \_______/ _.' /_/. β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• + /// (_/_/. + |\\ [\\. + ||:| | I| + :/\: ([]) + ([]) [| + || |\_ + _/_\_ [ -'-.__ + snd <] \> \_____.> + \__/ ___Br3akp0int_] + + """ + self.clear_screen() + + ColorPrint.print_cyan_fg(banner) + + return + + def header_divider(self, header:str, tag:str="...")->str: + timestamp = f"[ {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ]==> [+][. TOTAL-REPLAY: {tag}]: ..." + header_div = f""" + ╔══════════════════════════════════════════════════════════════════ [ {header} ] ══════════════════════════════════════════════════════════════════ + β•‘ + β•šβ•{timestamp} + """ + return header_div + + def footer_divider(self,footer:str): + timestamp = f"[ {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} ]==> [+][. END]: ..." + + footer_div = f""" + ╔═{timestamp} + β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β• [ {footer} ] ═══════════════════════════════════════════════════════════════════ + """ + return footer_div + + def load_environment_variables(self)->dict: + """Load required environment variables for Splunk connection.""" + ColorPrint.print_info_fg("[+][. INFO]: ... Checking SPLUNK HOST and HEC_TOKEN ENV VARIABLE ...") + required_vars = ['SPLUNK_HOST', 'SPLUNK_HEC_TOKEN'] + env_vars = {} + for var in required_vars: + value = os.environ.get(var) + if not value: + ColorPrint.print_error_fg(f"[-][. ERROR]: ... Environment variable {var} is required but not set") + return {} + #raise ValueError(f"[-][. ERROR]: Environment variable {var} is required but not set") + env_vars[var.lower().replace('splunk_', '')] = value + return env_vars + + def normalized_args_tolist(self, input_args:str)->list: + return [i.strip() for i in input_args.split(',')] + + def parse_needed_detection_name(self, normalized_args_list:list)->list: + + ColorPrint.print_info_fg(f'[+][. INFO]: ... Enumerating detection .yml file name ... []') + security_content_dir_path = self.read_config_settings("security_content_detection_path") + + if not os.path.isdir(os.path.expanduser(security_content_dir_path)): + ColorPrint.print_error_fg("[+][. ERROR]: The security Content folder path in config is invalid or not exist.") + return + + search_count= 0 + search_found_list = [] + found_flag = False + needed_replay_yaml_field = {} + for roots, dirs, files in os.walk(os.path.expanduser(security_content_dir_path)): + + ### skip deprecated directories + if dirs == "deprecated": + continue + + if found_flag: + break + + ### enumerate the files in the directory + for file in files: + + if file.lower() in [fn.lower() for fn in normalized_args_list]: + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... SEARCH FOUND -> [ {file} ]") + file_path = os.path.join(roots, file) + + yaml_data = self.read_yaml_file(file_path) + + ### check if file content is empty or invalid + if yaml_data == None: + ColorPrint.print_warning_fg(f"[!][WARNING]: ... Skipping empty or invalid YAML file: {file_path}") + continue + needed_replay_yaml_field = self.create_metadata_cache(yaml_data) + search_found_list.append(needed_replay_yaml_field) + + search_count+=1 + if search_count == len(normalized_args_list): + found_flag = True + break + + ColorPrint.print_info_fg(f"[+][. INFO]: ... Total filtered detections: {len(search_found_list)} ") + ColorPrint.print_info_fg(f"[+][. INFO]: ... ") + ColorPrint.print_info_fg(f"[+][. INFO]: ... ") + + return search_found_list + + def process_replay_attack_data_by_file_name(self, tag:str, normalized_args_list:list, index_value:str, generated_guid:str)->None: + """main function in replaying attack data base on Splunk detection filename""" + + ColorPrint.print_cyan_fg(self.header_divider("TOTAl-REPLAY-ACTIVATED", tag)) + + search_found_list=[] + search_found_list = self.parse_needed_detection_name(normalized_args_list) + + ### generate uid for caching purposes of replay data + + for ctr, needed_replay_yaml_field in enumerate(search_found_list): + + ### get the attack data url link + if "attack_data_link" in needed_replay_yaml_field: + attack_data_link = needed_replay_yaml_field["attack_data_link"] + else: + continue + + ### generate replay yaml output folder + output_base_dir = os.path.join(self.curdir, self.read_config_settings('output_dir_name')) + self.generate_output_dir(output_base_dir) + + attack_data_timestamp_dir_path = os.path.join(output_base_dir, datetime.date.today().strftime("%Y-%m-%d")) + escu_detection_guid = needed_replay_yaml_field['id'] + + ColorPrint.print_info_fg(f"[+][. INFO]: ... Downloading attack data for: {needed_replay_yaml_field['name']} ... item:{ctr+1}") + attack_datasets_full_path, attack_datasets_path = self.download_via_attack_data(attack_data_link, attack_data_timestamp_dir_path, generated_guid) + + ### update the total-replay cache yaml file + needed_replay_yaml_field['attack_data_output_file_path'] = attack_datasets_full_path + + + needed_replay_yaml_field = self.locate_associated_attack_data_yaml_file(attack_data_link, attack_datasets_path, needed_replay_yaml_field) + if not needed_replay_yaml_field: + return + + ### dropped the replay yaml cache + replayed_yaml_cache_path = os.path.join(attack_data_timestamp_dir_path, generated_guid, self.read_config_settings('replayed_yaml_cache_dir_name')) + self.generate_output_dir(replayed_yaml_cache_path) + + cache_replay_yaml_file_path = os.path.join(replayed_yaml_cache_path, needed_replay_yaml_field['id']+ "_" + self.read_config_settings('cache_replay_yaml_name')) + self.dump_yaml_file(cache_replay_yaml_file_path, needed_replay_yaml_field) + + ### comment me if you dont want to see the replay_cache yml file + if self.read_config_settings('debug_print'): + ColorPrint.print_yellow_fg(Style.DIM + self.header_divider("TOTAL-REPLAY CACHE YAML FILE", tag)) + ColorPrint.print_yellow_fg(Style.DIM + f"[+] ... \n{json.dumps(needed_replay_yaml_field, indent=4)}") + ColorPrint.print_yellow_fg(Style.DIM + self.footer_divider("TOTAL-REPLAY CACHE YAML FILE")) + + self.processed_attack_data_uuid.append(needed_replay_yaml_field['attack_data_uuid']) + + + try: + self.attack_data_replay_cmd(needed_replay_yaml_field, index_value) + except: + raise ValueError(f"[+][. ERROR]: ... Attack Data Replay Exception!") + + + + ColorPrint.print_magenta_fg("\n[+]" + "β–ˆ" * 160 + "\n") + + ColorPrint.print_cyan_fg(self.footer_divider("TOTAl-REPLAY-ACTIVATED")) + + return + + def dump_yaml_file(self, file_path:str, data:str): + + ### generate cache data + with open(file_path, "w") as f: + yaml.dump(data, f, default_flow_style=False) + + return + + def generate_output_dir(self, dir_name:str): + """generate the base output folder for total-replay cache data""" + + ### if not exist create output directory + if not os.path.isdir(dir_name): + os.makedirs(dir_name, exist_ok=True) + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... {dir_name} folder created!") + return + + def download_via_attack_data(self, attack_data_link:str, attack_data_timestamp_dir_path:str, generated_guid:str)->tuple: + """download needed raw attack data via attack data feature""" + + ### generate a unique guid folder path + guid_dir_path = os.path.join(attack_data_timestamp_dir_path, generated_guid) + self.generate_output_dir(guid_dir_path) + + ### verify if the string contain url scheme + p = urlparse(attack_data_link) + if not p.scheme or not p.netloc: + raise ValueError(f"[-][. ERROR]: ... Unsupported GitHub URL format: {attack_data_link}") + + m, datasets_path = str(p.path).split("master/") + + ### locate the attack_data path in config + if not os.path.isdir(os.path.expanduser(self.read_config_settings('attack_data_dir_path'))): + ColorPrint.print_error_fg("[+][. ERROR]: The attack data folder path in config is invalid or not exist.") + return {} + else: + attack_datasets_full_path = os.path.join(os.path.expanduser(self.read_config_settings('attack_data_dir_path')), datasets_path) + if os.path.isfile(attack_datasets_full_path): + ColorPrint.print_info_fg(f"[+][. INFO]: ... Attack data at: {attack_datasets_full_path} already exists. Download skipped.") + return (attack_datasets_full_path, datasets_path) + + # Find the Git repository root + repo_root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"],text=True).strip() + + git_lfs_cmd = ["git", "lfs", "pull", f"--include={datasets_path}"] + ColorPrint.print_info_fg(f"[+][. INFO]: ... command: {" ".join(git_lfs_cmd)}") + + ### execute the git command process + try: + result = subprocess.run(git_lfs_cmd, check=True, cwd=repo_root, capture_output=True, text=True) + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... Git Command succeeded! attack data: {datasets_path} ==> downloaded succesfully!") + except subprocess.CalledProcessError as e: + ColorPrint.print_error_fg("[-][. ERROR]: ... Command failed!") + ColorPrint.print_error_fg("[-][. ERROR]: ... Exit code:", e.returncode) + ColorPrint.print_error_fg("[-][. ERROR]: ... Stdout:", e.stdout) + ColorPrint.print_error_fg("[-][. ERROR]: ... Stderr:", e.stderr) + + + return (attack_datasets_full_path, datasets_path) + + + def read_yaml_file(self, file_path:str)->dict: + """Read a YAML file and return its content as a dictionary.""" + try: + with open(file_path, "r", encoding="utf-8") as file: + return yaml.safe_load(file) + except FileNotFoundError: + ColorPrint.print_error_fg(f"[!] [ERROR]: File not found: {file_path}") + return None + except PermissionError: + ColorPrint.print_error_fg(f"[!] [STATUS]: error ... permission denied: {file_path}") + return None + except yaml.YAMLError as e: + ColorPrint.print_error_fg(f"[!] [ERROR]: Error reading YAML file {file_path}: {e}") + return None + + def locate_associated_attack_data_yaml_file(self, attack_data_link:str, attack_datasets_path:str, needed_yaml_field_cache:dict)->dict: + + ### check and parsed the yml file associated with the attack data + yml_name = os.path.basename(os.path.dirname(unquote(urlparse(attack_data_link).path))) + + ### locate the attack_data path in config + if not os.path.isdir(os.path.expanduser(self.read_config_settings('attack_data_dir_path'))): + ColorPrint.print_error_fg("[+][. ERROR]: The attack data folder path in config is invalid or not exist.") + return {} + + attack_data_full_dir_path = os.path.join(os.path.expanduser(self.read_config_settings('attack_data_dir_path')), os.path.dirname(attack_datasets_path)) + + ### enumerate all yaml file inside the attack data folder base on the attack_data_link in escu + for file in os.listdir(attack_data_full_dir_path): + if file.endswith((".yml", ".yaml")): + yml_file_path = os.path.join(attack_data_full_dir_path, file) + attack_data_yaml_buff = self.read_yaml_file(yml_file_path) + + ### this is to support the old and new attack data yaml format + datasets = attack_data_yaml_buff.get("datasets", []) + dataset = attack_data_yaml_buff.get("dataset", "") + + has_new_format = ( + isinstance(datasets, list) + and len(datasets) > 0 + and attack_datasets_path in datasets[0].get("path", "") + ) + + has_old_format = ( + (isinstance(dataset, str) and attack_datasets_path in dataset) + or (isinstance(dataset, list) and dataset and attack_datasets_path in dataset[0]) + ) + + if yml_name in file or has_new_format or has_old_format: + + needed_yaml_field_cache['attack_data_yml_file_path'] = yml_file_path + attack_id = attack_data_yaml_buff.get("id") + if attack_id: + needed_yaml_field_cache['attack_data_uuid'] = attack_data_yaml_buff['id'] + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... {file} ==> associated yml file extracted successfully") + return needed_yaml_field_cache + else: + ColorPrint.print_warning_fg(f"[!][WARNING]: ... attack_data yaml field: [uuid] => not found!!") + else: + # This runs ONLY if the loop never hits 'return' or 'break' + ColorPrint.print_error_fg("[-][. ERROR]: ... No matching YAML file was found during iteration!") + return {} + + def create_metadata_cache(self, yaml_data: yaml) -> dict: + """Create a metadata cache from the YAML data.""" + + needed_replay_yaml_field = { + "name": yaml_data.get("name", "Unknown"), + "id": yaml_data.get("id", "Unknown"), + "mitre_attack_id": yaml_data.get("tags", {}).get("mitre_attack_id", "Unknown"), + "analytic_story": yaml_data.get("tags", {}).get("analytic_story", "Unknown"), + "description": yaml_data.get("description", "No description available"), + #"file_path": yaml_data.get("file_path", "Unknown") + } + + # Checking for 'tests' key for handling 'attack_data' yaml field + if "tests" in yaml_data: + + # Check if 'attack_data' is present and not empty + if yaml_data["tests"]: + test = yaml_data["tests"][0] # First test in the list + + # Check for 'attack_data' key and that it's not empty + if "attack_data" in test and test["attack_data"]: + attack_data = test["attack_data"][0] # Assuming it's a list with at least one entry + + # Adding the 'attack_data' fields to the cache dictionary + needed_replay_yaml_field["attack_data_link"] = attack_data.get("data", "N/A") + needed_replay_yaml_field["attack_data_source"] = attack_data.get("source", "N/A") + needed_replay_yaml_field["attack_data_sourcetype"] = attack_data.get("sourcetype", "N/A") + + else: + ColorPrint.print_warning_fg("[!][WARNING]: ... attack_data is empty or missing in the first test") + else: + ColorPrint.print_warning_fg("[!][WARNING]: ... no tests available in the YAML") + else: + ColorPrint.print_warning_fg("[!][WARNING]: ... no tests field in YAML data") + + return needed_replay_yaml_field + + def send_data_to_splunk(self, file_path, splunk_host, hec_token, event_host_uuid, index="test", source="test", sourcetype="test"): + """Send a data file to Splunk HEC.""" + disable_warnings() + hec_channel = str(uuid.uuid4()) + headers = { + "Authorization": f"Splunk {hec_token}", + "X-Splunk-Request-Channel": hec_channel, + } + url_params = { + "index": index, + "source": source, + "sourcetype": sourcetype, + "host": event_host_uuid, + } + url = urllib.parse.urljoin( + f"https://{splunk_host}:8088", + "services/collector/raw" + ) + if self.read_config_settings('debug_print'): + ColorPrint.print_yellow_fg(Style.DIM + self.header_divider("ATTACK DATA REPLAY SUMMARY")) + name_value = os.path.basename(file_path).split(".")[0] + ColorPrint.print_yellow_fg(Style.DIM + f"[+][. INFO]: ... Sending dataset '{name_value}' from {file_path}") + ColorPrint.print_yellow_fg(Style.DIM + f"[+][. INFO]: ... index: {index}") + ColorPrint.print_yellow_fg(Style.DIM + f"[+][. INFO]: ... source: {source}") + ColorPrint.print_yellow_fg(Style.DIM + f"[+][. INFO]: ... sourcetype: {sourcetype}") + ColorPrint.print_yellow_fg(Style.DIM + f"[+][. INFO]: ... uuid: {event_host_uuid}") + ColorPrint.print_yellow_fg(Style.DIM + self.footer_divider("ATTACK DATA REPLAY SUMMARY")) + + with open(file_path, "rb") as datafile: + try: + res = requests.post( + url, + params=url_params, + data=datafile.read(), + allow_redirects=True, + headers=headers, + verify=False, + ) + res.raise_for_status() + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... :white_check_mark: Sent {file_path} to Splunk HEC") + except Exception as e: + ColorPrint.print_error_fg(f"[+][. ERROR]: ... :x: Error sending {file_path} to Splunk HEC: {e}") + return + + def attack_data_replay_cmd(self, needed_replay_yaml_field:dict, index_value:str)->bool: + """main function in replaying attack data in splunk server""" + + env_var = self.load_environment_variables() + splunk_host = env_var['host'] + hec_token = env_var['hec_token'] + #error_terms = ["error", "Failed to connect","unreacheable=1", "timed out", "exception","fatal"] + ### setup Command arguments + attack_data_file_path = needed_replay_yaml_field['attack_data_output_file_path'] + attack_data_source = needed_replay_yaml_field['attack_data_source'] + attack_data_source_type = needed_replay_yaml_field['attack_data_sourcetype'] + attack_data_uuid = needed_replay_yaml_field['attack_data_uuid'] + attack_data_yml_file_path = needed_replay_yaml_field['attack_data_yml_file_path'] + + if not os.path.isdir(os.path.expanduser(self.read_config_settings('attack_data_dir_path'))): + ColorPrint.print_error_fg("[-][. ERROR]: ... The attack data folder path in config is invalid or not exist.") + return False + + try: + self.send_data_to_splunk(attack_data_file_path, splunk_host, hec_token, attack_data_uuid, index_value, attack_data_source, attack_data_source_type) + return True + except Exception as e: + ColorPrint.print_error_fg(f"[-][. ERROR]: ... running command: {e}") + return False + + + def process_replay_attack_data(self, tag:str, normalized_args_list:list, index_value:str, generated_guid:str)->None: + """main function in replaying attack data using ESCU metadata""" + + + ColorPrint.print_cyan_fg(self.header_divider("TOTAl-REPLAY-ACTIVATED", tag)) + + + search_found_list = [] + for field_name in normalized_args_list: + + ### skipped if the inputted string has file extension or not a .yml file + if field_name.endswith(".yml"): + continue + + needed_replay_yaml_field = {} + search_found_list = self.search_security_content(tag, field_name) + + if not search_found_list: + return + + ColorPrint.print_info_fg(f"[+][. INFO]: ... Total filtered detections: {len(search_found_list)} ") + + + for ctr, needed_replay_yaml_field in enumerate(search_found_list): + + ### get the attack data url link + if "attack_data_link" in needed_replay_yaml_field: + attack_data_link = needed_replay_yaml_field["attack_data_link"] + else: + continue + + ### generate replay yaml output folder + output_base_dir = os.path.join(self.curdir, self.read_config_settings('output_dir_name')) + self.generate_output_dir(output_base_dir) + + attack_data_timestamp_dir_path = os.path.join(output_base_dir, datetime.date.today().strftime("%Y-%m-%d")) + escu_detection_guid = needed_replay_yaml_field['id'] + + ColorPrint.print_info_fg(f"[+][. INFO]: ... Downloading attack data for: {needed_replay_yaml_field['name']} ... item:{ctr+1}") + attack_datasets_full_path, attack_datasets_path = self.download_via_attack_data(attack_data_link, attack_data_timestamp_dir_path, generated_guid) + + ### update the total-replay cache yaml file + needed_replay_yaml_field['attack_data_output_file_path'] = attack_datasets_full_path + needed_replay_yaml_field = self.locate_associated_attack_data_yaml_file(attack_data_link, attack_datasets_path, needed_replay_yaml_field) + if not needed_replay_yaml_field: + return + + ### dropped the replay yaml cache + replayed_yaml_cache_path = os.path.join(attack_data_timestamp_dir_path, generated_guid, self.read_config_settings('replayed_yaml_cache_dir_name')) + self.generate_output_dir(replayed_yaml_cache_path) + + cache_replay_yaml_file_path = os.path.join(replayed_yaml_cache_path, needed_replay_yaml_field['id']+ "_" + self.read_config_settings('cache_replay_yaml_name')) + self.dump_yaml_file(cache_replay_yaml_file_path, needed_replay_yaml_field) + + ### comment me if you dont want to see the replay_cache yml file + if self.read_config_settings('debug_print'): + ColorPrint.print_yellow_fg(Style.DIM + self.header_divider("TOTAL-REPLAY CACHE YAML FILE", tag)) + ColorPrint.print_yellow_fg(Style.DIM + f"[+] ... \n{json.dumps(needed_replay_yaml_field, indent=4)}") + ColorPrint.print_yellow_fg(Style.DIM + self.footer_divider("TOTAL-REPLAY CACHE YAML FILE")) + + + self.processed_attack_data_uuid.append(needed_replay_yaml_field['attack_data_uuid']) + + try: + self.attack_data_replay_cmd(needed_replay_yaml_field, index_value) + except: + raise ValueError(f"[+][. ERROR]: ... Attack Data Replay Exception!") + + ColorPrint.print_magenta_fg("\n[+]" + "β–ˆ" * 160 + "\n") + + ColorPrint.print_cyan_fg(self.footer_divider("TOTAl-REPLAY-ACTIVATED")) + + + return + + def search_security_content(self, key_name: str, field_name:str)->list: + """Function in parsing Splunk Security Content Repo""" + + ColorPrint.print_info_fg(f'[+][. INFO]: ... processing => [ {field_name} ]') + + search_found_list = [] + + found_flag = False + + needed_replay_yaml_field = {} + security_content_dir_path = self.read_config_settings("security_content_detection_path") + + if not os.path.isdir(os.path.expanduser(security_content_dir_path)): + ColorPrint.print_error_fg("[+][. ERROR]: The security Content folder path in config is invalid or not exist.") + return [] + + for root, dirs, files in os.walk(os.path.expanduser(security_content_dir_path)): + ## skip deprecated folder + if dirs == "deprecated": + continue + ## break the loop once we found the needed field value + if found_flag: + break + + for file in files: + + if file.endswith((".yaml",".yml")): + file_path = os.path.join(root, file) + + yaml_data = self.read_yaml_file(file_path) + + ### check if file content is empty or invalid + if yaml_data is None: + ColorPrint.print_error_fg(f"[!] [STATUS]: [. ERROR] ... skipping empty or invalid YAML file: {file_path}") + continue + + if self.check_needed_yaml_field(key_name, field_name, yaml_data): + if key_name != "mitre_attack_id" and key_name != "analytic_story": + found_flag = True + + needed_replay_yaml_field = self.create_metadata_cache(yaml_data) + search_found_list.append(needed_replay_yaml_field) + + return search_found_list + + def check_needed_yaml_field(self, yaml_key_name:str, field_name:str, yaml_data:yaml)->bool: + """Function for checking needed yaml fields per search type""" + if yaml_key_name == "name": + if yaml_data.get(yaml_key_name).lower() == field_name.lower(): + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... found -> [ {yaml_data.get(yaml_key_name)} ]") + return True + + elif yaml_key_name == "mitre_attack_id": + if yaml_data['tags']: + if "mitre_attack_id" in yaml_data['tags']: + if field_name.lower() in [i.lower() for i in yaml_data['tags']['mitre_attack_id']]: + return True + + elif yaml_key_name == "analytic_story": + if yaml_data['tags']: + if "analytic_story" in yaml_data['tags']: + if field_name.lower() in [i.lower() for i in yaml_data['tags']['analytic_story']]: + return True + + elif yaml_key_name == "id": + if yaml_data.get(yaml_key_name).lower() == field_name.lower(): + ColorPrint.print_success_fg(f"[+][SUCCESS]: ... found -> [ {yaml_data.get(yaml_key_name)} ]") + return True + + return False + + def normalized_file_args(self, file_path:str)->list: + """segregate string by possible Splunk Security content yaml field via regex""" + ColorPrint.print_info_fg("[+][. INFO]: ... segregating the file data ...") + + with open(file_path, "r") as f: + lines = f.readlines() + + filter_line = [l.strip() for l in lines] + + segregate = defaultdict(list) + regex_patterns = { + "detection_filename": r"^[a-z0-9_]+(?:\.yml)?$", + "guid": r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", + "mitre_attack_tid": r"^T\d{4}(?:\.\d{3})?$", + "detection_and_analytic_story_name": r"^[A-Za-z0-9\s\-]+$" + } + + + for item in filter_line: + found_category = False + for category, pattern in regex_patterns.items(): + if re.fullmatch(pattern, item): # fullmatch ensures the entire string matches the pattern + segregate[category].append(item) + + found_category = True + + if not found_category: + + ColorPrint.print_warning_fg(f"[!][WARNING]: ... {item} - No category matched.") + + + ### remove guid and technique id catched by generic regex detection__and_analytic_story_name + if 'detection_and_analytic_story_name' in segregate and ("guid" in segregate or "mitre_attack_tid" in segregate): + removed_list = [] + for v in segregate['detection_and_analytic_story_name']: + if v in segregate['guid']: + removed_list.append(v) + if v in segregate['mitre_attack_tid']: + removed_list.append(v) + for r in removed_list: + segregate['detection_and_analytic_story_name'].remove(r) + regular_dict = dict(segregate) + beautified_json_string = json.dumps(regular_dict, indent=4) + + ### comment me if you dont want to see the replay_cache yml file + if self.read_config_settings('debug_print'): + ColorPrint.print_yellow_fg(Style.DIM + self.header_divider("STRING CATEGORIZATION")) + ColorPrint.print_yellow_fg(Style.DIM + f"[+] ... \n{beautified_json_string}") + ColorPrint.print_yellow_fg(Style.DIM + self.footer_divider("STRING CATEGORIZATION")) + return segregate + + def process_local_yaml_cache(self, local_replayed_yaml_dir_path:str,index_value:str)->None: + """Process local YAML cache files for replaying attack data.""" + if not os.path.isdir(local_replayed_yaml_dir_path): + ColorPrint.print_error_fg(f"[-][. ERROR]: ... Inputted {local_replayed_yaml_dir_path} is invalid!") + return + else: + for root, dirs, files in os.walk(local_replayed_yaml_dir_path): + for ctr, file in enumerate(files): + ColorPrint.print_info_fg(f'[+][. INFO]: ... Processing {ctr+1} => [ {file} ]') + file_path = os.path.join(root, file) + + if file.endswith((".yaml",".yml")): + yaml_data = self.read_yaml_file(file_path) + + if yaml_data is None: + ColorPrint.print_error_fg(f"[-][. ERROR]: ... Skipping empty or invalid YAML file: {file_path}") + continue + + ### comment me if you dont want to see the replay_cache yml file + if self.read_config_settings('debug_print'): + ColorPrint.print_yellow_fg(Style.DIM + self.header_divider("TOTAL-REPLAY CACHE YAML FILE", "local cache")) + ColorPrint.print_yellow_fg(Style.DIM + f"[+] ... \n{json.dumps(yaml_data, indent=4)}") + ColorPrint.print_yellow_fg(Style.DIM + self.footer_divider("TOTAL-REPLAY CACHE YAML FILE")) + + self.processed_attack_data_uuid.append(yaml_data['attack_data_uuid']) + + try: + self.attack_data_replay_cmd(yaml_data, index_value) + except: + raise ValueError(f"[+][. ERROR]: ... Attack Data Replay Exception!") + + ColorPrint.print_magenta_fg("\n[+]" + "β–ˆ" * 160 + "\n") + + + return True \ No newline at end of file