diff --git a/samples/README.md b/samples/README.md
index 55b38087d..aef43f11d 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -43,6 +43,7 @@ The following samples are categorized by CrowdStrike Falcon API service collecti
| [Hosts](#hosts) | [List sensors by hostname](#list-sensors-by-hostname)
[Manage duplicate sensors](#manage-duplicate-sensors)
[CUSSED (Manage stale sensors)](#cussed-manage-stale-sensors)
[Match usernames to hosts](#match-usernames-to-hosts)
[Offset vs. Token](#offset-vs-token)
[Prune Hosts by Hostname or AID](#prune-hosts-by-hostname-or-aid)
[Quarantine a host](#quarantine-a-host)
[Quarantine a host (updated version)](#quarantine-a-host-updated-version) |
| [Identity Protection](#identity-protection) | [GraphQL Pagination](#graphql-pagination) |
| [Incidents](#incidents) | [CrowdScore QuickChart](#crowdscore-quickchart)
[Incident Triage](#incident-triage) |
+| [Installation Tokens](#installation-tokens) | [Token Dispenser](#token-dispenser) |
| [Intel](#intel) | [MISP Import](#misp-import)
[Intel Search](#intel-search) |
| [IOC](#ioc) | [Create indicators](#create-indicators) |
| [MalQuery](#malquery) | [Malqueryinator](#malqueryinator) |
@@ -643,6 +644,41 @@ This sample demonstrates the following CrowdStrike Incidents API operations:
---
+## Installation Tokens
+This category is dedicated to demonstrating the functionality provided by the CrowdStrike Installation Tokens API service collection.
+
+- [Token Dispenser](#token-dispenser)
+
+### Token Dispenser
+Easily manage installation tokens within your tenant or across child tenants with the [Token Dispenser](installation_tokens#token-dispenser).
+
+[![Installation Tokens](https://img.shields.io/badge/Service%20Class-Token_Dispenser-silver?style=for-the-badge&labelColor=red&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAOCAYAAAAi2ky3AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TpaIVBzuIOGSoDmJBVEQ3rUIRKoRaoVUHk5f+CE0akhQXR8G14ODPYtXBxVlXB1dBEPwBcXNzUnSREu9LCi1ifPB4H+e9c7jvXkColZhmtY0Cmm6bqURczGRXxNAruhAEMI1hmVnGrCQl4bu+7hHg512MZ/m/+3N1qzmLAQGReIYZpk28Tjy5aRuc94kjrCirxOfEIyYVSPzIdcXjN84FlwWeGTHTqTniCLFYaGGlhVnR1IgniKOqplO+kPFY5bzFWStVWKNO/sNwTl9e4jrtASSwgEVIEKGggg2UYCNGp06KhRTdx338/a5fIpdCrg0wcsyjDA2y6wefwe/eWvnxMS8pHAfaXxznYxAI7QL1quN8HztO/QQIPgNXetNfrgFTn6RXm1r0COjZBi6um5qyB1zuAH1PhmzKrsTnL+TzwPsZjSkL9N4Cnate3xr3OH0A0tSr5A1wcAgMFSh7zeffHa19+/dNo38/hq9yr+iELI0AAAAGYktHRAAAAAAAAPlDu38AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQflDAsTByz7Va2cAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYBJREFUKM+lkjFIlVEYht/zn3sFkYYUyUnIRcemhCtCU6JQOLiIU+QeJEQg6BBIm0s4RBCBLjq5OEvgJC1uOniJhivesLx17/97/vO9b4NK4g25157hfHCGB773/cA0HZIEAKiMj+LWiOxljG/i96pnCFP58XHnrWX2+9cj0dYl9Yu2FE9/9rXrcAAgs2eSyiBfOe/XRD503h/CuffOubQVUXL+Jh9BllzBbyJJBgDclVkO4Kukd8zzkXJbeUljIldFTstsmSHM6S81ma2KfPKlFdkGAMY4wzx/bbXapMy21My+YizdKNq5mDzLkrxafSxySFKjSWX2oTmjKzz4vN0r2lOFcL/Q3V0/mX95ILMXTTGYVfaut/aP2+oCMAvnZgCcsF5fcR0dg65YHAdwB+QApADvu0AuOe/ftlJAD7Nsgmm6yBjDtfWORJZlNtFyo/lR5Z7MyheKA5ktSur7sTAHazSG27pehjAiaVfkN8b4XFIJ/wOzbOx07VNRUuHy7w98CzCcGPyWywAAAABJRU5ErkJggg==)](installation_tokens#token-dispenser)
+[![MSSP Use supported](https://img.shields.io/badge/-Supports%20MSSP-darkblue?logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAAOCAYAAAAi2ky3AAABhWlDQ1BJQ0MgcHJvZmlsZQAAKJF9kT1Iw1AUhU9TpaIVBzuIOGSoDmJBVEQ3rUIRKoRaoVUHk5f+CE0akhQXR8G14ODPYtXBxVlXB1dBEPwBcXNzUnSREu9LCi1ifPB4H+e9c7jvXkColZhmtY0Cmm6bqURczGRXxNAruhAEMI1hmVnGrCQl4bu+7hHg512MZ/m/+3N1qzmLAQGReIYZpk28Tjy5aRuc94kjrCirxOfEIyYVSPzIdcXjN84FlwWeGTHTqTniCLFYaGGlhVnR1IgniKOqplO+kPFY5bzFWStVWKNO/sNwTl9e4jrtASSwgEVIEKGggg2UYCNGp06KhRTdx338/a5fIpdCrg0wcsyjDA2y6wefwe/eWvnxMS8pHAfaXxznYxAI7QL1quN8HztO/QQIPgNXetNfrgFTn6RXm1r0COjZBi6um5qyB1zuAH1PhmzKrsTnL+TzwPsZjSkL9N4Cnate3xr3OH0A0tSr5A1wcAgMFSh7zeffHa19+/dNo38/hq9yr+iELI0AAAAGYktHRAAAAAAAAPlDu38AAAAJcEhZcwAACxMAAAsTAQCanBgAAAAHdElNRQflDAsTByz7Va2cAAAAGXRFWHRDb21tZW50AENyZWF0ZWQgd2l0aCBHSU1QV4EOFwAAAYBJREFUKM+lkjFIlVEYht/zn3sFkYYUyUnIRcemhCtCU6JQOLiIU+QeJEQg6BBIm0s4RBCBLjq5OEvgJC1uOniJhivesLx17/97/vO9b4NK4g25157hfHCGB773/cA0HZIEAKiMj+LWiOxljG/i96pnCFP58XHnrWX2+9cj0dYl9Yu2FE9/9rXrcAAgs2eSyiBfOe/XRD503h/CuffOubQVUXL+Jh9BllzBbyJJBgDclVkO4Kukd8zzkXJbeUljIldFTstsmSHM6S81ma2KfPKlFdkGAMY4wzx/bbXapMy21My+YizdKNq5mDzLkrxafSxySFKjSWX2oTmjKzz4vN0r2lOFcL/Q3V0/mX95ILMXTTGYVfaut/aP2+oCMAvnZgCcsF5fcR0dg65YHAdwB+QApADvu0AuOe/ftlJAD7Nsgmm6yBjDtfWORJZlNtFyo/lR5Z7MyheKA5ktSur7sTAHazSG27pehjAiaVfkN8b4XFIJ/wOzbOx07VNRUuHy7w98CzCcGPyWywAAAABJRU5ErkJggg==&style=for-the-badge)](installation_tokens#token-dispenser)
+
+#### Installation Tokens API operations discussed
+This sample demonstrates the following CrowdStrike Installation Tokens API operations:
+
+| Operation | Description |
+| :--- | :--- |
+| [tokens_create](https://www.falconpy.io/Service-Collections/Installation-Tokens.html#tokens_create) | Creates a token. |
+| [tokens_delete](https://www.falconpy.io/Service-Collections/Installation-Tokens.html#tokens_delete) | Deletes a token immediately. To revoke a token, use `token_update` instead. |
+| [tokens_read](https://www.falconpy.io/Service-Collections/Installation-Tokens.html#tokens_read) | Get the details of one or more tokens by ID. |
+| [tokens_update](https://www.falconpy.io/Service-Collections/Installation-Tokens.html#tokens_update) | Updates one or more tokens. Use this endpoint to edit labels, change expiration, revoke, or restore. |
+
+#### Flight Control API operations discussed
+This sample demonstrates the following CrowdStrike Flight Control API operations:
+| Operation | Description |
+| :--- | :--- |
+| [queryChildren](https://www.falconpy.io/Service-Collections/MSSP.html#querychildren) | Query for customers linked as children. |
+
+#### Sensor Download API operations discussed
+This sample demonstrates the following CrowdStrike Sensor Download API operations:
+| Operation | Description |
+| :--- | :--- |
+| [GetSensorInstallersCCIDByQuery](https://www.falconpy.io/Service-Collections/Sensor-Download.html#getsensorinstallersccidbyquery) | Get CCID to use with sensor installers. |
+
+---
+
## Intel
This category provides samples that demonstrate the CrowdStrike Falcon Intel API service collection.
diff --git a/samples/installation_tokens/README.md b/samples/installation_tokens/README.md
new file mode 100644
index 000000000..771e6e1dc
--- /dev/null
+++ b/samples/installation_tokens/README.md
@@ -0,0 +1,908 @@
+![CrowdStrike Falcon](https://raw.githubusercontent.com/CrowdStrike/falconpy/main/docs/asset/cs-logo.png)
+
+[![CrowdStrike Subreddit](https://img.shields.io/badge/-r%2Fcrowdstrike-white?logo=reddit&labelColor=gray&link=https%3A%2F%2Freddit.com%2Fr%2Fcrowdstrike)](https://reddit.com/r/crowdstrike)
+
+# Installation Tokens examples
+The examples in this folder focus on leveraging CrowdStrike's Installation Tokens API to manage sensor installation tokens.
+- [Token Dispenser](#token-dispenser)
+
+## Token Dispenser
+This application displays and manages installation tokens within your CrowdStrike tenant.
+> [!NOTE]
+> This solution supports Flight Control (MSSP) usage for all functionality, allowing administrators to manage multiple tokens across child tenants with a single command.
+
+- [Requirements](#requirements)
+- [Running the program](#running-the-program)
+- [Execution syntax](#execution-syntax)
+- [Commands](#commands)
+- [Source code](#example-source-code)
+
+### Requirements
+- [Python 3.7 or greater](https://www.python.org)
+- [CrowdStrike FalconPy v1.3.4 or greater](https://github.com/CrowdStrike/falconpy/releases/tag/v1.3.4)
+- [pyfiglet](https://pypi.org/project/pyfiglet/)
+- [tabulate](https://pypi.org/project/tabulate/)
+
+### Running the program
+In order to run this demonstration, you will need access to CrowdStrike API keys with the following scope:
+| Service Collection | Scope |
+| :---- | :---- |
+| Installation Tokens | __READ__, __WRITE__ |
+
+To take advantage of MSSP mode (Flight Control) functionality, you will also need the following scopes:
+| Service Collection | Scope |
+| :---- | :---- |
+| Flight Control | __READ__ |
+| Sensor Downloads | __READ__ |
+
+> [!NOTE]
+> All operations within the Installation Tokens service collection maintain low rate limits. This application automatically backs off and retries the request when these limits are exceeded.
+
+### Execution syntax
+This application provides multiple commands, each with unique options.
+
+```shell
+python3 token_dispenser.py [-h] command [options]
+```
+
+##### Command line help
+The menu of commands can be retrieved by providing `-h` on the command line with no other arguments.
+
+```shell
+Installation Token management utility.
+
+ _______ __ _______ __ __ __
+| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----.
+|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__|
+|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____|
+|: 1 | |: 1 |
+|::.. . | |::.. . | FalconPy v1.3.4
+`-------' `-------'
+
+_______ _____ _ _ _______ __ _
+ | | | |____/ |______ | \ |
+ | |_____| | \_ |______ | \_|
+
+______ _____ _______ _____ _______ __ _ _______ _______ ______
+| \ | |______ |_____] |______ | \ | |______ |______ |_____/
+|_____/ __|__ ______| | |______ | \_| ______| |______ | \_
+
+ .-------. with ________)
+ |Jackpot| (, / /) , /)
+ ____________|_______|____________ /___, // _ (/ _/_
+ | __ __ ___ _____ __ | ) / (/__(_(_/_/ )_(__
+ | / _\ / / /___\/__ \ / _\ | (_/ .-/
+ | \ \ / / // // / /\ \\ \ 25| (_/ ) ___
+ | _\ \/ /___/ \_// / / \/_\ \ []| __ (__/_____) /)
+ | \__/\____/\___/ \/ \__/ []| (__) / _____ _/_ __ ___//
+ |===_______===_______===_______===| || / (_) / (_(__/ (_(_)(/_
+ ||*| _____ |*| ,_ |*| ___ |*|| || (______)
+ ||*|| ||*| | \ _ |*| |_ | |*|| ||
+ ||*||*BAR*||*| \_(_)|*| / / |*|| ||
+ ||*||_____||*| (_) |*| /_/ |*|| ||
+ ||*|_______|*|_______|*|_______|*||_// Creation date: 11.15.2023
+ | \=___________________________=/ |_/ jshcodes@CrowdStrike
+ _| \_______________________/ |_ WE STOP BREACHES
+(_____________________________________)
+
+positional arguments:
+ Token command Command description
+ list (l) List all tokens [default]
+ create (c) Create tokens
+ revoke (x) Revoke tokens
+ restore (r) Restore tokens
+ update (u) Update tokens
+ delete (d) Delete tokens
+
+optional arguments:
+ -h, --help show this help message and exit
+```
+
+### Commands
+The token dispenser supports 6 primary commands, each accepting optional arguments that alter how the command is performed.
+When using [MSSP mode](#flight-control-mssp-mode-arguments) operations performed cross all tenants.
+> Example: Calling the `list` command while also enabling MSSP mode with the `-m` command line argument will show tokens for the parent and all children.
+
+- [List](#list-tokens) - List all tokens within the environment.
+- [Create](#create-tokens) - Create one or multiple tokens with a specified expiration and label.
+- [Revoke](#revoke-tokens) - Revoke one or multiple tokens by label or ID.
+- [Restore](#restore-tokens) - Restore one or multiple tokens by label or ID.
+- [Update](#update-tokens) - Update the label or expiration for one or multiple tokens by label or ID.
+- [Delete](#delete-tokens) - Delete one or multiple tokens by label or ID.
+
+#### Authentication, display and saving results to a file
+All commands accept universal arguments that may be mixed with command-specific arguments.
+These arguments control configuration elements that are shared across all available commands such as:
+- Authentication
+- Flight Control (MSSP mode)
+- Display options (such as filtering, sorting and formatting)
+- Outputting displayed results to CSV or JSON format
+
+##### Universal arguments
+The following options are available as command line arguments regardless of command performed.
+Universal arguments may be provided in any order.
+
+###### General, display and output arguments
+These arguments allow users to control debug and result display settings.
+Results can also be exported to a file in JSON or CSV format using these options.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+ | `-h` | `--help` | Show help for the specified command and exit. | General |
+ | `-d` | `--debug` | Enable debug. | General |
+ | `-f` FILTER | `--filter` FILTER | Filter results by searching token labels (stemmed search). | Display |
+ | `-o` ORDER_BY | `--order-by` ORDER_BY | Sort key to use for tabular displays. | Display |
+ | `-r` | `--reverse` | Reverses the sort order. | Display |
+ | `-t` TABLE_FORMAT | `--table-format` TABLE_FORMAT | Format to use for tabular output. | Display |
+ | `-v` | `--show-version` | Show FalconPy version in output. | Display |
+ | | `--output-file` OUTPUT_FILE | Output token list results to a CSV or JSON file. | Output |
+ | | `--output-format` OUTPUT_FORMAT | Set output file format.
Allowed options:
| Output |
+
+###### Authentication arguments
+> [!NOTE]
+> The following arguments are not required when you are using [environment authentication](https://www.falconpy.io/Usage/Authenticating-to-the-API.html#environment-authentication).
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| `-k` CLIENT_ID | `--client_id` CLIENT_ID | Falcon API client ID. | Authentication |
+| `-s` CLIENT_SECRET | `--client_secret` CLIENT_SECRET | Falcon API client secret. | Authentication
+
+###### Flight Control (MSSP mode) arguments
+> [!NOTE]
+> The following arguments are not required when you are not using Flight Control.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| `-c` CHILD | `--child` CHILD | CID of the child tenant to target. | MSSP |
+| `-m` | `--mssp` | Flight Control (MSSP) mode.
Commands executed are performed within every tenant unless the parent is explicitly skipped. | MSSP |
+| | `--skip-parent`| Do not execute commands within the parent tenant. | MSSP |
+| | `--show-tenant` | Display tenant CID values as part of execution. | MSSP |
+
+##### Examples
+The following examples demonstrate different universal argument variations.
+
+###### Enable debugging
+Passing the `-d` (`--debug`) argument will enable API debugging for every operation performed.
+
+```shell
+python3 token_dispenser.py -d
+```
+
+###### Filter display results by label
+The `-f` (`--filter`) option will only display results that include the word "Example" in any position within the label.
+
+```shell
+python3 token_dispenser.py -f Example
+```
+
+###### Sort display results
+You can sort results by any column in the display results using the `-o` (`order-by`) argument.
+Using the `-r` (`--reverse`) argument will reverse the sort.
+
+```shell
+python3 token_dispenser.py -o status -r
+```
+
+###### Change the display table format
+You can change the format of the display table to any of the following options using the `-t` (`table-format`) argument.
+
+```shell
+python3 token_dispenser.py -t fancy_grid
+```
+
+###### *Avialable table format options*
+| | | | | | |
+| :-- | :-- | :-- | :-- | :-- | :-- |
+| `plain` | `simple` | `github` | `grid` | `simple_grid` | `rounded_grid` |
+| `heavy_grid` | `mixed_grid` | `double_grid` | `fancy_grid` | `outline` | `simple_outline` |
+| `rounded_outline` | `heavy_outline` | `mixed_outline` | `double_outline` | `fancy_outline` | `pipe` |
+| `orgtbl` | `asciidoc` | `jira` | `presto` | `pretty` | `psql` |
+| `rst` | `mediawiki` | `moinmoin` | `youtrack` | `html` | `unsafehtml` |
+| `latex` | `latex_raw` | `latex_booktabs` | `latex_longtable` | `textile` | `tsv` |
+
+
+###### Authenticating to a single tenant
+If you are not using [Environment Authentication](https://www.falconpy.io/Usage/Authenticating-to-the-API.html#environment-authentication), you will need to provide authentication detail on the command line using the `-k` (`--client-id`) and `-s` (`--client-secret`) arguments.
+
+```shell
+python3 token_dispenser.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET
+```
+
+###### Authenticating to a parent tenant and enabling MSSP mode
+MSSP mode will perform commands against all child tenants and the parent (if not explicitly skipped using the `--skip-parent` argument).
+This includes API calls used to create display results.
+
+```shell
+python3 token_dispenser.py -k $PARENT_CLIENT_ID -s $PARENT_CLIENT_SECRET -m
+```
+
+###### Authenticating as a parent to a single child
+You can also directly authenticate (as a parent) to the child tenant using the `-c` (`--child`) argument.
+This argument does not require MSSP mode and may be provided with or without the `-m` argument.
+
+```shell
+python3 token_dispenser.py -k $PARENT_CLIENT_ID -s $PARENT_CLIENT_SECRET -c $CHILD_TENANT_CID
+```
+
+###### Displaying the tenant ID
+You can display the tenant ID for the parent and child tenants before the operation is performed with the `--show-tenant` argument.
+
+```shell
+python3 token_dispenser.py --show-tenant
+```
+
+---
+
+#### List tokens
+The list command is the default command, and is executed when no command is specified.
+After the execution of any other command, the list command is executed to display the results generated.
+
+There are no list command-specific arguments. All universal arguments are accepted.
+
+##### Command line help (list)
+Command-line help for this command is available when the command is called along with the `-h` argument.
+
+```shell
+usage: token_dispenser.py list [-h] [-d] [-f FILTER] [-o ORDER_BY] [-r] [-t TABLE_FORMAT] [-v] [--output-file OUTPUT_FILE] [--output-format {csv,json}] [-k CLIENT_ID] [-s CLIENT_SECRET] [-c CHILD] [-m] [--skip-parent]
+ [--show-tenant]
+
+ _ _ _
+| | (_) | |
+| | _ ___| |_
+| | | / __| __|
+| |____| \__ \ |_
+|______|_|___/\__|
+
+
+
+optional arguments:
+ -h, --help show this help message and exit
+ -d, --debug Enable debug.
+ -f FILTER, --filter FILTER
+ Filter results by searching token labels (stemmed search).
+ -o ORDER_BY, --order-by ORDER_BY
+ Sort key to use for tabular displays.
+ -r, --reverse Reverses the sort order.
+ -t TABLE_FORMAT, --table-format TABLE_FORMAT
+ Format to use for tabular output.
+ -v, --show-version Show FalconPy version in output.
+ --output-file OUTPUT_FILE
+ Output token list results to a CSV or JSON file.
+ --output-format {csv,json}
+ Set output file format.
+
+authentication arguments (not required if using environment authentication):
+ -k CLIENT_ID, --client_id CLIENT_ID
+ Falcon API client ID
+ -s CLIENT_SECRET, --client_secret CLIENT_SECRET
+ Falcon API client secret
+
+mssp arguments:
+ -c CHILD, --child CHILD
+ CID of the child tenant to target.
+ -m, --mssp Flight Control (MSSP) mode.
+ --skip-parent Do not take action within the parent tenant.
+ --show-tenant Display tenant CID values.
+```
+
+---
+
+#### Create tokens
+Create tokens within your tenant, or across parent and child tenants simultaneously. Supports the creation of multiple tokens with specified expiration dates.
+Expiration may be set by number of days or by specifying a specific date in UTC format.
+
+##### Create command arguments
+There are two create command-specific required arguments (`token-label` and `expiration`). There are also two optional arguments `count` and `force`.
+All [universal arguments](#universal-arguments) are supported and can be mixed with create command arguments in any order or combination.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| | `--force` | Perform the operation without asking for confirmation. | General |
+| `-l` TOKEN_LABEL | `--token-label` TOKEN_LABEL | Label for the token. | Create |
+| `-e` EXPIRATION | `--expiration` EXPIRATION | Token expiration.
(number of days or a specific date in `YYYY-mm-ddTHH:MM:SSZ` format). | Create |
+| `-n` COUNT | `--count` COUNT | Number of tokens to create. | Create |
+
+##### Examples
+The following examples demonstrate different create command variations.
+
+###### Create a single token in a standard tenant
+This example will create a token labelled "ExampleToken" with an expiration of 5 days from now.
+
+```shell
+python3 token_dispenser.py create -l ExampleToken -e 5
+```
+
+##### Flight Control examples
+> [!IMPORTANT]
+> You must provide either the MSSP mode (`-m`) or the child (`-c`) argument in order to execute operations within child tenants.
+
+###### Create a single token across the parent and child tenants
+This example will create a token labelled "ExampleToken" with an expiration 10 days from now in the parent and every child tenant.
+
+```shell
+python3 token_dispenser.py create -l ExampleToken -e 10 -m
+```
+
+###### Create multiple tokens in all child tenants but do not create one in the parent
+This example will create three tokens with a specific expiration date, labelled "ExampleToken1", "ExampleToken2", and "ExampleToken3" within child tenants.
+The parent tenant will remain unchanged as the `skip-parent` argument has been provided.
+
+```shell
+python3 token_dispenser.py create -l ExampleToken -e 2025-01-01T00:00:01Z -n 3 -m --skip-parent
+```
+
+> [!NOTE]
+> To skip the confirmation dialog presented when performing multi-tenant operations, provide the `--force` argument. This argument has no impact on operations where a confirmation dialog is not normally presented.
+
+##### Command line help (create)
+Command-line help for this command is available when the command is called along with the `-h` argument.
+
+```shell
+usage: token_dispenser.py create [-h] -l TOKEN_LABEL -e EXPIRATION [-n COUNT] [--force] [-d] [-f FILTER] [-o ORDER_BY] [-r] [-t TABLE_FORMAT] [-v] [--output-file OUTPUT_FILE] [--output-format {csv,json}] [-k CLIENT_ID]
+ [-s CLIENT_SECRET] [-c CHILD] [-m] [--skip-parent] [--show-tenant]
+
+ _____ _
+ / ____| | |
+| | _ __ ___ __ _| |_ ___
+| | | '__/ _ \/ _` | __/ _ \
+| |____| | | __/ (_| | || __/
+ \_____|_| \___|\__,_|\__\___|
+
+
+
+optional arguments:
+ -h, --help show this help message and exit
+ -n COUNT, --count COUNT
+ Number of tokens to create
+ --force Perform the operation without asking for confirmation.
+ -d, --debug Enable debug.
+ -f FILTER, --filter FILTER
+ Filter results by searching token labels (stemmed search).
+ -o ORDER_BY, --order-by ORDER_BY
+ Sort key to use for tabular displays.
+ -r, --reverse Reverses the sort order.
+ -t TABLE_FORMAT, --table-format TABLE_FORMAT
+ Format to use for tabular output.
+ -v, --show-version Show FalconPy version in output.
+ --output-file OUTPUT_FILE
+ Output token list results to a CSV or JSON file.
+ --output-format {csv,json}
+ Set output file format.
+
+required arguments:
+ -l TOKEN_LABEL, --token-label TOKEN_LABEL
+ Label for the token.
+ -e EXPIRATION, --expiration EXPIRATION
+ Token expiration (number of days or YYYY-mm-ddTHH:MM:SSZ).
+
+authentication arguments (not required if using environment authentication):
+ -k CLIENT_ID, --client_id CLIENT_ID
+ Falcon API client ID
+ -s CLIENT_SECRET, --client_secret CLIENT_SECRET
+ Falcon API client secret
+
+mssp arguments:
+ -c CHILD, --child CHILD
+ CID of the child tenant to target.
+ -m, --mssp Flight Control (MSSP) mode.
+ --skip-parent Do not take action within the parent tenant.
+ --show-tenant Display tenant CID values.
+```
+
+---
+
+#### Revoke tokens
+Revoke tokens within your tenant, or across parent and child tenants simultaneously. Supports the revocation of multiple tokens.
+
+##### Revoke command arguments
+There are two revoke command-specific required arguments (`token-id` and `token-label`). These arguments are mutually exclusive. There is one optional argument `force`.
+All [universal arguments](#universal-arguments) are supported and can be mixed with create command arguments in any order or combination.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| | `--force` | Perform the operation without asking for confirmation. | General |
+| `-i` TOKEN_ID | `--token-id` TOKEN_ID | ID of the token to revoke. | Revoke |
+| `-l` TOKEN_LABEL | `--token-label` TOKEN_LABEL | Label of the token to revoke (starts with match). | Revoke |
+
+##### Examples
+The following examples demonstrate different revoke command variations.
+
+###### Revoke tokens in a standard tenant
+This example will revoke any token with a label starting with "ExampleToken".
+
+```shell
+python3 token_dispenser.py revoke -l ExampleToken
+```
+
+You can also revoke specific tokens by ID.
+
+```shell
+python3 token_dispenser.py delete -i $TOKEN_ID
+```
+
+##### Flight Control examples
+> [!IMPORTANT]
+> You must provide the MSSP mode (`-m`) argument in order to access child tenants. If you wish processing to only occur within child tenants, you must provide the `--skip-parent` argument.
+
+###### Revoke a single token in a child tenant
+This example will revoke a single token within a child tenant.
+
+```shell
+python3 token_dispenser.py revoke -i $TOKEN_ID -c $CHILD_TENANT_CID
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for a token that matches the ID.
+
+```shell
+python3 token_dispenser.py revoke -i $TOKEN_ID -m
+```
+
+###### Revoke tokens in a child tenant that have a label starting with a specific string
+This example will revoke tokens labelled "ExampleToken" (or any variation starting with this string) within child tenants.
+
+```shell
+python3 token_dispenser.py revoke -l ExampleToken -c $CHILD_TENANT_CID
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for labels that match the specified string.
+
+```shell
+python3 token_dispenser.py revoke -l ExampleToken -m
+```
+
+> [!NOTE]
+> To skip the confirmation dialog presented when performing multi-tenant operations, provide the `--force` argument. This argument has no impact on operations where a confirmation dialog is not normally presented.
+
+##### Command line help (revoke)
+Command-line help for this command is available when the command is called along with the `-h` argument.
+
+```shell
+usage: token_dispenser.py revoke [-h] (-i TOKEN_ID | -l TOKEN_LABEL) [--force] [-d] [-f FILTER] [-o ORDER_BY] [-r] [-t TABLE_FORMAT] [-v] [--output-file OUTPUT_FILE] [--output-format {csv,json}] [-k CLIENT_ID]
+ [-s CLIENT_SECRET] [-c CHILD] [-m] [--skip-parent] [--show-tenant]
+
+ _____ _
+| __ \ | |
+| |__) |_____ _____ | | _____
+| _ // _ \ \ / / _ \| |/ / _ \
+| | \ \ __/\ V / (_) | < __/
+|_| \_\___| \_/ \___/|_|\_\___|
+
+
+
+optional arguments:
+ -h, --help show this help message and exit
+ --force Perform the operation without asking for confirmation.
+ -d, --debug Enable debug.
+ -f FILTER, --filter FILTER
+ Filter results by searching token labels (stemmed search).
+ -o ORDER_BY, --order-by ORDER_BY
+ Sort key to use for tabular displays.
+ -r, --reverse Reverses the sort order.
+ -t TABLE_FORMAT, --table-format TABLE_FORMAT
+ Format to use for tabular output.
+ -v, --show-version Show FalconPy version in output.
+ --output-file OUTPUT_FILE
+ Output token list results to a CSV or JSON file.
+ --output-format {csv,json}
+ Set output file format.
+
+required arguments (mutually exclusive):
+ -i TOKEN_ID, --token-id TOKEN_ID
+ ID of the token to revoke.
+ -l TOKEN_LABEL, --token-label TOKEN_LABEL
+ Label of the token to revoke (starts with match).
+
+authentication arguments (not required if using environment authentication):
+ -k CLIENT_ID, --client_id CLIENT_ID
+ Falcon API client ID
+ -s CLIENT_SECRET, --client_secret CLIENT_SECRET
+ Falcon API client secret
+
+mssp arguments:
+ -c CHILD, --child CHILD
+ CID of the child tenant to target.
+ -m, --mssp Flight Control (MSSP) mode.
+ --skip-parent Do not take action within the parent tenant.
+ --show-tenant Display tenant CID values.
+```
+
+---
+
+#### Restore tokens
+Restore tokens within your tenant, or across parent and child tenants simultaneously. Supports the restoration of multiple tokens.
+
+##### Restore command arguments
+There are two restore command-specific required arguments (`token-id` and `token-label`). These arguments are mutually exclusive. There is one optional argument `force`.
+All [universal arguments](#universal-arguments) are supported and can be mixed with create command arguments in any order or combination.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| | `--force` | Perform the operation without asking for confirmation. | General |
+| `-i` TOKEN_ID | `--token-id` TOKEN_ID | ID of the token to restore. | Restore |
+| `-l` TOKEN_LABEL | `--token-label` TOKEN_LABEL | Label of the token to restore (starts with match). | Restore |
+
+##### Examples
+The following examples demonstrate different restore command variations.
+
+###### Restore tokens in a standard tenant
+This example will restore any token with a label starting with "ExampleToken".
+
+```shell
+python3 token_dispenser.py restore -l ExampleToken
+```
+
+You can also restore specific tokens by ID.
+
+```shell
+python3 token_dispenser.py restore -i $TOKEN_ID
+```
+
+##### Flight Control examples
+> [!IMPORTANT]
+> You must provide the MSSP mode (`-m`) argument in order to access child tenants. If you wish processing to only occur within child tenants, you must provide the `--skip-parent` argument.
+
+###### Restore a single token in a child tenant
+This example will restore a single token within a child tenant.
+
+```shell
+python3 token_dispenser.py restore -i $TOKEN_ID -c $CHILD_TENANT_CID
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for a token that matches the ID.
+
+```shell
+python3 token_dispenser.py restore -i $TOKEN_ID -m
+```
+
+###### Restore tokens in a child tenant that have a label starting with a specific string
+This example will restore tokens labelled "ExampleToken" (or any variation starting with this string) within child tenants.
+
+```shell
+python3 token_dispenser.py restore -l ExampleToken -c $CHILD_TENANT_CID
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for labels that match the specified string.
+
+```shell
+python3 token_dispenser.py restore -l ExampleToken -m
+```
+
+> [!NOTE]
+> To skip the confirmation dialog presented when performing multi-tenant operations, provide the `--force` argument. This argument has no impact on operations where a confirmation dialog is not normally presented.
+
+##### Command line help (restore)
+Command-line help for this command is available when the command is called along with the `-h` argument.
+
+```shell
+usage: token_dispenser.py restore [-h] (-i TOKEN_ID | -l TOKEN_LABEL) [--force] [-d] [-f FILTER] [-o ORDER_BY] [-r] [-t TABLE_FORMAT] [-v] [--output-file OUTPUT_FILE] [--output-format {csv,json}] [-k CLIENT_ID]
+ [-s CLIENT_SECRET] [-c CHILD] [-m] [--skip-parent] [--show-tenant]
+
+ _____ _
+| __ \ | |
+| |__) |___ ___| |_ ___ _ __ ___
+| _ // _ \/ __| __/ _ \| '__/ _ \
+| | \ \ __/\__ \ || (_) | | | __/
+|_| \_\___||___/\__\___/|_| \___|
+
+
+
+optional arguments:
+ -h, --help show this help message and exit
+ --force Perform the operation without asking for confirmation.
+ -d, --debug Enable debug.
+ -f FILTER, --filter FILTER
+ Filter results by searching token labels (stemmed search).
+ -o ORDER_BY, --order-by ORDER_BY
+ Sort key to use for tabular displays.
+ -r, --reverse Reverses the sort order.
+ -t TABLE_FORMAT, --table-format TABLE_FORMAT
+ Format to use for tabular output.
+ -v, --show-version Show FalconPy version in output.
+ --output-file OUTPUT_FILE
+ Output token list results to a CSV or JSON file.
+ --output-format {csv,json}
+ Set output file format.
+
+required arguments (mutually exclusive):
+ -i TOKEN_ID, --token-id TOKEN_ID
+ ID of the token to restore.
+ -l TOKEN_LABEL, --token-label TOKEN_LABEL
+ Label of the token to restore (starts with match).
+
+authentication arguments (not required if using environment authentication):
+ -k CLIENT_ID, --client_id CLIENT_ID
+ Falcon API client ID
+ -s CLIENT_SECRET, --client_secret CLIENT_SECRET
+ Falcon API client secret
+
+mssp arguments:
+ -c CHILD, --child CHILD
+ CID of the child tenant to target.
+ -m, --mssp Flight Control (MSSP) mode.
+ --skip-parent Do not take action within the parent tenant.
+ --show-tenant Display tenant CID values.
+```
+
+---
+
+#### Update tokens
+Update tokens within your tenant, or across parent and child tenants simultaneously. Supports the restoration of multiple tokens.
+
+##### Update command arguments
+There are two sets of update command-specific required arguments. The first set includes `token-id` and `token-label` which are mutually exclusive to each other.
+The second set of required arguments includes `add-days`, `expiration` and `new_token_label`. These three are mutually exclusive to each other. There is one optional argument `force`.
+All [universal arguments](#universal-arguments) are supported and can be mixed with create command arguments in any order or combination.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| | `--force` | Perform the operation without asking for confirmation. | General |
+| `-i` TOKEN_ID | `--token-id` TOKEN_ID | ID of the token to update. | Update |
+| `-l` TOKEN_LABEL | `--token-label` TOKEN_LABEL | Label of the token to update (starts with match). | Update |
+| `-a` ADD_DAYS | `--add-days` ADD_DAYS | Add specified number of days to token expiration. |
+| `-e` EXPIRATION | `--expiration` EXPIRATION | Token expiration (`YYYY-mm-ddTHH:MM:SSZ` format). | Update |
+| `-n` NEW_TOKEN_LABEL | `--new-label` NEW_TOKEN_LABEL | New label for the token. | Update |
+
+##### Examples
+The following examples demonstrate different update command variations.
+
+###### Update tokens in a standard tenant to extend the expiration
+This example will update all tokens with a label starting with "ExampleToken" and add 5 days to the expiration.
+
+```shell
+python3 token_dispenser.py update -l ExampleToken -a 5
+```
+
+You can also update specific tokens by ID.
+
+```shell
+python3 token_dispenser.py update -i $TOKEN_ID -a 5
+```
+
+###### Update tokens in a standard tenant to a specific expiration
+This example will update all tokens with a label starting with "ExampleToken" to have the specified expiration date.
+
+```shell
+python3 token_dispenser.py update -l ExampleToken -e 2025-01-01T12:01:01Z
+```
+
+You can also perform this update on a specific token by providing the ID.
+
+```shell
+python3 token_dispenser.py update -i $TOKEN_ID -e 2025-01-01T12:01:01Z
+```
+
+###### Change the label of tokens within a standard tenant
+This example will change the label for any token with a label starting with "ExampleToken" to be "NewExampleToken". If multiple tokens are renamed within a tenant, a number will be appended at the end of each.
+
+```shell
+python3 token_dispenser.py update -l ExampleToken -n NewExampleToken
+```
+
+You can also update a token label by providing the specific token ID.
+
+```shell
+python3 token_dispenser.py delete -i $TOKEN_ID -n NewExampleToken
+```
+
+##### Flight Control examples
+> [!IMPORTANT]
+> You must provide the MSSP mode (`-m`) argument in order to access child tenants. If you wish processing to only occur within child tenants, you must provide the `--skip-parent` argument.
+
+###### Update a single token to extend the expiration
+This example will update a single token within a parent or child tenant to add 5 days to the expiration.
+
+```shell
+python3 token_dispenser.py update -i $TOKEN_ID -c $CHILD_TENANT_CID -a 5
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for the token with the matching ID.
+
+```shell
+python3 token_dispenser.py update -i $TOKEN_ID -m -a 5
+```
+
+###### Update tokens that have a label starting with a specific string to a specific expiration
+This example will update tokens labelled "ExampleToken" (or any variation starting with this string) within the parent and child tenants to have the specified expiration date.
+
+```shell
+python3 token_dispenser.py update -l ExampleToken -c $CHILD_TENANT_CID -e 2025-01-01T12:01:01Z
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for labels that match the specified string.
+
+```shell
+python3 token_dispenser.py update -l ExampleToken -m -e 2025-01-01T12:01:01Z
+```
+
+###### Changing the label of a token
+This example will change the label for the token "ExampleToken" to be "NewExampleToken" within the tenant it is found.
+
+```shell
+python3 token_dispenser.py update -i $TOKEN_ID -m -n NewExampleToken
+```
+
+This example will change the label for any token matching "ExampleToken" to be "NewExampleToken" within the tenant it is found. If multiple tokens are updated within a tenant, a number will be appended to the end of each.
+
+```shell
+python3 token_dispenser.py update -l ExampleToken -m -n NewExampleToken
+```
+
+> [!NOTE]
+> To skip the confirmation dialog presented when performing multi-tenant operations, provide the `--force` argument. This argument has no impact on operations where a confirmation dialog is not normally presented.
+
+##### Command line help (update)
+Command-line help for this command is available when the command is called along with the `-h` argument.
+
+```shell
+usage: token_dispenser.py update [-h] (-i TOKEN_ID | -l TOKEN_LABEL) (-a ADD_DAYS | -e EXPIRATION | -n NEW_TOKEN_LABEL) [--force] [-d] [-f FILTER] [-o ORDER_BY] [-r] [-t TABLE_FORMAT] [-v] [--output-file OUTPUT_FILE]
+ [--output-format {csv,json}] [-k CLIENT_ID] [-s CLIENT_SECRET] [-c CHILD] [-m] [--skip-parent] [--show-tenant]
+
+ _ _ _ _
+| | | | | | | |
+| | | |_ __ __| | __ _| |_ ___
+| | | | '_ \ / _` |/ _` | __/ _ \
+| |__| | |_) | (_| | (_| | || __/
+ \____/| .__/ \__,_|\__,_|\__\___|
+ | |
+ |_|
+
+optional arguments:
+ -h, --help show this help message and exit
+ --force Perform the operation without asking for confirmation.
+ -d, --debug Enable debug.
+ -f FILTER, --filter FILTER
+ Filter results by searching token labels (stemmed search).
+ -o ORDER_BY, --order-by ORDER_BY
+ Sort key to use for tabular displays.
+ -r, --reverse Reverses the sort order.
+ -t TABLE_FORMAT, --table-format TABLE_FORMAT
+ Format to use for tabular output.
+ -v, --show-version Show FalconPy version in output.
+ --output-file OUTPUT_FILE
+ Output token list results to a CSV or JSON file.
+ --output-format {csv,json}
+ Set output file format.
+
+required arguments:
+ -i TOKEN_ID, --token-id TOKEN_ID
+ ID of the token to update.
+ -l TOKEN_LABEL, --token-label TOKEN_LABEL
+ Label of the token to update (starts with match).
+ -a ADD_DAYS, --add-days ADD_DAYS
+ Add specified number of days to token expiration.
+ -e EXPIRATION, --expiration EXPIRATION
+ Token expiration (YYYY-mm-ddTHH:MM:SSZ).
+ -n NEW_TOKEN_LABEL, --new-label NEW_TOKEN_LABEL
+ New label for the token.
+
+authentication arguments (not required if using environment authentication):
+ -k CLIENT_ID, --client_id CLIENT_ID
+ Falcon API client ID
+ -s CLIENT_SECRET, --client_secret CLIENT_SECRET
+ Falcon API client secret
+
+mssp arguments:
+ -c CHILD, --child CHILD
+ CID of the child tenant to target.
+ -m, --mssp Flight Control (MSSP) mode.
+ --skip-parent Do not take action within the parent tenant.
+ --show-tenant Display tenant CID values.
+```
+
+#### Delete tokens
+Delete tokens within your tenant, or across parent and child tenants simultaneously. Supports the restoration of multiple tokens.
+
+##### Delete command arguments
+There are two delete command-specific required arguments (`token-id` and `token-label`). These arguments are mutually exclusive. There is one optional argument `force`.
+All [universal arguments](#universal-arguments) are supported and can be mixed with create command arguments in any order or combination.
+
+| Argument | Long Argument | Description | Category |
+| :-- | :-- | :-- | :-- |
+| | `--force` | Perform the operation without asking for confirmation. | General |
+| `-i` TOKEN_ID | `--token-id` TOKEN_ID | ID of the token to delete. | Delete |
+| `-l` TOKEN_LABEL | `--token-label` TOKEN_LABEL | Label of the token to delete (starts with match). | Delete |
+
+##### Examples
+The following examples demonstrate different delete command variations.
+
+###### Delete tokens in a standard tenant
+This example will delete any token with a label starting with "ExampleToken".
+
+```shell
+python3 token_dispenser.py delete -l ExampleToken
+```
+
+You can also delete specific tokens by ID.
+
+```shell
+python3 token_dispenser.py delete -i $TOKEN_ID
+```
+
+##### Flight Control examples
+> [!IMPORTANT]
+> You must provide the MSSP mode (`-m`) argument in order to access child tenants. If you wish processing to only occur within child tenants, you must provide the `--skip-parent` argument.
+
+###### Delete a single token in a child tenant
+This example will delete a single token within a child tenant.
+
+```shell
+python3 token_dispenser.py delete -i $TOKEN_ID -c $CHILD_TENANT_CID
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for a token that matches the ID.
+
+```shell
+python3 token_dispenser.py delete -i $TOKEN_ID -m
+```
+
+###### Delete tokens in a child tenant that have a label starting with a specific string
+This example will delete tokens labelled "ExampleToken" (or any variation starting with this string) within child tenants.
+
+```shell
+python3 token_dispenser.py delete -l ExampleToken -c $CHILD_TENANT_CID
+```
+
+You can also accomplish this leveraging MSSP mode. All child tenants will be searched for labels that match the specified string.
+
+```shell
+python3 token_dispenser.py delete -l ExampleToken -m
+```
+
+> [!NOTE]
+> To skip the confirmation dialog presented when performing multi-tenant operations, provide the `--force` argument. This argument has no impact on operations where a confirmation dialog is not normally presented.
+
+##### Command line help (delete)
+Command-line help for this command is available when the command is called along with the `-h` argument.
+
+```shell
+usage: token_dispenser.py delete [-h] (-i TOKEN_ID | -l TOKEN_LABEL) [--force] [-d] [-f FILTER] [-o ORDER_BY] [-r] [-t TABLE_FORMAT] [-v] [--output-file OUTPUT_FILE] [--output-format {csv,json}] [-k CLIENT_ID]
+ [-s CLIENT_SECRET] [-c CHILD] [-m] [--skip-parent] [--show-tenant]
+
+ _____ _ _
+| __ \ | | | |
+| | | | ___| | ___| |_ ___
+| | | |/ _ \ |/ _ \ __/ _ \
+| |__| | __/ | __/ || __/
+|_____/ \___|_|\___|\__\___|
+
+
+
+optional arguments:
+ -h, --help show this help message and exit
+ --force Perform the operation without asking for confirmation.
+ -d, --debug Enable debug.
+ -f FILTER, --filter FILTER
+ Filter results by searching token labels (stemmed search).
+ -o ORDER_BY, --order-by ORDER_BY
+ Sort key to use for tabular displays.
+ -r, --reverse Reverses the sort order.
+ -t TABLE_FORMAT, --table-format TABLE_FORMAT
+ Format to use for tabular output.
+ -v, --show-version Show FalconPy version in output.
+ --output-file OUTPUT_FILE
+ Output token list results to a CSV or JSON file.
+ --output-format {csv,json}
+ Set output file format.
+
+required arguments (mutually exclusive):
+ -i TOKEN_ID, --token-id TOKEN_ID
+ ID of the token to remove.
+ -l TOKEN_LABEL, --token-label TOKEN_LABEL
+ Label of the token to remove (starts with match).
+
+authentication arguments (not required if using environment authentication):
+ -k CLIENT_ID, --client_id CLIENT_ID
+ Falcon API client ID
+ -s CLIENT_SECRET, --client_secret CLIENT_SECRET
+ Falcon API client secret
+
+mssp arguments:
+ -c CHILD, --child CHILD
+ CID of the child tenant to target.
+ -m, --mssp Flight Control (MSSP) mode.
+ --skip-parent Do not take action within the parent tenant.
+ --show-tenant Display tenant CID values.
+```
+
+### Example source code
+The source code for this example can be found [here](token_dispenser.py).
\ No newline at end of file
diff --git a/samples/installation_tokens/token_dispenser.py b/samples/installation_tokens/token_dispenser.py
new file mode 100755
index 000000000..836e213ff
--- /dev/null
+++ b/samples/installation_tokens/token_dispenser.py
@@ -0,0 +1,999 @@
+r"""Installation Token management utility.
+
+ _______ __ _______ __ __ __
+| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----.
+|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__|
+|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____|
+|: 1 | |: 1 |
+|::.. . | |::.. . | FalconPy v1.3.4
+`-------' `-------'
+
+_______ _____ _ _ _______ __ _
+ | | | |____/ |______ | \ |
+ | |_____| | \_ |______ | \_|
+
+______ _____ _______ _____ _______ __ _ _______ _______ ______
+| \ | |______ |_____] |______ | \ | |______ |______ |_____/
+|_____/ __|__ ______| | |______ | \_| ______| |______ | \_
+
+ .-------. with ________)
+ |Jackpot| (, / /) , /)
+ ____________|_______|____________ /___, // _ (/ _/_
+ | __ __ ___ _____ __ | ) / (/__(_(_/_/ )_(__
+ | / _\ / / /___\/__ \ / _\ | (_/ .-/
+ | \ \ / / // // / /\ \\ \ 25| (_/ ) ___
+ | _\ \/ /___/ \_// / / \/_\ \ []| __ (__/_____) /)
+ | \__/\____/\___/ \/ \__/ []| (__) / _____ _/_ __ ___//
+ |===_______===_______===_______===| || / (_) / (_(__/ (_(_)(/_
+ ||*| _____ |*| ,_ |*| ___ |*|| || (______)
+ ||*|| ||*| | \ _ |*| |_ | |*|| ||
+ ||*||*BAR*||*| \_(_)|*| / / |*|| ||
+ ||*||_____||*| (_) |*| /_/ |*|| ||
+ ||*|_______|*|_______|*|_______|*||_// Creation date: 11.15.2023
+ | \=___________________________=/ |_/ jshcodes@CrowdStrike
+ _| \_______________________/ |_ WE STOP BREACHES
+(_____________________________________)
+"""
+# _____ _______ _____ _____ ______ _______ _______
+# | | | | |_____] | | |_____/ | |______
+# __|__ | | | | |_____| | \_ | ______|
+#
+import sys
+from argparse import ArgumentParser, Namespace, RawTextHelpFormatter, _SubParsersAction
+from copy import deepcopy
+from csv import writer
+from datetime import datetime, timedelta
+from json import dump
+from logging import basicConfig, DEBUG
+from os import getenv
+from random import randrange
+from time import sleep
+from typing import Tuple, Callable, List, Dict
+try:
+ from pyfiglet import figlet_format
+except ImportError as no_figlet:
+ raise SystemExit("The pyfiglet library is required to use this program.") from no_figlet
+try:
+ from tabulate import tabulate
+except ImportError as no_tabulate:
+ raise SystemExit("The tabulate library is required to use this program.") from no_tabulate
+try:
+ from falconpy import (
+ APIError,
+ InstallationTokens,
+ SensorDownload,
+ FlightControl,
+ Result,
+ version
+ )
+except ImportError as no_falconpy:
+ raise SystemExit("The CrowdStrike FalconPy library (version 1.3.4 or greater) is required "
+ "to use this program."
+ ) from no_falconpy
+
+
+# _____ _____ _______ _____ _____ __ _ _______
+# | | |_____] | | | | | \ | |_____| |
+# |_____| | | __|__ |_____| | \_| | | |_____
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def add_opt_arguments(sbp: ArgumentParser) -> ArgumentParser:
+ """Add shared optional arguments to the provided command line argument subparser."""
+ sbp.add_argument("-d", "--debug", help="Enable debug.", default=False, action="store_true")
+ sbp.add_argument("-f", "--filter",
+ help="Filter results by searching token labels (stemmed search)."
+ )
+ sbp.add_argument("-o", "--order-by",
+ help="Sort key to use for tabular displays.",
+ dest="order_by",
+ default="label"
+ )
+ sbp.add_argument("-r", "--reverse",
+ help="Reverses the sort order.",
+ default=False,
+ action="store_true"
+ )
+ sbp.add_argument("-t", "--table-format",
+ dest="table_format",
+ help="Format to use for tabular output.",
+ default="simple"
+ )
+ sbp.add_argument("-v", "--show-version",
+ dest="show_version",
+ help="Show FalconPy version in output.",
+ default=False,
+ action="store_true"
+ )
+ sbp.add_argument("--output-file",
+ dest="output_file",
+ help="Output token list results to a CSV or JSON file.",
+ default=None
+ )
+ sbp.add_argument("--output-format",
+ dest="output_format",
+ help="Set output file format.",
+ default="csv",
+ choices=["csv", "json"]
+ )
+ auth = sbp.add_argument_group("authentication arguments "
+ "(not required if using environment authentication)")
+ auth.add_argument("-k", "--client_id",
+ help="Falcon API client ID",
+ default=getenv("FALCON_CLIENT_ID")
+ )
+ auth.add_argument("-s", "--client_secret",
+ help="Falcon API client secret",
+ default=getenv("FALCON_CLIENT_SECRET")
+ )
+ mssp = sbp.add_argument_group("mssp arguments")
+ mssp.add_argument("-c", "--child",
+ dest="child",
+ help="CID of the child tenant to target.",
+ default=None
+ )
+ mssp.add_argument("-m", "--mssp",
+ dest="mssp",
+ help="Flight Control (MSSP) mode.",
+ default=False,
+ action="store_true"
+ )
+ mssp.add_argument("--skip-parent",
+ dest="skip_parent",
+ help="Do not take action within the parent tenant.",
+ action="store_true",
+ default=False
+ )
+ mssp.add_argument("--show-tenant",
+ dest="show_tenant",
+ help="Display tenant CID values.",
+ action="store_true",
+ default=False
+ )
+ return sbp
+
+
+def add_force_argument(sbp: ArgumentParser) -> ArgumentParser:
+ """Add shared optional arguments to the provided command line argument subparser."""
+ sbp.add_argument("--force",
+ help="Perform the operation without asking for confirmation.",
+ action="store_true",
+ default=False
+ )
+ return sbp
+
+
+def extra_args(subp: ArgumentParser) -> ArgumentParser:
+ """Add in optional arguments along with the force argument."""
+ subp = add_force_argument(subp)
+ subp = add_opt_arguments(subp)
+ return subp
+
+
+# | _____ _______ _______
+# | | |______ |
+# |_____ __|__ ______| |
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def handle_list_arguments(sub: _SubParsersAction, head: str) -> ArgumentParser:
+ """Handle list command arguments."""
+ do_list: ArgumentParser = sub.add_parser("list",
+ help="List all tokens [default]",
+ aliases=["l"],
+ description=figlet_format("List", font=head),
+ formatter_class=RawTextHelpFormatter
+ )
+ do_list = add_opt_arguments(do_list)
+ return do_list
+
+
+def show_tenant_list(arg_list: Namespace, cids_to_show: list):
+ """Show CIDs for all tenants searched."""
+ if arg_list.mssp and arg_list.show_tenant:
+ parent = False
+ for kid in cids_to_show:
+ if not parent:
+ print(f"Tenant: {kid}")
+ parent = True
+ else:
+ print(f"Child tenant: {kid}")
+
+
+# | _____ _______ _______
+# | | |______ |
+# |_____ __|__ ______| |
+#
+def show_all_tokens(sdk: InstallationTokens, cmdline: str, filter_str: str = None):
+ """Display every token in the tenant (or across all tenants)."""
+ hold_creds = sdk.auth_object.creds
+ this_cid, cid_list = get_cid_ids(sdk, cmdline)
+ show_tenant_list(cmdline, cid_list)
+ token_details = []
+ ptokens = []
+ for cid in cid_list:
+ if cid != this_cid:
+ hold_creds["member_cid"] = cid
+ api = sdk
+ if cmdline.mssp and (len(cid_list) > 1 and ptokens):
+ api = InstallationTokens(creds=hold_creds, pythonic=True, debug=cmdline.debug)
+ token_details = get_all_tokens(api, cmdline, filter_str, cid, token_details)
+ if cid == this_cid and not ptokens:
+ ptokens = deepcopy(token_details)
+ display_tokens(token_details, cmdline, ptokens)
+
+
+# _______ ______ _______ _______ _______ _______
+# | |_____/ |______ |_____| | |______
+# |_____ | \_ |______ | | | |______
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def handle_create_arguments(sub: _SubParsersAction, head: str) -> ArgumentParser:
+ """Handle create command arguments."""
+ do_create: ArgumentParser = sub.add_parser("create",
+ help="Create tokens",
+ aliases=["c"],
+ description=figlet_format("Create", font=head),
+ formatter_class=RawTextHelpFormatter
+ )
+ create_req = do_create.add_argument_group("required arguments")
+ create_req.add_argument("-l", "--token-label",
+ dest="token_label",
+ help="Label for the token.",
+ required=True
+ )
+ create_req.add_argument("-e", "--expiration",
+ help="Token expiration (number of days or YYYY-mm-ddTHH:MM:SSZ).",
+ required=True
+ )
+ do_create.add_argument("-n", "--count", help="Number of tokens to create.", type=int, default=1)
+ do_create = extra_args(do_create)
+ return do_create
+
+
+# _______ ______ _______ _______ _______ _______
+# | |_____/ |______ |_____| | |______
+# |_____ | \_ |______ | | | |______
+#
+def create_token(sdk: InstallationTokens, cmdline: Namespace):
+ """Create a token with the specified expiration and label."""
+ hold_creds = sdk.auth_object.creds
+ this_cid, cids = get_cid_ids(sdk, cmdline)
+ token_expiration = cmdline.expiration
+ cids_to_process = [
+ c for c in cids if (cmdline.skip_parent and not c == this_cid) or not cmdline.skip_parent
+ ]
+ for cid in cids_to_process:
+ if cid != this_cid:
+ hold_creds["member_cid"] = cid
+ api = sdk
+ if cmdline.mssp and len(cids) > 1:
+ api = InstallationTokens(creds=hold_creds, pythonic=True, debug=cmdline.debug)
+ try:
+ if int(cmdline.expiration) > 0:
+ token_expiration = (
+ datetime.now() + timedelta(days=int(cmdline.expiration))
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
+ else:
+ raise SystemExit("Token expiration days must be an integer greater than zero.")
+ except ValueError:
+ pass
+
+ for num in range(1, cmdline.count+1):
+ label = f"{cmdline.token_label}{num if cmdline.count > 1 else ''}"
+ create_result = sdk_operation(api.tokens_create,
+ LONG_WAIT,
+ cmdline.debug,
+ label=label,
+ expires_timestamp=token_expiration
+ )
+ if create_result.errors:
+ for error in create_result.errors:
+ print(f"NONFATAL {error['code']} ERROR: {error['message']}")
+
+
+# ______ _______ _ _ _____ _ _ _______
+# |_____/ |______ \ / | | |____/ |______
+# | \_ |______ \/ |_____| | \_ |______
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def handle_revoke_arguments(sub: _SubParsersAction, head: str) -> ArgumentParser:
+ """Handle revoke command arguments."""
+ do_revoke: ArgumentParser = sub.add_parser("revoke",
+ help="Revoke tokens",
+ aliases=["x"],
+ description=figlet_format("Revoke", font=head),
+ formatter_class=RawTextHelpFormatter
+ )
+ revoke_req = do_revoke.add_argument_group("required arguments (mutually exclusive)")
+ revoke_grp = revoke_req.add_mutually_exclusive_group(required=True)
+ revoke_grp.add_argument("-i", "--token-id", dest="token_id", help="ID of the token to revoke.")
+ revoke_grp.add_argument("-l", "--token-label",
+ dest="token_label",
+ help="Label of the token to revoke (starts with match)."
+ )
+ do_revoke = extra_args(do_revoke)
+ return do_revoke
+
+
+# ______ _______ _______ _______ _____ ______ _______
+# |_____/ |______ |______ | | | |_____/ |______
+# | \_ |______ ______| | |_____| | \_ |______
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def handle_restore_arguments(sub: _SubParsersAction, head: str) -> ArgumentParser:
+ """Handle restore command arguments."""
+ do_restore: ArgumentParser = sub.add_parser("restore",
+ help="Restore tokens",
+ aliases=["r"],
+ description=figlet_format("Restore", font=head),
+ formatter_class=RawTextHelpFormatter
+ )
+ restore_req = do_restore.add_argument_group("required arguments (mutually exclusive)")
+ restore_grp = restore_req.add_mutually_exclusive_group(required=True)
+ restore_grp.add_argument("-i", "--token-id",
+ dest="token_id",
+ help="ID of the token to restore."
+ )
+ restore_grp.add_argument("-l", "--token-label",
+ dest="token_label",
+ help="Label of the token to restore (starts with match)."
+ )
+ do_restore = extra_args(do_restore)
+ return do_restore
+
+
+# ______ _______ _ _ _____ _ _ _______ _______ __ _ ______
+# |_____/ |______ \ / | | |____/ |______ |_____| | \ | | \
+# | \_ |______ \/ |_____| | \_ |______ | | | \_| |_____/
+# ______ _______ _______ _______ _____ ______ _______
+# |_____/ |______ |______ | | | |_____/ |______
+# | \_ |______ ______| | |_____| | \_ |______
+#
+def token_revocation(sdk: InstallationTokens, cmdline: Namespace, revoking: bool = False):
+ """Revoke a token by ID or name."""
+ hold_creds = sdk.auth_object.creds
+ this_cid, cids = get_cid_ids(sdk, cmdline)
+ cids_to_process = [
+ c for c in cids if (cmdline.skip_parent and not c == this_cid) or not cmdline.skip_parent
+ ]
+ for cid in cids_to_process:
+ if cid != this_cid:
+ hold_creds["member_cid"] = cid
+ api = sdk
+ if cmdline.mssp and len(cids) > 1:
+ api = InstallationTokens(creds=hold_creds, pythonic=True, debug=cmdline.debug)
+
+ if cmdline.token_id:
+ revoke_result = sdk_operation(api.tokens_update,
+ SHORT_WAIT,
+ cmdline.debug,
+ ids=cmdline.token_id,
+ revoked=revoking
+ )
+ if revoke_result.errors:
+ for error in revoke_result.errors:
+ print(f"NONFATAL {error['code']} ERROR: {error['message']} ({error['id']})")
+ if cmdline.token_label:
+ token_lookup = sdk_operation(api.tokens_query,
+ LONG_WAIT,
+ cmdline.debug,
+ filter=f"label:*'{cmdline.token_label}*'"
+ )
+ if token_lookup.status_code == 200 and len(token_lookup.data):
+ for returned_token_id in token_lookup.data:
+ sdk_operation(api.tokens_update,
+ LONG_WAIT,
+ cmdline.debug,
+ ids=returned_token_id,
+ revoked=revoking
+ )
+ else:
+ print(f"NONFATAL 404 ERROR: Not Found ({cmdline.token_label})")
+
+
+# _ _ _____ ______ _______ _______ _______
+# | | |_____] | \ |_____| | |______
+# |_____| | |_____/ | | | |______
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def handle_update_arguments(sub: _SubParsersAction, head: str) -> ArgumentParser:
+ """Handle update command arguments."""
+ do_update: ArgumentParser = sub.add_parser("update",
+ help="Update tokens",
+ aliases=["u"],
+ description=figlet_format("Update", font=head),
+ formatter_class=RawTextHelpFormatter
+ )
+ update_req = do_update.add_argument_group("required arguments")
+ update_grp1 = update_req.add_mutually_exclusive_group(required=True)
+ update_grp1.add_argument("-i", "--token-id",
+ dest="token_id",
+ help="ID of the token to update."
+ )
+ update_grp1.add_argument("-l", "--token-label",
+ dest="token_label",
+ help="Label of the token to update (starts with match)."
+ )
+ update_grp2 = update_req.add_mutually_exclusive_group(required=True)
+ update_grp2.add_argument("-a", "--add-days",
+ help="Add specified number of days to token expiration."
+ )
+ update_grp2.add_argument("-e", "--expiration", help="Token expiration (YYYY-mm-ddTHH:MM:SSZ).")
+ update_grp2.add_argument("-n", "--new-label",
+ dest="new_token_label",
+ help="New label for the token."
+ )
+ do_update = extra_args(do_update)
+ return do_update
+
+
+# _ _ _____ ______ _______ _______ _______ ______ __ __ _____ ______
+# | | |_____] | \ |_____| | |______ |_____] \_/ | | \
+# |_____| | |_____/ | | | |______ |_____] | __|__ |_____/
+#
+def update_token_by_id(sdk: InstallationTokens, cmdline: Namespace):
+ """Update a token by ID."""
+ hold_creds = sdk.auth_object.creds
+ this_cid, cids = get_cid_ids(sdk, cmdline)
+ cids_to_process = [
+ c for c in cids if (cmdline.skip_parent and not c == this_cid) or not cmdline.skip_parent
+ ]
+ for cid in cids_to_process:
+ if cid != this_cid:
+ hold_creds["member_cid"] = cid
+ api = sdk
+ if cmdline.mssp and len(cids) > 1:
+ api = InstallationTokens(creds=hold_creds, pythonic=True, debug=cmdline.debug)
+ updates = {}
+ if cmdline.new_token_label:
+ updates["label"] = cmdline.new_token_label
+ if cmdline.expiration:
+ updates["expires_timestamp"] = cmdline.expiration
+ if cmdline.add_days:
+ exp = sdk_operation(api.tokens_read,
+ LONG_WAIT,
+ cmdline.debug,
+ ids=cmdline.token_id
+ )
+ if exp.status_code == 200 and len(exp.data):
+ new_exp = datetime.strptime(exp.data[0]["expires_timestamp"], "%Y-%m-%dT%H:%M:%SZ")
+ updates["expires_timestamp"] = (
+ new_exp + timedelta(days=int(cmdline.add_days))
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
+ if updates:
+ updates["ids"] = cmdline.token_id
+ update_result = sdk_operation(api.tokens_update, SHORT_WAIT, cmdline.debug, **updates)
+ if update_result.errors:
+ for error in update_result.errors:
+ print(f"NONFATAL {error['code']} ERROR: {error['message']} ({error['id']})")
+
+
+# _ _ _____ ______ _______ _______ _______
+# | | |_____] | \ |_____| | |______
+# |_____| | |_____/ | | | |______
+# ______ __ __ _______ ______ _______
+# |_____] \_/ | |_____| |_____] |______ |
+# |_____] | |_____ | | |_____] |______ |_____
+#
+def update_token_by_label(sdk: InstallationTokens, cmdline: Namespace):
+ """Update a token by label."""
+ hold_creds = sdk.auth_object.creds
+ this_cid, cids = get_cid_ids(sdk, cmdline)
+ cids_to_process = [
+ c for c in cids if (cmdline.skip_parent and not c == this_cid) or not cmdline.skip_parent
+ ]
+ for cid in cids_to_process:
+ if cid != this_cid:
+ hold_creds["member_cid"] = cid
+ api = sdk
+ if cmdline.mssp and len(cids) > 1:
+ api = InstallationTokens(creds=hold_creds, pythonic=True, debug=cmdline.debug)
+ updates = {}
+ if cmdline.new_token_label:
+ updates["label"] = cmdline.new_token_label
+ if cmdline.expiration:
+ updates["expires_timestamp"] = cmdline.expiration
+ token_lookup = sdk_operation(api.tokens_query,
+ LONG_WAIT,
+ cmdline.debug,
+ filter=f"label:*'{cmdline.token_label}*'"
+ )
+ if token_lookup.status_code == 200 and len(token_lookup.data):
+ loop = 1
+ for returned_token_id in token_lookup.data:
+ if cmdline.add_days:
+ exp = sdk_operation(api.tokens_read,
+ LONG_WAIT,
+ cmdline.debug,
+ ids=returned_token_id
+ )
+ if exp.status_code == 200 and len(exp.data):
+ new_exp = datetime.strptime(exp.data[0]["expires_timestamp"],
+ "%Y-%m-%dT%H:%M:%SZ"
+ )
+ updates["expires_timestamp"] = (
+ new_exp + timedelta(days=int(cmdline.add_days))
+ ).strftime("%Y-%m-%dT%H:%M:%SZ")
+ if cmdline.new_token_label and len(token_lookup.data) > 1:
+ updates["label"] = f"{cmdline.new_token_label}{loop}"
+ if updates:
+ updates["ids"] = returned_token_id
+ sdk_operation(api.tokens_update, LONG_WAIT, cmdline.debug, **updates)
+ loop += 1
+ else:
+ print(f"NONFATAL 404 ERROR: Not Found ({cmdline.token_label})")
+
+
+# ______ _______ _______ _______ _______
+# | \ |______ | |______ | |______
+# |_____/ |______ |_____ |______ | |______
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def handle_delete_arguments(sub: _SubParsersAction, head: str) -> ArgumentParser:
+ """Handle delete command arguments."""
+ do_delete: ArgumentParser = sub.add_parser("delete",
+ help="Delete tokens",
+ aliases=["d"],
+ description=figlet_format("Delete", font=head),
+ formatter_class=RawTextHelpFormatter
+ )
+ delete_req = do_delete.add_argument_group("required arguments (mutually exclusive)")
+ delete_grp = delete_req.add_mutually_exclusive_group(required=True)
+ delete_grp.add_argument("-i", "--token-id", dest="token_id", help="ID of the token to remove.")
+ delete_grp.add_argument("-l", "--token-label",
+ dest="token_label",
+ help="Label of the token to remove (starts with match)."
+ )
+ do_delete = extra_args(do_delete)
+ return do_delete
+
+
+# ______ _______ _______ _______ _______
+# | \ |______ | |______ | |______
+# |_____/ |______ |_____ |______ | |______
+#
+def delete_token(sdk: InstallationTokens, cmdline: Namespace):
+ """Delete a token by ID or name."""
+ hold_creds = sdk.auth_object.creds
+ this_cid, cids = get_cid_ids(sdk, cmdline)
+ cids_to_process = [
+ c for c in cids if (cmdline.skip_parent and not c == this_cid) or not cmdline.skip_parent
+ ]
+ for cid in cids_to_process:
+ if cid != this_cid:
+ hold_creds["member_cid"] = cid
+ api = sdk
+ if cmdline.mssp and len(cids) > 1:
+ api = InstallationTokens(creds=hold_creds, pythonic=True, debug=cmdline.debug)
+ if cmdline.token_id:
+ delete_result = sdk_operation(api.tokens_delete,
+ SHORT_WAIT,
+ cmdline.debug,
+ ids=cmdline.token_id
+ )
+ if delete_result.errors:
+ for error in delete_result.errors:
+ print(f"NONFATAL {error['code']} ERROR: {error['message']} ({error['id']})")
+ if cmdline.token_label:
+ token_lookup = sdk_operation(api.tokens_query,
+ LONG_WAIT,
+ cmdline.debug,
+ filter=f"label:*'{cmdline.token_label}*'"
+ )
+ if token_lookup.status_code == 200 and len(token_lookup.data):
+ for returned_token_id in token_lookup.data:
+ sdk_operation(api.tokens_delete,
+ LONG_WAIT,
+ cmdline.debug,
+ ids=returned_token_id
+ )
+ else:
+ print(f"NONFATAL 404 ERROR: Not Found ({cmdline.token_label})")
+
+
+# _____ _______ ______ _______ _______
+# |_____] |_____| |_____/ |______ |______
+# | | | | \_ ______| |______
+# _______ _____ _______ _______ _______ __ _ ______ _____ __ _ _______
+# | | | | | | | | | |_____| | \ | | \ | | | \ | |______
+# |_____ |_____| | | | | | | | | | \_| |_____/ |_____ __|__ | \_| |______
+# _______ ______ ______ _ _ _______ _______ __ _ _______ _______
+# |_____| |_____/ | ____ | | | | | |______ | \ | | |______
+# | | | \_ |_____| |_____| | | | |______ | \_| | ______|
+#
+def consume_arguments() -> Tuple[Namespace, ArgumentParser]:
+ """Retrieve any provided command line arguments."""
+ subcommands = [
+ "create", "c", "list", "l", "delete", "d", "revoke", "x", "restore", "r", "update", "u"
+ ]
+ parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
+ header_font = "big"
+ subparsers = parser.add_subparsers(help="Command description",
+ dest="subcommand",
+ required=False,
+ metavar="Token command"
+ )
+ handle_list_arguments(subparsers, header_font) # List
+ handle_create_arguments(subparsers, header_font) # Create
+ handle_revoke_arguments(subparsers, header_font) # Revoke
+ handle_restore_arguments(subparsers, header_font) # Restore
+ handle_update_arguments(subparsers, header_font) # Update
+ handle_delete_arguments(subparsers, header_font) # Delete
+ # Force "list" as the default subcommand without breaking the help processor
+ if len(sys.argv) == 1:
+ sys.argv.append("list")
+ if sys.argv[1].lower() not in subcommands and "-h" not in sys.argv:
+ sys.argv.insert(1, "list")
+ else:
+ if sys.argv[1].lower() not in subcommands:
+ sys.argv = [sys.argv[0], "-h"]
+ return parser.parse_args(), parser
+
+
+# ______ _______ _______ _______ _____ _______ _____ _______
+# |_____/ |_____| | |______ | | | | | | |
+# | \_ | | | |______ |_____ __|__ | | | __|__ |
+# _ _ _______ __ _ ______ _______ ______
+# |_____| |_____| | \ | | \ | |______ |_____/
+# | | | | | \_| |_____/ |_____ |______ | \_
+#
+def rate_delay(wait_time: int):
+ """Wait for the specified amount of time while informing the user."""
+ for wait in range(wait_time, 0, -1):
+ print(f" Rate limit exceeded, sleeping for {wait} seconds. ", end="\r")
+ sleep(1)
+ print(" " * 80, end="\r")
+
+
+def sdk_operation(operation: Callable, delay_time: int, debugging: bool, **kwargs) -> Result:
+ """Perform an operation against the CrowdStrike API, gracefully handling rate limit errors."""
+ rate_limited = True
+ while rate_limited:
+ try:
+ operation_result: Result = operation(**kwargs)
+ rate_limited = False
+ except APIError as rate_limit_met:
+ if rate_limit_met.code == 429:
+ rate_delay(delay_time)
+ elif debugging:
+ raise rate_limit_met
+ else:
+ failure = FAIL if randrange(1, 3000, 1) % 2 == 0 else FAIL2
+ raise SystemExit(
+ failure.format(rate_limit_met.code, rate_limit_met.message)
+ ) from rate_limit_met
+ return operation_result
+
+
+# _ _ _______ _____ _______ ______ _______
+# |_____| |______ | |_____] |______ |_____/ |______
+# | | |______ |_____ | |______ | \_ ______|
+#
+def reorganize_token_dictionary(tenant: str, record: dict) -> Dict[str, str]:
+ """Reorganize a token record dictionary to include the CID column."""
+ cid_key = {"cid": tenant}
+ cid_list = list(cid_key.items())
+ token_keys = list(record.keys())
+ token_values = list(record.values())
+ token_keys.insert(token_keys.index("id")+1, cid_list[0][0])
+ token_values.insert(token_keys.index("id")+1, cid_list[0][1])
+ return {
+ token_keys[i]: token_values[i]
+ for i in range(0, len(token_keys))
+ }
+
+
+def get_all_tokens(api_sdk: InstallationTokens,
+ cmd_args: Namespace,
+ filt: str,
+ cur_cid: str,
+ returning: List[Dict[str, str]]
+ ) -> List[Dict[str, str]]:
+ """Retrieve all tokens across all tenants."""
+ offset = None
+ running = True
+ while running:
+ token_lookup = sdk_operation(api_sdk.tokens_query,
+ LONG_WAIT,
+ cmd_args.debug,
+ limit=1000,
+ offset=offset,
+ filter=f"label:*'*{filt}*'" if filt else None
+ )
+ if not token_lookup.data:
+ running = False
+ batches = [token_lookup.data[i:i+100] for i in range(0, len(token_lookup.data), 100)]
+ for batch in batches:
+ token_detail = sdk_operation(api_sdk.tokens_read, LONG_WAIT, cmd_args.debug, ids=batch)
+ found = token_detail.data
+ if cmd_args.mssp:
+ found = []
+ for token_det in token_detail.data:
+ new_dict = reorganize_token_dictionary(cur_cid, token_det)
+ found.append(new_dict)
+ returning.extend(found)
+
+ offset = len(returning)
+ if token_lookup.total <= len(returning):
+ running = False
+ return returning
+
+
+def confirm(msg: str):
+ """Request confirmation from the user and return a boolean of the response."""
+ return input(msg) in ["y", "yes", "Y", "YES"]
+
+
+def get_this_cid(auth: InstallationTokens, debug_mode: bool = False):
+ """Retrieve the CID for the current tenant."""
+ my_id = "Not available"
+ running = True
+ while running:
+ try:
+ my_id = sdk_operation(
+ SensorDownload(auth_object=auth).get_sensor_installer_ccid,
+ SHORT_WAIT,
+ debug_mode
+ ).data[0][:-3].lower()
+ running = False
+ except APIError as no_sensor_dl:
+ if no_sensor_dl.code != 429:
+ print("NONFATAL 403 ERROR: This API client is not scoped for Sensor Downloads.")
+ running = False
+ else:
+ rate_delay(SHORT_WAIT)
+ return my_id
+
+
+def check_mssp_scope(auth: InstallationTokens):
+ """Confirm if this API client has access to Flight Control."""
+ valid = False
+ running = True
+ while running:
+ try:
+ valid = bool(FlightControl(auth_object=auth).query_children(limit=1).status_code == 200)
+ running = False
+ except APIError as rate_limit_met:
+ if rate_limit_met.code != 429:
+ running = False
+ raise rate_limit_met
+ rate_delay(SHORT_WAIT)
+ return valid
+
+
+def get_cid_ids(interface: InstallationTokens, arguments: Namespace) -> Tuple[str, List[str]]:
+ """Return all CIDs associated with the API client (if MSSP mode is enabled)."""
+ this_cid = "NonMSSP"
+ cid_list = [this_cid]
+ if arguments.mssp:
+ this_cid = get_this_cid(interface, arguments.debug)
+ cid_list = [this_cid]
+ try:
+ mssp = FlightControl(auth_object=interface)
+ cid_list.extend(mssp.query_children().data)
+ except APIError:
+ pass
+ return this_cid, cid_list
+
+
+def write_output_results(cmdline_args: Namespace, tresults: list):
+ """Write the displayed token results to the requested file."""
+ if cmdline_args.output_file:
+ if cmdline_args.output_format.lower() == "csv":
+ with open(cmdline_args.output_file, "w", newline="", encoding="utf-8") as csv_file:
+ csv_writer = writer(csv_file)
+ if tresults:
+ csv_writer.writerow(tresults[0].keys())
+ for token_row in tresults:
+ csv_writer.writerow(token_row.values())
+ print(f"CSV results output to {cmdline_args.output_file}.")
+ elif cmdline_args.output_format.lower() == "json":
+ with open(cmdline_args.output_file, "w", encoding="utf-8") as json_file:
+ dump(tresults, json_file, indent=4)
+ print(f"JSON results output to {cmdline_args.output_file}.")
+
+
+def display_tokens(token_results: list, cmd_args: Namespace, parent_tokens: list):
+ """Display the retrieved tokens in a tabular format."""
+ new_token_results = token_results
+ if cmd_args.mssp:
+ new_token_results = []
+ for tok in token_results:
+ matched = False
+ for ptok in parent_tokens:
+ if ptok["cid"] == tok["cid"]:
+ if tok["id"] == ptok["id"] and tok["value"] == ptok["value"]:
+ matched = True
+ elif ptok["cid"] != tok["cid"]:
+ matched = True
+ if matched:
+ new_token_results.append(tok)
+
+ token_results = sorted(new_token_results,
+ key=lambda x: x[cmd_args.order_by],
+ reverse=cmd_args.reverse
+ )
+ vers = ""
+ if cmd_args.show_version:
+ vers = f" (FalconPy v{version(agent_string=False)})"
+ if token_results:
+ tabular_display = tabulate(tabular_data=[t.values() for t in token_results],
+ headers=token_results[0].keys(),
+ tablefmt=cmd_args.table_format
+ )
+ print(tabular_display)
+ print(f"{len(token_results)} total tokens found{vers}")
+ write_output_results(cmd_args, token_results)
+ else:
+ print(NOT_FOUND.format(vers.replace("(", "").replace(")", "")))
+
+
+def cross_tenant_action(action: Callable, msg: str, **kwargs):
+ """Check and warn if this action impacts multiple CIDs."""
+ proceed = True
+ if not kwargs.get("cmdline").force:
+ if kwargs.get("cmdline").mssp and check_mssp_scope(kwargs.get("sdk")):
+ parent_to = "the parent and "
+ if kwargs.get("cmdline").skip_parent:
+ parent_to = ""
+ proceed = confirm(WARNING.format(msg, parent_to))
+ if proceed:
+ action(**kwargs)
+ else:
+ print("Operation cancelled.")
+ sys.exit(0)
+
+
+# _______ _____ __ _ _______ _______ _______ __ _ _______ _______
+# | | | | \ | |______ | |_____| | \ | | |______
+# |_____ |_____| | \_| ______| | | | | \_| | ______|
+#
+LONG_WAIT = 10
+SHORT_WAIT = 5
+FAIL = r"""
+ , ,
+ (\____/) FATAL {} {}
+ (_oo_) /
+ (O)
+ __||__ \)
+ []/______\[] /
+ / \______/ \/
+ / /__\
+(\ /____\
+"""
+FAIL2 = r"""
+ _
+ [ ] FATAL {} {}
+ ( ) /
+ |>|
+ __/===\__
+ //| o=o |\\
+<] | o=o | [>
+ \=====/
+ / / | \ \
+ <_________>
+"""
+NOT_FOUND = r"""
+ __ No tokens found!
+ _(\ |@@| /
+(__/\__ \--/ __
+ \___|----| | __
+ \ CS /\ )_ / _\
+ /\__/\ \__O (__
+ (--/\--) \__/
+ _)( )(_
+ `---''---` {}
+"""
+WARNING = r"""
+ __,_,
+ [_|_/ ⚠️ Warning ⚠️
+ // This action will {} multiple tokens
+ _// __ / across {}child tenants.
+(_|) |@@|
+ \ \__ \--/ __
+ \o__|----| | __
+ \ CS /\ )_ / _\
+ /\__/\ \__O (__
+ (--/\--) \__/
+ _)( )(_
+ `---''---`
+
+Are you sure you wish to proceed? (y/n) => """
+# _______ _______ _____ __ _ ______ _____ _ _ _______ _____ __ _ _______
+# | | | |_____| | | \ | |_____/ | | | | | | | \ | |______
+# | | | | | __|__ | \_| | \_ |_____| |_____| | __|__ | \_| |______
+#
+if __name__ == "__main__":
+ begin = datetime.now().timestamp() # Start the timer
+ if sys.version_info <= (3, 7): # Make sure we're running the minimum version of Python
+ raise SystemExit("This application only supports Python 3.7 or greater.")
+ if not version(compare="1.3.4"): # Check for 1.3.4 or greater
+ raise SystemExit("In order to use this sample application, the CrowdStrike FalconPy "
+ "library (version 1.3.4 or greater) must be installed."
+ )
+ parsed, handler = consume_arguments() # Retrieve command line arguments and the parser
+ # There are no credentials in the environment or command line, show help and quit
+ if not parsed.client_id or not parsed.client_secret:
+ handler.print_help()
+ raise SystemExit(
+ "\nYou must provide API credentials via the environment variables\n"
+ "FALCON_CLIENT_ID and FALCON_CLIENT_SECRET or you must provide\n"
+ "these values using the '-k' and '-s' command line arguments."
+ )
+ if parsed.debug: # Enable debug logging to the console if requested
+ basicConfig(level=DEBUG)
+ # Construct an instance of the InstallationTokens Service Class
+ tokens = InstallationTokens(client_id=parsed.client_id,
+ client_secret=parsed.client_secret,
+ debug=parsed.debug,
+ pythonic=True,
+ member_cid=parsed.child
+ )
+ default_action_args = [tokens, parsed] # We display all tokens regardless of command executed
+ tcommand = parsed.subcommand.lower() # Selected token command
+ if tcommand in ["create", "c"]: # Create
+ cross_tenant_action(create_token, "create", sdk=tokens, cmdline=parsed)
+ elif tcommand in ["delete", "d"]: # Delete
+ if parsed.token_label:
+ cross_tenant_action(delete_token, "delete", sdk=tokens, cmdline=parsed)
+ else:
+ delete_token(tokens, parsed)
+ elif tcommand in ["revoke", "x", "restore", "r"]: # Revoke and Restore
+ if parsed.token_label:
+ cross_tenant_action(token_revocation,
+ "restore" if tcommand in ["restore", "r"] else "revoke",
+ sdk=tokens,
+ cmdline=parsed,
+ revoking=tcommand in ["revoke", "x"]
+ )
+ else:
+ token_revocation(tokens, parsed, tcommand in ["revoke", "x"])
+ elif tcommand in ["update", "u"]: # Update
+ if parsed.token_id:
+ update_token_by_id(tokens, parsed)
+ elif parsed.token_label:
+ cross_tenant_action(update_token_by_label, "update", sdk=tokens, cmdline=parsed)
+ if parsed.filter: # List / all commands
+ # Add any provided command line filters to the arguments for the default action
+ default_action_args.append(parsed.filter)
+ show_all_tokens(*default_action_args) # After all processing, display the list of tokens
+
+
+# █ █
+# █ ██
+# ██ _ _ _ _______ _______ _______ _____ _____ ▓█
+# ▒▒███ | | | |______ |______ | | | |_____] ██▓▒▓
+# ▒░▒▓████ |__|__| |______ ______| | |_____| | █████▒▒▒▓
+# █▒▒▓████▒▓███ ▓██▓▓████▒░▒
+# ▒░▒████▒░░▒▒▓▓█▓▓ ████▒▒▒░░▓███▓▒░▒
+# ▓░▒▒███▓░░▒▒▒▓██▓▒█▓█▓▓ ▒▓█▓▓▒███▒▒▒▒░▒████▒░▒
+# ▒░▒▓███▓░░▒▒▒███▒░░░▓███▓█▓ █▓█▓███▒░░░▓██▓▒▒▒░▒████▒░▒
+# ▒░▒▓███▓░░▒▒▒███▒░░░▓███░░▒▓▓█▓ ▓██▒▒░▒███▒░░░▓██▓▒▒▒░▒████▒░▒
+# ▓░▒▒███▓░░▒▒▒▒██▓░░░░░░░░▒▒▒█████▓ ▓█████░▒░░░░░░░░░███▒▒▒▒░▒████▒░▒
+# ▒░▒████▒░░▒▒▒▓███▒░▒▒▒▒░░████▒▒▒▓██ █▓▓▒▒▓███▒░▒▒▒▒░░▓███▒▒▒▒░░▓███▓▒░▒
+# █▒▒▒████▒░▒▒▒▒▒████████████▓▒▒▒▒▒▒███ █▓▒░▒▒▒▒▒████████████▓▒▒▒▒░░▓████▒▒▒
+# ▒░▒▓████▒░░▒▒▒▒▒▓███████▒▒▒▒▒▒░░▓███ ████▒░▒▒▒▒▒▒▓███████▒▒▒▒▒▒░░▓████▒▒░▒
+# ▒░▒▓████▓▒░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒█▓███▓█ █▓████▓▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░░▒█▓███▒▒▒▓
+# ▒░▒▒██████▒░░░▒▒▒▒▒▒▒▒▒░░▒▒█▓███▓▒▒▓█ █▒▒▒██████▒░░▒▒▒▒▒▒▒▒▒▒░░▒▒█▓███▓▒▒▒▒
+# ▓▒▒▒▒███████▒▒▒▒▒▒▒▒▒▒▓███████▒▒▒▒ ▒▒▒▒███████▒▒▒▒▒░▒▒▒▒▓███████▒▒░▒
+# ▒▒▒▒▒▓█████████▓█████████▒▒▒▒▒ ▓▒▒▒▒▓█████████▓█████████▒▒▒░▒
+# ▓▒▒▒▒▒▒████████████▓▒▒▒▒▒▓ █▒▒▒▒▒▒████████████▓▒▒▒▒░▒
+# ▓▒░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ █▓▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒░▓
+# ▓▒▓▓▓▓▒▓█ █▒░▓▓▓▒▓█
+#
+# ______ ______ _______ _______ _______ _ _ _______ _______
+# |_____] |_____/ |______ |_____| | |_____| |______ |______
+# |_____] | \_ |______ | | |_____ | | |______ ______|