From d01a422c97380c2e48570bd4e156e7e1cfa269b6 Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Thu, 28 Sep 2023 18:44:21 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20=E2=9C=A8=20New=20CDK=20component?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now you can just install this component via `lacework component install cloud-hunter`, and then execute it as: ``` $ lacework cloud-hunter _ (` ). _ ( ). .:(` )`. ) _( '`. :( . ) .=(`( . ) .-- `. ( ) ) (( (..__.:'-' .+( ) ` _` ) ) `. `( ) ) ( . ) ( ) ._ ) ` __.:' ) ( ( )) `-'.-(` ) ) ) ( ) --' `- __.' :( )) .-' (_.' .') `( ) )) (_ ) ` __.:' _ / `/_ _/ /_/ _ _ _/__ _ /_,//_//_//_/ / //_// //_'/ /_'/ Lacework Labs usage: cloud-hunter [-h] [--environment LW_ENV] [--any ANYTHING] [--source EVTSOURCE] [--event EVTNAME] [--events EVTNAMES] [--type EVTTYPE] [--username ACCOUNT] [--ip SRCIP] [--userAgent UASTRING] [--reqParam PARAM] [--reqParams PARAMS] [--region REGION] [--errorCode ERROR] [--errorCodes ERRORS] [--accessDenied STATUS] [--dns DNS] [--os OPERATING_SYSTEM] [--hostname HOSTNAME] [--filename FILENAME] [--filetype FILETYPE] [--cmdline CMDLINE] [--hunt EXQUERY] [-y YAML_FILE] [-t DAYS] [-q] [-c] [-j] [-o OUTPUT_FILENAME] Dynamically create queries and hunt with the Lacework Query Language (LQL) quickly and efficiently options: -h, --help show this help message and exit --environment LW_ENV Lacework environment (will be set to "default" if not specified) --any ANYTHING Include literally any keyword in an LQL query (Waring: may return thousands of results) --source EVTSOURCE Include events by source in an LQL query --event EVTNAME Include specific event type in an LQL query --events EVTNAMES Include multiple events - Important - use this format: "'event1','event2'" --type EVTTYPE Include a specific event type in an LQL query --username ACCOUNT Include a username in an LQL query --ip SRCIP Include a source IP address in an LQL query --userAgent UASTRING Include a User Agent string in an LQL query --reqParam PARAM Include a Request Parameter String in an LQL query --reqParams PARAMS Include multiple Request Parameters - Important - use this format: "'param1','param2'" --region REGION Include region within an LQL query --errorCode ERROR Include an error code in an LQL query --errorCodes ERRORS Include multiple error codes - Important - use this format: "'error1','error2'" --accessDenied STATUS Include Access Status in LQL query - Provide: (Y/N) --dns DNS Include DNS entries queried from the environment --os OPERATING_SYSTEM Include activities related to the operating system name --hostname HOSTNAME Include activities tied to a hostname --filename FILENAME Include activities tied to a filename --filetype FILETYPE Include activities tied to a type of file --cmdline CMDLINE Include command line items in LQL query --hunt EXQUERY Hunt by executing a raw LQL query -y YAML_FILE Hunt using a LQL YAML file -t DAYS Hunt timeframe in days (default 7-days) -q, --query Display the crafted query -c, --count Hunt and only count the hits, do not print the details to the screen -j, --JSON View the results as raw JSON -o OUTPUT_FILENAME Export the results in CSV format or JSON if -j argument is passed ``` Signed-off-by: Salim Afiune Maya --- .gitignore | 7 +- Makefile | 9 + README.md | 126 +++-- VERSION | 1 + cloud-hunter.spec | 41 ++ poetry.lock | 469 ++++++++++++++++++ pyproject.toml | 28 ++ requirements.txt | 8 - src/cloud-hunter/__init__.py | 0 .../cloud-hunter/__main__.py | 258 +++++----- 10 files changed, 756 insertions(+), 191 deletions(-) create mode 100644 Makefile create mode 100644 VERSION create mode 100644 cloud-hunter.spec create mode 100644 poetry.lock create mode 100644 pyproject.toml delete mode 100644 requirements.txt create mode 100755 src/cloud-hunter/__init__.py rename cloud-hunter.py => src/cloud-hunter/__main__.py (82%) diff --git a/.gitignore b/.gitignore index 741f4ec..b16347c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ .DS_Store -config.json \ No newline at end of file +.dev +.version +.signature +config.json +/cloud-hunter +build/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c849751 --- /dev/null +++ b/Makefile @@ -0,0 +1,9 @@ +.PHONY: build +build: + poetry lock + poetry install + poetry run poe build + +.PHONY: clean +clean: + rm cloud-hunter diff --git a/README.md b/README.md index 3dad378..1c30c9e 100644 --- a/README.md +++ b/README.md @@ -17,13 +17,16 @@ ``` ### Dynamically create queries and hunt with the Lacework Query Language (LQL) -Cloud-Hunter allows you to search for key data across the Lacework platform, with the advantage of crafting LQL queries for every search being executed. This not only helps to find data quickly and easily, (even including content that isn't displayed in the console) but develop queries for ongoing monitoring as you scale the queries along with your organization's cloud security program. +Cloud-Hunter allows you to search for key data across the Lacework platform, with the advantage of crafting LQL queries +for every search being executed. This not only helps to find data quickly and easily, (even including content that isn't +displayed in the console) but develop queries for ongoing monitoring as you scale the queries along with your +organization's cloud security program. -Works alongside the Lacework CLI +Works as a component of the Lacework CLI * [Lacework CLI](https://docs.lacework.com/cli) -* [API Docs] https://.lacework.net/api/v2/docs +* [API Docs](https://docs.lacework.net/api/v2/docs/) # Installation @@ -38,18 +41,43 @@ $ lacework configure # Configuration data will be stored in the following file: ~/.lacework.toml ``` + +Install `cloud-hunter` component: +```bash +$ lacework component install cloud-hunter +``` + +# Development Mode + +Once the component is installed, you can enter development mode by running: +``` +$ lacework component dev cloud-hunter --noninteractive +``` + +Clone this repository at `~/.config/lacework/components/cloud-hunter` +``` +$ cd ~/.config/lacework/components/cloud-hunter +$ git init +$ git remote add origin https://github.com/afiune/cloud-hunter.git +$ git pull origin master +``` + +Build the component: +``` +$ make build +``` + +Execute the component via `lacework cloud-hunter` ✨ + # Configuration Make a note of the environments configured for use with the GO-SDK. The "default" setting will be used, so if you only have one environment configured, you can proceed on to the next steps. ```bash -# Install the python3 requirements: -$ pip3 install -r requirements.txt - -# To run against environments other than the "default" configuration, declare using -environment: -$ ./cloud-hunter.py -environment MyEnvironment +# To run against environments other than the "default" configuration, declare using --environment: +$ lacework cloud-hunter --environment MyEnvironment # Display the help menu -$ ./cloud-hunter.py +$ lacework cloud-hunter ``` # Query Generation @@ -59,7 +87,7 @@ Leverage the included command line operators to develop queries for the Lacework ### Query Source ```bash # Hunt for events matching an AWS event source -$ ./cloud-hunter.py -source +$ lacework cloud-hunter --source # Example Event Sources: iam.amazonaws.com, iam, kms, ec2, s3, etc... @@ -70,43 +98,43 @@ iam.amazonaws.com, iam, kms, ec2, s3, etc... ### Events ```bash # Single Event -$ ./cloud-hunter.py -event +$ lacework cloud-hunter --event # Example Events: Client.DryRunOperation, ListAccessKeys, ListAttachedRolePolicies, etc. # Multiple Events -$ ./cloud-hunter.py -events "'', '', ''" +$ lacework cloud-hunter --events "'', '', ''" # Example Event Chaining: -$ ./cloud-hunter.py -events "'ListBackupVaults', 'ListBackupJobs', 'ListBackupPlans', 'ListCopyJobs', 'ListProtectedResources', 'ListRestoreJobs'" +$ lacework cloud-hunter --events "'ListBackupVaults', 'ListBackupJobs', 'ListBackupPlans', 'ListCopyJobs', 'ListProtectedResources', 'ListRestoreJobs'" ``` ### Event Type ```bash # Generate a query for specific event type -$ ./cloud-hunter.py -type AwsConsoleSignIn +$ lacework cloud-hunter --type AwsConsoleSignIn ``` ### Users ```bash # Generate a query for specific user activity -$ ./cloud-hunter.py -username greg +$ lacework cloud-hunter --username greg ``` ### Source IP Address ```bash # Generate a query for a source IP Address -$ ./cloud-hunter.py -ip 127.0.0.1 +$ lacework cloud-hunter --ip 127.0.0.1 ``` ### User Agent String ```bash # User Agent String by keyword -$ ./cloud-hunter.py -userAgent aws-cli +$ lacework cloud-hunter --userAgent aws-cli # Full user agent string - no quotes (") and escape the spaces -$ ./cloud-hunter.py -userAgent aws-cli/1.19.59\ Python/3.9.5\ Darwin/20.6.0\ botocore/1.20.59 +$ lacework cloud-hunter --userAgent aws-cli/1.19.59\ Python/3.9.5\ Darwin/20.6.0\ botocore/1.20.59 # Note that LQL is case-sensitive ``` @@ -114,55 +142,55 @@ $ ./cloud-hunter.py -userAgent aws-cli/1.19.59\ Python/3.9.5\ Darwin/20.6.0\ bot ### DNS ```bash # Search for queries to a specific domain -$ ./cloud-hunter.py -dns evil.site.com +$ lacework cloud-hunter --dns evil.site.com # Search for a relative domain, such as any DNS query containing .ru -$ ./cloud-hunter.py -dns .ru +$ lacework cloud-hunter --dns .ru ``` ### Hostname ```bash # Search for activities involving either a specific or relative hostname -$ ./cloud-hunter.py -hostname pwnedhost1234 +$ lacework cloud-hunter --hostname pwnedhost1234 ``` ### Filename ```bash # Search for a specific file -$ ./cloud-hunter.py -filename potato.json +$ lacework cloud-hunter --filename potato.json # Search for all files with a specified extension -$ ./cloud-hunter.py -filename .sh +$ lacework cloud-hunter --filename .sh ``` ### Command Line ```bash # Search for any command line values -$ ./cloud-hunter.py -cmdline netcat +$ lacework cloud-hunter --cmdline netcat ``` ### Request Parameters ```bash # Hunting by request parameters to look for potential injection attacks -$ ./cloud-hunter.py -reqParam + +$ lacework cloud-hunter --reqParam + # Multiple request parameters -$ ./cloud-hunter.py -reqParams "'+%','@%','=%','-%'" +$ lacework cloud-hunter --reqParams "'+%','@%','=%','-%'" ``` ### Errors ```bash # Single Error -$ ./cloud-hunter.py -errorCode +$ lacework cloud-hunter --errorCode # Example Error: AccessDenied, Client.UnauthorizedOperation, etc. # Multiple Errors -$ ./cloud-hunter.py -errorCodes "'', '', ''" +$ lacework cloud-hunter --errorCodes "'', '', ''" # Example Error Chaining: -$ ./cloud-hunter.py -errorCodes "'AccessDenied', 'Client.UnauthorizedOperation'" +$ lacework cloud-hunter --errorCodes "'AccessDenied', 'Client.UnauthorizedOperation'" ``` @@ -171,26 +199,26 @@ $ ./cloud-hunter.py -errorCodes "'AccessDenied', 'Client.UnauthorizedOperation'" # Query for access denied events # Toggle 'y' to list access denied events # Toggle 'n' to set error type to 'null' -$ ./cloud-hunter.py -accessDenied y +$ lacework cloud-hunter --accessDenied y ``` ### Query Chaining ```bash # All parameters can be chained together to develop more complex and targeted queries # Example: -$ ./cloud-hunter.py -source backup -events "'ListBackupVaults', 'ListProtectedResources'" -username bob -userAgent aws-cli -accessDenied y +$ lacework cloud-hunter --source backup --events "'ListBackupVaults', 'ListProtectedResources'" --username bob --userAgent aws-cli --accessDenied y ``` ### Special Queries ```bash # Filter out certain values by adding '!' to each string -$ ./cloud-hunter.py -username '!greg' -accessDenied y +$ lacework cloud-hunter --username '!greg' --accessDenied y # Check if a certain parameter exists -$ /cloud-hunter.py -username exists -errorCode Client.DryRunOperation +$ /cloud-hunter.py --username exists --errorCode Client.DryRunOperation # To view the generated LQL query, append -j to the command. The will be idisplayed but will not execute -$ /cloud-hunter.py -username exists -errorCode Client.DryRunOperation -j +$ /cloud-hunter.py --username exists --errorCode Client.DryRunOperation -j ``` # Hunting @@ -201,20 +229,20 @@ For any search term, append to execute the query and view results from the past ```bash # Default timeframe is 7-days, this can be modified with the -t parameter # Example search over 1-day: -$ ./cloud-hunter.py -username bob -t 1 +$ lacework cloud-hunter --username bob -t 1 # Multiple parameters example: -$ ./cloud-hunter.py -source backup -event ListBackupVaults -username bob -userAgent aws-cli -accessDenied y +$ lacework cloud-hunter --source backup --event ListBackupVaults --username bob --userAgent aws-cli --accessDenied y # Count the hits and do not display results to the screen -$ ./cloud-hunter.py -username bob -c +$ lacework cloud-hunter --username bob -c ``` ### File-Based Rule Hunting ```bash # Hunting with a LQL rule that is stored in a file: -$ ./cloud-hunter.py -y /path/to/file.yaml +$ lacework cloud-hunter -y /path/to/file.yaml # YAML format-files are preferred # Raw LQL query text-files will work as well @@ -224,10 +252,10 @@ $ ./cloud-hunter.py -y /path/to/file.yaml ```bash # Execute any LQL query directly via the -hunt option # Example: -$ ./cloud-hunter.py -hunt "LaceworkLabs_CloudHunter {SOURCE {CloudTrailRawEvents} FILTER { EVENT NOT IN ('DescribeTags', 'ListGrants') AND ERROR_CODE IN ('AccessDenied', 'Client.UnauthorizedOperation') } RETURN DISTINCT {INSERT_ID, INSERT_TIME, EVENT_TIME, EVENT}}" +$ lacework cloud-hunter --hunt "LaceworkLabs_CloudHunter {SOURCE {CloudTrailRawEvents} FILTER { EVENT NOT IN ('DescribeTags', 'ListGrants') AND ERROR_CODE IN ('AccessDenied', 'Client.UnauthorizedOperation') } RETURN DISTINCT {INSERT_ID, INSERT_TIME, EVENT_TIME, EVENT}}" # Hunting with a fully-formatted multi-line LQL rule: -$ ./cloud-hunter.py -hunt """LaceworkLabs_CloudHunter { +$ lacework cloud-hunter --hunt """LaceworkLabs_CloudHunter { source { LW_CFG_AWS_S3_GET_BUCKET_POLICY } @@ -252,31 +280,31 @@ $ ./cloud-hunter.py -hunt """LaceworkLabs_CloudHunter { # Raw hunting can be combined with any time, output, and counting options as well... # Example hunting over a 30-day period (default is 7-days): -$ ./cloud-hunter.py -hunt "query" -t 30 +$ lacework cloud-hunter --hunt "query" -t 30 # Example counting the hits: -$ ./cloud-hunter.py -hunt "query" -c +$ lacework cloud-hunter --hunt "query" -c # Example with JSON output: -$ ./cloud-hunter.py -hunt "query" -j -o filename.json +$ lacework cloud-hunter --hunt "query" -j -o filename.json # Example with CSV output: -$ ./cloud-hunter.py -hunt "query" -o filename.csv +$ lacework cloud-hunter --hunt "query" -o filename.csv ``` ### Exporting Data ```bash # View the raw query data in JSON: -$ ./cloud-hunter.py -source backup -event ListBackupVaults -username bob -userAgent aws-cli -accessDenied y -j +$ lacework cloud-hunter --source backup --event ListBackupVaults --username bob --userAgent aws-cli --accessDenied y -j # View the raw query data in JSON and export to a file: -$ ./cloud-hunter.py -source backup -event ListBackupVaults -username bob -userAgent aws-cli -accessDenied y -j -o filename.json +$ lacework cloud-hunter --source backup --event ListBackupVaults --username bob --userAgent aws-cli --accessDenied y -j -o filename.json # Export the full query output to CSV: -$ ./cloud-hunter.py -source backup -event ListBackupVaults -username bob -userAgent aws-cli -accessDenied y -o filename.csv +$ lacework cloud-hunter --source backup --event ListBackupVaults --username bob --userAgent aws-cli --accessDenied y -o filename.csv # Do not display output to screen but save the data to a CSV file. -$ ./cloud-hunter.py -source backup -event ListBackupVaults -username bob -userAgent aws-cli -accessDenied y -o filename.csv -c +$ lacework cloud-hunter --source backup --event ListBackupVaults --username bob --userAgent aws-cli --accessDenied y -o filename.csv -c # Note - the count argument only works with CSV output. ``` @@ -345,6 +373,8 @@ Contribute to the framework by opening a pull request Tracking major changes to the codebase ```bash +10/01/2023 - CDK Component Release + 9/19/2022 - Public Release 4/4/2022 - NoCase and Sub Accounts diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..c0ab82c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.0.1-dev diff --git a/cloud-hunter.spec b/cloud-hunter.spec new file mode 100644 index 0000000..8d2fb05 --- /dev/null +++ b/cloud-hunter.spec @@ -0,0 +1,41 @@ +# -*- mode: python ; coding: utf-8 -*- +from PyInstaller.utils.hooks import collect_submodules + +hiddenimports = [] +hiddenimports += collect_submodules('application') + + +a = Analysis( + ['src/cloud-hunter/__main__.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='cloud-hunter', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..f9d4c77 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,469 @@ +# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. + +[[package]] +name = "altgraph" +version = "0.17.4" +description = "Python graph (network) package" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "altgraph-0.17.4-py2.py3-none-any.whl", hash = "sha256:642743b4750de17e655e6711601b077bc6598dbfa3ba5fa2b2a35ce12b508dff"}, + {file = "altgraph-0.17.4.tar.gz", hash = "sha256:1b5afbb98f6c4dcadb2e2ae6ab9fa994bbb8c1d75f4fa96d340f9437ae454406"}, +] + +[[package]] +name = "certifi" +version = "2023.7.22" +description = "Python package for providing Mozilla's CA Bundle." +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, + {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, +] + +[[package]] +name = "chardet" +version = "4.0.0" +description = "Universal encoding detector for Python 2 and 3" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "chardet-4.0.0-py2.py3-none-any.whl", hash = "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"}, + {file = "chardet-4.0.0.tar.gz", hash = "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa"}, +] + +[[package]] +name = "idna" +version = "2.10" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "idna-2.10-py2.py3-none-any.whl", hash = "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"}, + {file = "idna-2.10.tar.gz", hash = "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6"}, +] + +[[package]] +name = "macholib" +version = "1.16.3" +description = "Mach-O header analysis and editing" +category = "dev" +optional = false +python-versions = "*" +files = [ + {file = "macholib-1.16.3-py2.py3-none-any.whl", hash = "sha256:0e315d7583d38b8c77e815b1ecbdbf504a8258d8b3e17b61165c6feb60d18f2c"}, + {file = "macholib-1.16.3.tar.gz", hash = "sha256:07ae9e15e8e4cd9a788013d81f5908b3609aa76f9b1421bae9c4d7606ec86a30"}, +] + +[package.dependencies] +altgraph = ">=0.17" + +[[package]] +name = "numpy" +version = "1.25.0" +description = "Fundamental package for array computing in Python" +category = "main" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8aa130c3042052d656751df5e81f6d61edff3e289b5994edcf77f54118a8d9f4"}, + {file = "numpy-1.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e3f2b96e3b63c978bc29daaa3700c028fe3f049ea3031b58aa33fe2a5809d24"}, + {file = "numpy-1.25.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6b267f349a99d3908b56645eebf340cb58f01bd1e773b4eea1a905b3f0e4208"}, + {file = "numpy-1.25.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aedd08f15d3045a4e9c648f1e04daca2ab1044256959f1f95aafeeb3d794c16"}, + {file = "numpy-1.25.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6d183b5c58513f74225c376643234c369468e02947b47942eacbb23c1671f25d"}, + {file = "numpy-1.25.0-cp310-cp310-win32.whl", hash = "sha256:d76a84998c51b8b68b40448ddd02bd1081bb33abcdc28beee6cd284fe11036c6"}, + {file = "numpy-1.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:c0dc071017bc00abb7d7201bac06fa80333c6314477b3d10b52b58fa6a6e38f6"}, + {file = "numpy-1.25.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c69fe5f05eea336b7a740e114dec995e2f927003c30702d896892403df6dbf0"}, + {file = "numpy-1.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c7211d7920b97aeca7b3773a6783492b5b93baba39e7c36054f6e749fc7490c"}, + {file = "numpy-1.25.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecc68f11404930e9c7ecfc937aa423e1e50158317bf67ca91736a9864eae0232"}, + {file = "numpy-1.25.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e559c6afbca484072a98a51b6fa466aae785cfe89b69e8b856c3191bc8872a82"}, + {file = "numpy-1.25.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6c284907e37f5e04d2412950960894b143a648dea3f79290757eb878b91acbd1"}, + {file = "numpy-1.25.0-cp311-cp311-win32.whl", hash = "sha256:95367ccd88c07af21b379be1725b5322362bb83679d36691f124a16357390153"}, + {file = "numpy-1.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:b76aa836a952059d70a2788a2d98cb2a533ccd46222558b6970348939e55fc24"}, + {file = "numpy-1.25.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b792164e539d99d93e4e5e09ae10f8cbe5466de7d759fc155e075237e0c274e4"}, + {file = "numpy-1.25.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7cd981ccc0afe49b9883f14761bb57c964df71124dcd155b0cba2b591f0d64b9"}, + {file = "numpy-1.25.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5aa48bebfb41f93043a796128854b84407d4df730d3fb6e5dc36402f5cd594c0"}, + {file = "numpy-1.25.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5177310ac2e63d6603f659fadc1e7bab33dd5a8db4e0596df34214eeab0fee3b"}, + {file = "numpy-1.25.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0ac6edfb35d2a99aaf102b509c8e9319c499ebd4978df4971b94419a116d0790"}, + {file = "numpy-1.25.0-cp39-cp39-win32.whl", hash = "sha256:7412125b4f18aeddca2ecd7219ea2d2708f697943e6f624be41aa5f8a9852cc4"}, + {file = "numpy-1.25.0-cp39-cp39-win_amd64.whl", hash = "sha256:26815c6c8498dc49d81faa76d61078c4f9f0859ce7817919021b9eba72b425e3"}, + {file = "numpy-1.25.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5b1b90860bf7d8a8c313b372d4f27343a54f415b20fb69dd601b7efe1029c91e"}, + {file = "numpy-1.25.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85cdae87d8c136fd4da4dad1e48064d700f63e923d5af6c8c782ac0df8044542"}, + {file = "numpy-1.25.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cc3fda2b36482891db1060f00f881c77f9423eead4c3579629940a3e12095fe8"}, + {file = "numpy-1.25.0.tar.gz", hash = "sha256:f1accae9a28dc3cda46a91de86acf69de0d1b5f4edd44a9b0c3ceb8036dfff19"}, +] + +[[package]] +name = "packaging" +version = "23.1" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"}, + {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"}, +] + +[[package]] +name = "pandas" +version = "1.3.5" +description = "Powerful data structures for data analysis, time series, and statistics" +category = "main" +optional = false +python-versions = ">=3.7.1" +files = [ + {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:62d5b5ce965bae78f12c1c0df0d387899dd4211ec0bdc52822373f13a3a022b9"}, + {file = "pandas-1.3.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:adfeb11be2d54f275142c8ba9bf67acee771b7186a5745249c7d5a06c670136b"}, + {file = "pandas-1.3.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60a8c055d58873ad81cae290d974d13dd479b82cbb975c3e1fa2cf1920715296"}, + {file = "pandas-1.3.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd541ab09e1f80a2a1760032d665f6e032d8e44055d602d65eeea6e6e85498cb"}, + {file = "pandas-1.3.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2651d75b9a167cc8cc572cf787ab512d16e316ae00ba81874b560586fa1325e0"}, + {file = "pandas-1.3.5-cp310-cp310-win_amd64.whl", hash = "sha256:aaf183a615ad790801fa3cf2fa450e5b6d23a54684fe386f7e3208f8b9bfbef6"}, + {file = "pandas-1.3.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:344295811e67f8200de2390093aeb3c8309f5648951b684d8db7eee7d1c81fb7"}, + {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:552020bf83b7f9033b57cbae65589c01e7ef1544416122da0c79140c93288f56"}, + {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5cce0c6bbeb266b0e39e35176ee615ce3585233092f685b6a82362523e59e5b4"}, + {file = "pandas-1.3.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7d28a3c65463fd0d0ba8bbb7696b23073efee0510783340a44b08f5e96ffce0c"}, + {file = "pandas-1.3.5-cp37-cp37m-win32.whl", hash = "sha256:a62949c626dd0ef7de11de34b44c6475db76995c2064e2d99c6498c3dba7fe58"}, + {file = "pandas-1.3.5-cp37-cp37m-win_amd64.whl", hash = "sha256:8025750767e138320b15ca16d70d5cdc1886e8f9cc56652d89735c016cd8aea6"}, + {file = "pandas-1.3.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fe95bae4e2d579812865db2212bb733144e34d0c6785c0685329e5b60fcb85dd"}, + {file = "pandas-1.3.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f261553a1e9c65b7a310302b9dbac31cf0049a51695c14ebe04e4bfd4a96f02"}, + {file = "pandas-1.3.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b6dbec5f3e6d5dc80dcfee250e0a2a652b3f28663492f7dab9a24416a48ac39"}, + {file = "pandas-1.3.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3bc49af96cd6285030a64779de5b3688633a07eb75c124b0747134a63f4c05f"}, + {file = "pandas-1.3.5-cp38-cp38-win32.whl", hash = "sha256:b6b87b2fb39e6383ca28e2829cddef1d9fc9e27e55ad91ca9c435572cdba51bf"}, + {file = "pandas-1.3.5-cp38-cp38-win_amd64.whl", hash = "sha256:a395692046fd8ce1edb4c6295c35184ae0c2bbe787ecbe384251da609e27edcb"}, + {file = "pandas-1.3.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bd971a3f08b745a75a86c00b97f3007c2ea175951286cdda6abe543e687e5f2f"}, + {file = "pandas-1.3.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37f06b59e5bc05711a518aa10beaec10942188dccb48918bb5ae602ccbc9f1a0"}, + {file = "pandas-1.3.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c21778a688d3712d35710501f8001cdbf96eb70a7c587a3d5613573299fdca6"}, + {file = "pandas-1.3.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3345343206546545bc26a05b4602b6a24385b5ec7c75cb6059599e3d56831da2"}, + {file = "pandas-1.3.5-cp39-cp39-win32.whl", hash = "sha256:c69406a2808ba6cf580c2255bcf260b3f214d2664a3a4197d0e640f573b46fd3"}, + {file = "pandas-1.3.5-cp39-cp39-win_amd64.whl", hash = "sha256:32e1a26d5ade11b547721a72f9bfc4bd113396947606e00d5b4a5b79b3dcb006"}, + {file = "pandas-1.3.5.tar.gz", hash = "sha256:1e4285f5de1012de20ca46b188ccf33521bff61ba5c5ebd78b4fb28e5416a9f1"}, +] + +[package.dependencies] +numpy = {version = ">=1.21.0", markers = "python_version >= \"3.10\""} +python-dateutil = ">=2.7.3" +pytz = ">=2017.3" + +[package.extras] +test = ["hypothesis (>=3.58)", "pytest (>=6.0)", "pytest-xdist"] + +[[package]] +name = "pastel" +version = "0.2.1" +description = "Bring colors to your terminal." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pastel-0.2.1-py2.py3-none-any.whl", hash = "sha256:4349225fcdf6c2bb34d483e523475de5bb04a5c10ef711263452cb37d7dd4364"}, + {file = "pastel-0.2.1.tar.gz", hash = "sha256:e6581ac04e973cac858828c6202c1e1e81fee1dc7de7683f3e1ffe0bfd8a573d"}, +] + +[[package]] +name = "pefile" +version = "2023.2.7" +description = "Python PE parsing module" +category = "dev" +optional = false +python-versions = ">=3.6.0" +files = [ + {file = "pefile-2023.2.7-py3-none-any.whl", hash = "sha256:da185cd2af68c08a6cd4481f7325ed600a88f6a813bad9dea07ab3ef73d8d8d6"}, + {file = "pefile-2023.2.7.tar.gz", hash = "sha256:82e6114004b3d6911c77c3953e3838654b04511b8b66e8583db70c65998017dc"}, +] + +[[package]] +name = "poethepoet" +version = "0.23.0" +description = "A task runner that works well with poetry." +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "poethepoet-0.23.0-py3-none-any.whl", hash = "sha256:d573ff31d7678e62b6f9bc9a1291ae2009ac14e0eead0a450598f9f05abb27a3"}, + {file = "poethepoet-0.23.0.tar.gz", hash = "sha256:62a0a6a518df5985c191aee0c1fcd2bb6a0a04eb102997786fcdf118e4147d22"}, +] + +[package.dependencies] +pastel = ">=0.2.1,<0.3.0" +tomli = ">=1.2.2" + +[package.extras] +poetry-plugin = ["poetry (>=1.0,<2.0)"] + +[[package]] +name = "pyinstaller" +version = "6.0.0" +description = "PyInstaller bundles a Python application and all its dependencies into a single package." +category = "dev" +optional = false +python-versions = "<3.13,>=3.8" +files = [ + {file = "pyinstaller-6.0.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:d84b06fb9002109bfc542e76860b81459a8585af0bbdabcfc5dcf272ef230de7"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:aa922d1d73881d0820a341d2c406a571cc94630bdcdc275427c844a12e6e376e"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:52e5b3a2371d7231de17515c7c78d8d4a39d70c8c095e71d55b3b83434a193a8"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4a75bde5cda259bb31f2294960d75b9d5c148001b2b0bd20a91f9c2116675a6c"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:5314f6f08d2bcbc031778618ba97d9098d106119c2e616b3b081171fe42f5415"}, + {file = "pyinstaller-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:0ad7cc3776ca17d0bededcc352cba2b1c89eb4817bfabaf05972b9da8c424935"}, + {file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:cccdad6cfe7a5db7d7eb8df2e5678f8375268739d5933214e180da300aa54e37"}, + {file = "pyinstaller-6.0.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:fb6af82989dac7c58bd25ed9ba3323bc443f8c1f03804f69c9f5e363bf4a021c"}, + {file = "pyinstaller-6.0.0-py3-none-win32.whl", hash = "sha256:68769f5e6722474bb1038e35560444659db8b951388bfe0c669bb52a640cd0eb"}, + {file = "pyinstaller-6.0.0-py3-none-win_amd64.whl", hash = "sha256:438a9e0d72a57d5bba4f112d256e39ea4033c76c65414c0693d8311faa14b090"}, + {file = "pyinstaller-6.0.0-py3-none-win_arm64.whl", hash = "sha256:16a473065291dd7879bf596fa20e65bd9d1e8aafc2cef1bffa3e42e707e2e68e"}, + {file = "pyinstaller-6.0.0.tar.gz", hash = "sha256:d702cff041f30e7a53500b630e07b081e5328d4655023319253d73935e75ade2"}, +] + +[package.dependencies] +altgraph = "*" +macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} +packaging = ">=20.0" +pefile = {version = ">=2022.5.30", markers = "sys_platform == \"win32\""} +pyinstaller-hooks-contrib = ">=2021.4" +pywin32-ctypes = {version = ">=0.2.1", markers = "sys_platform == \"win32\""} +setuptools = ">=42.0.0" + +[package.extras] +hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"] + +[[package]] +name = "pyinstaller-hooks-contrib" +version = "2023.9" +description = "Community maintained hooks for PyInstaller" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyinstaller-hooks-contrib-2023.9.tar.gz", hash = "sha256:76084b5988e3957a9df169d2a935d65500136967e710ddebf57263f1a909cd80"}, + {file = "pyinstaller_hooks_contrib-2023.9-py2.py3-none-any.whl", hash = "sha256:f34f4c6807210025c8073ebe665f422a3aa2ac5f4c7ebf4c2a26cc77bebf63b5"}, +] + +[[package]] +name = "python-dateutil" +version = "2.8.2" +description = "Extensions to the standard Python datetime module" +category = "main" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.8.2.tar.gz", hash = "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86"}, + {file = "python_dateutil-2.8.2-py2.py3-none-any.whl", hash = "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pytz" +version = "2023.3.post1" +description = "World timezone definitions, modern and historical" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "pytz-2023.3.post1-py2.py3-none-any.whl", hash = "sha256:ce42d816b81b68506614c11e8937d3aa9e41007ceb50bfdcb0749b921bf646c7"}, + {file = "pytz-2023.3.post1.tar.gz", hash = "sha256:7b4fddbeb94a1eba4b557da24f19fdf9db575192544270a9101d8509f9f43d7b"}, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.2" +description = "A (partial) reimplementation of pywin32 using ctypes/cffi" +category = "dev" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, + {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, +] + +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "main" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] + +[[package]] +name = "requests" +version = "2.25.1" +description = "Python HTTP for Humans." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "requests-2.25.1-py2.py3-none-any.whl", hash = "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"}, + {file = "requests-2.25.1.tar.gz", hash = "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +chardet = ">=3.0.2,<5" +idna = ">=2.5,<3" +urllib3 = ">=1.21.1,<1.27" + +[package.extras] +security = ["cryptography (>=1.3.4)", "pyOpenSSL (>=0.14)"] +socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"] + +[[package]] +name = "setuptools" +version = "68.2.2" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, + {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tabulate" +version = "0.8.9" +description = "Pretty-print tabular data" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "tabulate-0.8.9-py3-none-any.whl", hash = "sha256:d7c013fe7abbc5e491394e10fa845f8f32fe54f8dc60c6622c6cf482d25d47e4"}, + {file = "tabulate-0.8.9.tar.gz", hash = "sha256:eb1d13f25760052e8931f2ef80aaf6045a6cceb47514db8beab24cded16f13a7"}, +] + +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "main" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "urllib3" +version = "1.26.16" +description = "HTTP library with thread-safe connection pooling, file post, and more." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +files = [ + {file = "urllib3-1.26.16-py2.py3-none-any.whl", hash = "sha256:8d36afa7616d8ab714608411b4a3b13e58f463aee519024578e062e141dce20f"}, + {file = "urllib3-1.26.16.tar.gz", hash = "sha256:8f135f6502756bde6b2a9b28989df5fbe87c9970cecaa69041edcce7f0589b14"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] +socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] + +[[package]] +name = "vtapi3" +version = "1.2.1" +description = "VirusTotal API" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "vtapi3-1.2.1-py3-none-any.whl", hash = "sha256:7641c5820f616fa7a286a0e7d4586a3513154a1e1f40d0674565c393f332aac4"}, +] + +[package.dependencies] +requests = ">=2.22.0" + +[[package]] +name = "yml" +version = "0.0.1" +description = "A fast, safe, pure Python YAML and JSON parser" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "yml-0.0.1.tar.gz", hash = "sha256:5168010f8cfd91fbcda1d0bffae869a1117050bf8cf0e909d4f00ccd9bd80ab9"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.11,<3.12" +content-hash = "a1899aabe9efb0f32ac776ec65337a29d0fc1791748abd60e9ad415a11f9e222" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..434e403 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,28 @@ +[tool.poetry] +name = "cloudhunter" +version = "0.1.0" +description = "Dynamically generate and hunt with Lacework LQL queries quickly and efficiently" +authors = ["Lacework Inc "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.11,<3.12" +toml = "0.10.2" +pandas = "1.3.5" +requests = "2.25.1" +tabulate = "0.8.9" +numpy = "1.25.0" +yml = "0.0.1" +PyYAML = "6.0" +vtapi3 = "1.2.1" + +[tool.poetry.group.dev.dependencies] +pyinstaller = "^6.0.0" +poethepoet = "^0.23.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +[tool.poe.tasks] +build = "poetry run pyinstaller src/cloud-hunter/__main__.py --collect-submodules application -F --name cloud-hunter --distpath ." +clean = "rm -r build/ cloud-hunter cloud-hunter.spec" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index de55d16..0000000 --- a/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -toml==0.10.2 -requests==2.25.1 -tabulate==0.8.9 -numpy==1.21.4 -pandas==1.3.5 -yml==0.0.1 -PyYAML==6.0 -vtapi3==1.2.1 diff --git a/src/cloud-hunter/__init__.py b/src/cloud-hunter/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/cloud-hunter.py b/src/cloud-hunter/__main__.py similarity index 82% rename from cloud-hunter.py rename to src/cloud-hunter/__main__.py index c9e7994..a9c85ce 100755 --- a/cloud-hunter.py +++ b/src/cloud-hunter/__main__.py @@ -54,28 +54,28 @@ class bcolors: def parse_the_things(): parser = argparse.ArgumentParser(description = 'Dynamically create queries and hunt with the Lacework Query Language (LQL) quickly and efficiently') - parser.add_argument('-environment', help = 'Lacework environment (will be set to "default" if not specified)', action = 'store', dest = 'lw_env') - parser.add_argument('-any', help = 'Include literally any keyword in an LQL query (Waring: may return thousands of results)', action = 'store', dest = 'anything') - parser.add_argument('-source', help = 'Include events by source in an LQL query', action = 'store', dest = 'evtSource') - parser.add_argument('-event', help = 'Include specific event type in an LQL query', action = 'store', dest = 'evtName') - parser.add_argument('-events', help = 'Include multiple events - Important - use this format: \"\'event1\',\'event2\'\"', action = 'store', dest = 'evtNames') - parser.add_argument('-type', help = 'Include a specific event type in an LQL query', action = 'store', dest = 'evtType') - parser.add_argument('-username', help = 'Include a username in an LQL query', action = 'store', dest = 'account') - parser.add_argument('-ip', help = 'Include a source IP address in an LQL query', action = 'store', dest = 'srcIp') - parser.add_argument('-userAgent', help = 'Include a User Agent string in an LQL query', action = 'store', dest = 'uaString') - parser.add_argument('-reqParam', help ='Include a Request Parameter String in an LQL query', action = 'store', dest = 'param') - parser.add_argument('-reqParams', help ='Include multiple Request Parameters - Important - use this format: \"\'param1\',\'param2\'\"', action = 'store', dest = 'params') - parser.add_argument('-region', help = 'Include region within an LQL query', action = 'store', dest = 'region') - parser.add_argument('-errorCode', help ='Include an error code in an LQL query', action ='store', dest = 'error') - parser.add_argument('-errorCodes', help ='Include multiple error codes - Important - use this format: \"\'error1\',\'error2\'\"', action ='store', dest = 'errors') - parser.add_argument('-accessDenied', help = 'Include Access Status in LQL query - Provide: (Y/N)', action = 'store', dest = 'status') - parser.add_argument('-dns', help = 'Include DNS entries queried from the environment', action = 'store', dest = 'dns') - parser.add_argument('-os', help = 'Include activities related to the operating system name', action = 'store', dest = 'operating_system') - parser.add_argument('-hostname', help = 'Include activities tied to a hostname', action = 'store', dest = 'hostname') - parser.add_argument('-filename', help = 'Include activities tied to a filename', action = 'store', dest = 'filename') - parser.add_argument('-filetype', help = 'Include activities tied to a type of file', action = 'store', dest = 'filetype') - parser.add_argument('-cmdline', help = 'Include command line items in LQL query', action = 'store', dest = 'cmdline') - parser.add_argument('-hunt', help = 'Hunt by executing a raw LQL query', action = 'store', dest = 'exQuery') + parser.add_argument('--environment', help = 'Lacework environment (will be set to "default" if not specified)', action = 'store', dest = 'lw_env') + parser.add_argument('--any', help = 'Include literally any keyword in an LQL query (Waring: may return thousands of results)', action = 'store', dest = 'anything') + parser.add_argument('--source', help = 'Include events by source in an LQL query', action = 'store', dest = 'evtSource') + parser.add_argument('--event', help = 'Include specific event type in an LQL query', action = 'store', dest = 'evtName') + parser.add_argument('--events', help = 'Include multiple events - Important - use this format: \"\'event1\',\'event2\'\"', action = 'store', dest = 'evtNames') + parser.add_argument('--type', help = 'Include a specific event type in an LQL query', action = 'store', dest = 'evtType') + parser.add_argument('--username', help = 'Include a username in an LQL query', action = 'store', dest = 'account') + parser.add_argument('--ip', help = 'Include a source IP address in an LQL query', action = 'store', dest = 'srcIp') + parser.add_argument('--userAgent', help = 'Include a User Agent string in an LQL query', action = 'store', dest = 'uaString') + parser.add_argument('--reqParam', help ='Include a Request Parameter String in an LQL query', action = 'store', dest = 'param') + parser.add_argument('--reqParams', help ='Include multiple Request Parameters - Important - use this format: \"\'param1\',\'param2\'\"', action = 'store', dest = 'params') + parser.add_argument('--region', help = 'Include region within an LQL query', action = 'store', dest = 'region') + parser.add_argument('--errorCode', help ='Include an error code in an LQL query', action ='store', dest = 'error') + parser.add_argument('--errorCodes', help ='Include multiple error codes - Important - use this format: \"\'error1\',\'error2\'\"', action ='store', dest = 'errors') + parser.add_argument('--accessDenied', help = 'Include Access Status in LQL query - Provide: (Y/N)', action = 'store', dest = 'status') + parser.add_argument('--dns', help = 'Include DNS entries queried from the environment', action = 'store', dest = 'dns') + parser.add_argument('--os', help = 'Include activities related to the operating system name', action = 'store', dest = 'operating_system') + parser.add_argument('--hostname', help = 'Include activities tied to a hostname', action = 'store', dest = 'hostname') + parser.add_argument('--filename', help = 'Include activities tied to a filename', action = 'store', dest = 'filename') + parser.add_argument('--filetype', help = 'Include activities tied to a type of file', action = 'store', dest = 'filetype') + parser.add_argument('--cmdline', help = 'Include command line items in LQL query', action = 'store', dest = 'cmdline') + parser.add_argument('--hunt', help = 'Hunt by executing a raw LQL query', action = 'store', dest = 'exQuery') parser.add_argument('-y', help = 'Hunt using a LQL YAML file', action = 'store', dest = 'yaml_file') parser.add_argument('-t', help ='Hunt timeframe in days (default 7-days)', action = 'store', dest = 'days') parser.add_argument('-q', '--query', help = 'Display the crafted query', action = 'store_true') @@ -85,7 +85,6 @@ def parse_the_things(): return parser def configuration(lw_env): - global lw_account global sub_account global authorization_token @@ -136,7 +135,6 @@ def configuration(lw_env): quit() def craft_query(**arguments): - global crafted_query global cmd_options global cloud_trail_activity @@ -149,7 +147,7 @@ def craft_query(**arguments): for arg in arguments.items(): variable = arg[0] value = arg[1] - + # ============================== CloudTrailRawEvents ============================== # # ===== Single Variable Options ===== # @@ -159,12 +157,12 @@ def craft_query(**arguments): cloud_trail_activity = True var_count += 1 if '!' in value: - joined_options['-source \'{}\''.format(value)]='any_value' + joined_options['--source \'{}\''.format(value)]='any_value' value = value.split("!") any_value = "EVENT NOT LIKE '%{}%'".format(value[1]) else: any_value = "contains(lower(EVENT), '{}')".format(value.lower()) - joined_options['-source {}'.format(value)]='any_value' + joined_options['--source {}'.format(value)]='any_value' joined_items[any_value]='any_value' # Event Source @@ -173,14 +171,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_source = "EVENT_SOURCE IS NOT NULL" - joined_options['-source {}'.format(value)]='event_source' + joined_options['--source {}'.format(value)]='event_source' elif '!' in value: - joined_options['-source \'{}\''.format(value)]='event_source' + joined_options['--source \'{}\''.format(value)]='event_source' value = value.split("!") event_source = "EVENT_SOURCE NOT LIKE '%{}%'".format(value[1]) else: event_source = "contains(lower(EVENT_SOURCE), '{}')".format(value.lower()) - joined_options['-source {}'.format(value)]='event_source' + joined_options['--source {}'.format(value)]='event_source' joined_items[event_source]='event_source' # Event Region @@ -191,13 +189,13 @@ def craft_query(**arguments): if value.lower() in regions: if value.lower() == 'exists': event_region = "EVENT:awsRegion IS NOT NULL" - joined_options['-region {}'.format(value)]='event_region' + joined_options['--region {}'.format(value)]='event_region' else: event_region = "EVENT:awsRegion = '{}'".format(value.lower()) - joined_options['-region {}'.format(value)]='event_region' + joined_options['--region {}'.format(value)]='event_region' joined_items[event_region]='event_region' elif '!' in value: - joined_options['-region \'{}\''.format(value)]='event_region' + joined_options['--region \'{}\''.format(value)]='event_region' value = value.split("!") if value[1] in regions: event_region = "EVENT:awsRegion NOT LIKE '%{}%'".format(value[1]) @@ -213,21 +211,21 @@ def craft_query(**arguments): print(f"{bcolors.CYAN}Regions:{bcolors.ENDC} {{}}".format(regions)) print() quit() - + # Event Name if variable == 'evtName': cloud_trail_activity = True var_count += 1 if value.lower() == 'exists': event_name = "EVENT_NAME IS NOT NULL" - joined_options['-event {}'.format(value)]='event_name' + joined_options['--event {}'.format(value)]='event_name' elif '!' in value: - joined_options['-event \'{}\''.format(value)]='event_name' + joined_options['--event \'{}\''.format(value)]='event_name' value = value.split("!") event_name = "EVENT_NAME NOT LIKE '%{}%'".format(value[1]) else: event_name = "contains(lower(EVENT_NAME), '{}')".format(value.lower()) - joined_options['-event {}'.format(value)]='event_name' + joined_options['--event {}'.format(value)]='event_name' joined_items[event_name]='event_name' # Event Type @@ -236,25 +234,25 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_type = "EVENT:eventType::String IS NOT NULL" - joined_options['-type {}'.format(value)]='event_type' + joined_options['--type {}'.format(value)]='event_type' elif '!' in value: - joined_options['-type \'{}\''.format(value)]='event_type' + joined_options['--type \'{}\''.format(value)]='event_type' value = value.split("!") event_type = "EVENT:eventType::String NOT LIKE '%{}%'".format(value[1]) else: event_type = "contains(lower(EVENT:eventType::String), '{}')".format(value.lower()) - joined_options['-type {}'.format(value)]='event_type' + joined_options['--type {}'.format(value)]='event_type' joined_items[event_type]='event_type' - + # Username if variable == 'username': cloud_trail_activity = True var_count += 1 if value.lower() == 'exists': event_username = "EVENT:userIdentity.userName IS NOT NULL" - joined_options['-username {}'.format(value)]='event_username' + joined_options['--username {}'.format(value)]='event_username' elif '!' in value: - joined_options['-username \'{}\''.format(value)]='event_username' + joined_options['--username \'{}\''.format(value)]='event_username' value = value.split("!") event_username = """(EVENT:userIdentity.userName NOT LIKE '%{}%' OR EVENT:userIdentity.arn NOT LIKE '%{}%' @@ -265,39 +263,39 @@ def craft_query(**arguments): OR contains(lower(EVENT:userIdentity.arn), '{}') OR contains(lower(EVENT:responseElements.assumedRoleUser.arn), '{}') OR contains(lower(EVENT:requestParameters.userName), '{}'))""".format(value.lower(),value.lower(),value.lower(),value.lower()) - joined_options['-username {}'.format(value)]='event_username' + joined_options['--username {}'.format(value)]='event_username' joined_items[event_username]='event_username' - + # Source IP if variable == 'srcIp': cloud_trail_activity = True var_count += 1 if value.lower() == 'exists': event_ip = "EVENT:sourceIPAddress IS NOT NULL" - joined_options['-ip {}'.format(value)]='event_ip' + joined_options['--ip {}'.format(value)]='event_ip' elif '!' in value: - joined_options['-ip \'{}\''.format(value)]='event_ip' + joined_options['--ip \'{}\''.format(value)]='event_ip' value = value.split("!") event_ip = "EVENT:sourceIPAddress NOT LIKE '%{}%'".format(value[1]) else: event_ip = "EVENT:sourceIPAddress = '{}'".format(value) - joined_options['-ip {}'.format(value)]='event_ip' + joined_options['--ip {}'.format(value)]='event_ip' joined_items[event_ip]='event_ip' - + # User Agent if variable == 'uaString': cloud_trail_activity = True var_count += 1 if value.lower() == 'exists': event_ua = "EVENT:userAgent IS NOT NULL" - joined_options['-userAgent {}'.format(value)]='event_ua' + joined_options['--userAgent {}'.format(value)]='event_ua' elif '!' in value: - joined_options['-userAgent \'{}\''.format(value)]='event_ua' + joined_options['--userAgent \'{}\''.format(value)]='event_ua' value = value.split("!") event_ua = "EVENT:userAgent NOT LIKE '%{}%'".format(value[1]) else: event_ua = "contains(lower(EVENT:userAgent), '{}')".format(value.lower()) - joined_options['-userAgent {}'.format(value)]='event_ua' + joined_options['--userAgent {}'.format(value)]='event_ua' joined_items[event_ua]='event_ua' # Request Parameter @@ -308,12 +306,12 @@ def craft_query(**arguments): request_param = "EVENT:requestParameters.name IS NOT NULL" joined_options['-request_param {}'.format(value)]='request_param' elif '!' in value: - joined_options['-reqParam \'{}\''.format(value)]='request_param' + joined_options['--reqParam \'{}\''.format(value)]='request_param' value = value.split("!") request_param = "EVENT:requestParameters NOT LIKE '%{}%'".format(value[1]) else: request_param = "contains(lower(EVENT:requestParameters), '{}')".format(value.lower()) - joined_options['-reqParam {}'.format(value)]='request_param' + joined_options['--reqParam {}'.format(value)]='request_param' joined_items[request_param]='request_param' # Error Code @@ -322,14 +320,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': error_code = "ERROR_CODE IS NOT NULL" - joined_options['-errorCode {}'.format(value)]='error_code' + joined_options['--errorCode {}'.format(value)]='error_code' elif '!' in value: - joined_options['-errorCode \'{}\''.format(value)]='error_code' + joined_options['--errorCode \'{}\''.format(value)]='error_code' value = value.split("!") error_code = "ERROR_CODE NOT LIKE '%{}%'".format(value[1]) else: error_code = "contains(lower(ERROR_CODE), '{}')".format(value.lower()) - joined_options['-errorCode {}'.format(value)]='error_code' + joined_options['--errorCode {}'.format(value)]='error_code' joined_items[error_code]='error_code' # Access Denied @@ -341,7 +339,7 @@ def craft_query(**arguments): else: access_status = "ERROR_CODE IS NULL" joined_items[access_status]='access_level' - joined_options['-accessDenied {}'.format(value)]='access_level' + joined_options['--accessDenied {}'.format(value)]='access_level' # ===== Multi-Variable Options ===== # @@ -353,7 +351,7 @@ def craft_query(**arguments): for event_value in value: multi_joined_items[value]='event_value' multiVariableName = "EVENT_NAME" - joined_options['-events \"{}\"'.format(value)]='event_value' + joined_options['--events \"{}\"'.format(value)]='event_value' # Request Parameters if variable == 'params': @@ -363,7 +361,7 @@ def craft_query(**arguments): for event_value in value: multi_joined_items[value]='request_params' multiVariableName = "EVENT:requestParameters.name" - joined_options['-reqParams \"{}\"'.format(value)]='request_params' + joined_options['--reqParams \"{}\"'.format(value)]='request_params' # Error Codes if variable == 'errors': @@ -373,7 +371,7 @@ def craft_query(**arguments): for error in value: multi_joined_items[value]='errors' multiVariableName = "ERROR_CODE" - joined_options['-errorCodes \"{}\"'.format(value)]='errors' + joined_options['--errorCodes \"{}\"'.format(value)]='errors' # ============================== LW_HE_MACHINES ============================== # @@ -382,14 +380,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_hostname = "HOSTNAME IS NOT NULL" - joined_options['-hostname {}'.format(value)]='hostname' + joined_options['--hostname {}'.format(value)]='hostname' elif '!' in value: - joined_options['-hostname \'{}\''.format(value)]='hostname' + joined_options['--hostname \'{}\''.format(value)]='hostname' value = value.split("!") event_hostname = "HOSTNAME NOT LIKE '%{}%'".format(value[1]) else: event_hostname = "contains(lower(HOSTNAME), '{}')".format(value.lower()) - joined_options['-hostname {}'.format(value)]='hostname' + joined_options['--hostname {}'.format(value)]='hostname' joined_items[event_hostname]='hostname' if variable == 'operating_system': @@ -397,14 +395,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_operating_system = "OS IS NOT NULL" - joined_options['-os {}'.format(value)]='operating_system' + joined_options['--os {}'.format(value)]='operating_system' elif '!' in value: - joined_options['-os \'{}\''.format(value)]='operating_system' + joined_options['--os \'{}\''.format(value)]='operating_system' value = value.split("!") event_operating_system = "OS NOT LIKE '%{}%'".format(value[1]) else: event_operating_system = "contains(lower(OS), '{}')".format(value.lower()) - joined_options['-os {}'.format(value)]='operating_system' + joined_options['--os {}'.format(value)]='operating_system' joined_items[event_operating_system]='operating_system' # ============================== LW_HE_FILES ============================== # @@ -414,14 +412,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_filename = "FILE_NAME IS NOT NULL" - joined_options['-filename {}'.format(value)]='filename' + joined_options['--filename {}'.format(value)]='filename' elif '!' in value: - joined_options['-filename \'{}\''.format(value)]='filename' + joined_options['--filename \'{}\''.format(value)]='filename' value = value.split("!") event_filename = "FILE_NAME NOT LIKE '%{}%'".format(value[1]) else: event_filename = "contains(lower(FILE_NAME), '{}')".format(value.lower()) - joined_options['-filename {}'.format(value)]='filename' + joined_options['--filename {}'.format(value)]='filename' joined_items[event_filename]='filename' if variable == 'filetype': @@ -429,14 +427,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_filetype = "FILE_TYPE IS NOT NULL" - joined_options['-filetype {}'.format(value)]='filetype' + joined_options['--filetype {}'.format(value)]='filetype' elif '!' in value: - joined_options['-filetype \'{}\''.format(value)]='filetype' + joined_options['--filetype \'{}\''.format(value)]='filetype' value = value.split("!") event_filetype = "FILE_TYPE NOT LIKE '%{}%'".format(value[1]) else: event_filetype = "contains(lower(FILE_TYPE), '{}')".format(value.lower()) - joined_options['-filetype {}'.format(value)]='filetype' + joined_options['--filetype {}'.format(value)]='filetype' joined_items[event_filetype]='filetype' # ============================== LW_HA_FILE_CHANGES ============================== # @@ -448,14 +446,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_dns = "HOSTNAME IS NOT NULL" - joined_options['-dns {}'.format(value)]='dns' + joined_options['--dns {}'.format(value)]='dns' elif '!' in value: - joined_options['-dns \'{}\''.format(value)]='dns' + joined_options['--dns \'{}\''.format(value)]='dns' value = value.split("!") event_dns = "HOSTNAME NOT LIKE '%{}%'".format(value[1]) else: event_dns = "contains(lower(HOSTNAME), '{}')".format(value.lower()) - joined_options['-dns {}'.format(value)]='dns' + joined_options['--dns {}'.format(value)]='dns' joined_items[event_dns]='dns' # ============================== LW_HA_USER_LOGINS ============================== # @@ -473,14 +471,14 @@ def craft_query(**arguments): var_count += 1 if value.lower() == 'exists': event_cmdline = "CMDLINE IS NOT NULL" - joined_options['-cmdline {}'.format(value)]='cmdline' + joined_options['--cmdline {}'.format(value)]='cmdline' elif '!' in value: - joined_options['-cmdline \'{}\''.format(value)]='cmdline' + joined_options['--cmdline \'{}\''.format(value)]='cmdline' value = value.split("!") event_cmdline = "CMDLINE NOT LIKE '%{}%'".format(value[1]) else: event_cmdline = "contains(lower(CMDLINE), '{}')".format(value.lower()) - joined_options['-cmdline {}'.format(value)]='cmdline' + joined_options['--cmdline {}'.format(value)]='cmdline' joined_items[event_cmdline]='cmdline' # ============================== LW_HA_CONNECTIONS ============================== # @@ -494,7 +492,7 @@ def craft_query(**arguments): joined_args = """ AND """.join(joined_items) multi_joined_args = ", ".join(multi_joined_items) - final_joined_args = """{} + final_joined_args = """{} AND {} IN ({})""".format(joined_args, multiVariableName, multi_joined_args) query_args = final_joined_args @@ -559,7 +557,7 @@ def craft_query(**arguments): SOURCE { LW_HE_IMAGES } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, IMAGE_CREATED_TIME, @@ -580,7 +578,7 @@ def craft_query(**arguments): SOURCE { LW_HE_FILES } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, MID, @@ -611,7 +609,7 @@ def craft_query(**arguments): SOURCE { LW_HA_FILE_CHANGES } FILTER { - %s + %s } RETURN DISTINCT { ACTIVITY_START_TIME, ACTIVITY_END_TIME, @@ -628,7 +626,7 @@ def craft_query(**arguments): SOURCE { LW_HA_DNS_REQUESTS } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, MID, @@ -644,7 +642,7 @@ def craft_query(**arguments): SOURCE { LW_HA_USER_LOGINS } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, LOGIN_TIME, @@ -664,7 +662,7 @@ def craft_query(**arguments): SOURCE { LW_CFG_AWS } FILTER { - %s + %s } RETURN DISTINCT { QUERY_START_TIME, QUERY_END_TIME, @@ -688,7 +686,7 @@ def craft_query(**arguments): SOURCE { LW_HE_CONTAINERS } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, CONTAINER_START_TIME, @@ -714,7 +712,7 @@ def craft_query(**arguments): SOURCE { LW_HE_USERS } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, MID, @@ -729,7 +727,7 @@ def craft_query(**arguments): SOURCE { LW_HE_PROCESSES } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, PROCESS_START_TIME, @@ -748,7 +746,7 @@ def craft_query(**arguments): SOURCE { LW_HA_CONNECTIONS } FILTER { - %s + %s } RETURN DISTINCT { RECORD_CREATED_TIME, CONN_START_TIME, @@ -782,12 +780,12 @@ def validate_query(queryValidation): validation_url = "https://{}.lacework.net/api/v2/Queries/validate".format(lw_account) if cloud_trail_activity: payload = json.dumps({ - "queryText": "{}".format(queryValidation), - "evaluatorId": "Cloudtrail" + "queryText": "{}".format(queryValidation), + "evaluatorId": "Cloudtrail" }) else: payload = json.dumps({ - "queryText": "{}".format(queryValidation), + "queryText": "{}".format(queryValidation), }) if sub_account: headers = { @@ -803,10 +801,10 @@ def validate_query(queryValidation): 'User-Agent': 'Lacework-Labs_Cloud-Hunter_v1' } try: - response = requests.request("POST", validation_url, headers=headers, data=payload) + response = requests.request("POST", validation_url, headers=headers, data=payload) except requests.exceptions.RequestException as e: - print(f"{bcolors.RED}[!] {bcolors.UNDERLINE}Query Validation Error{bcolors.ENDC}{bcolors.RED} [!]{bcolors.ENDC}") - print("{}".format(e)) + print(f"{bcolors.RED}[!] {bcolors.UNDERLINE}Query Validation Error{bcolors.ENDC}{bcolors.RED} [!]{bcolors.ENDC}") + print("{}".format(e)) if "data" in response.text: pass else: @@ -845,35 +843,31 @@ def hunt(exQuery): if cloud_trail_activity: payload = json.dumps({ "query": { - "evaluatorId": "Cloudtrail", - "queryText": "{}".format(exQuery) + "evaluatorId": "Cloudtrail", + "queryText": "{}".format(exQuery) }, - "arguments": [ - { - "name": "StartTimeRange", - "value": "{}".format(search_range) - }, - { - "name": "EndTimeRange", - "value": "{}".format(date_now) - } - ] + "arguments": [{ + "name": "StartTimeRange", + "value": "{}".format(search_range) + }, + { + "name": "EndTimeRange", + "value": "{}".format(date_now) + }] }) else: payload = json.dumps({ - "query": { - "queryText": "{}".format(exQuery) - }, - "arguments": [ - { - "name": "StartTimeRange", - "value": "{}".format(search_range) - }, - { - "name": "EndTimeRange", - "value": "{}".format(date_now) - } - ] + "query": { + "queryText": "{}".format(exQuery) + }, + "arguments": [{ + "name": "StartTimeRange", + "value": "{}".format(search_range) + }, + { + "name": "EndTimeRange", + "value": "{}".format(date_now) + }] }) if sub_account: headers = { @@ -890,7 +884,7 @@ def hunt(exQuery): } response = requests.request("POST", execute_custom_url, headers=headers, data=payload) json_data = json.loads(response.text) - + try: event_df = pd.DataFrame.from_dict(json_data['data'], orient='columns') except: @@ -902,7 +896,7 @@ def hunt(exQuery): print(response.text) print() quit() - + try: if cloud_trail_activity: event_count = len(json_data['data']) @@ -1102,9 +1096,9 @@ def hunt(exQuery): else: print("For additional information, export event details to a file:") if query_contents: - print(f"{bcolors.BLUE}$ ./{script_name} {{}} -o {bcolors.ENDC}".format(cmd_options)) + print(f"{bcolors.BLUE}$ lacework cloud-hunter {{}} -o {bcolors.ENDC}".format(cmd_options)) else: - print(f"{bcolors.BLUE}$ ./{script_name} -hunt -o {bcolors.ENDC}") + print(f"{bcolors.BLUE}$ lacework cloud-hunter --hunt -o {bcolors.ENDC}") print() elif event_count >= 2: if count: @@ -1134,9 +1128,9 @@ def hunt(exQuery): else: print("For additional information, export event details to a file:") if query_contents: - print(f"{bcolors.BLUE}$ ./{script_name} {{}} -o {bcolors.ENDC}".format(cmd_options)) + print(f"{bcolors.BLUE}$ lacework cloud-hunter {{}} -o {bcolors.ENDC}".format(cmd_options)) else: - print(f"{bcolors.BLUE}$ ./{script_name} -hunt -o {bcolors.ENDC}") + print(f"{bcolors.BLUE}$ lacework cloud-hunter --hunt -o {bcolors.ENDC}") print() def main(): @@ -1144,10 +1138,6 @@ def main(): parser = parse_the_things() args = parser.parse_args() - # cloud-hunter script - global script_name - script_name = os.path.basename(__file__) - # Global Hunting Terms global query_contents global event_source @@ -1225,7 +1215,7 @@ def main(): JSON = '' # Only query cloudtrail data if explicitly triggered - global cloud_trail_activity + global cloud_trail_activity cloud_trail_activity = False if args.exQuery: @@ -1293,7 +1283,7 @@ def main(): else: print(f"{{}}".format(banner)) print(parser.format_help()) - quit() + sys.exit() if __name__ == "__main__": - main() \ No newline at end of file + main()