diff --git a/README.md b/README.md
index 062858e..21bde88 100644
--- a/README.md
+++ b/README.md
@@ -183,18 +183,19 @@ Two types of configuration backends are provided out of the box: the default, wh
Your API keys should have the following scopes enabled in the Falcon dashboard:
-| ↓ API Scopes // Commands → | `host_search` | `shell` | `policies`
(Prevention) | `policies`
(Response) | `containment`
Host Containment |
-|--------------------------------------|:-------------:|:-------:|:--------------------------:|:-------------------------:|:---------------------------------:|
-| **Falcon Flight Control: Read** | X
*When using parent
CID API Keys* | X
*When using parent
CID API Keys* | X
*When using parent
CID API Keys* | X
*When using parent
CID API Keys* |
-| **Hosts: Read** | X | X | | | X |
-| **Hosts: Write** | | | | | X |
-| **Prevention Policies: Read** | | | X
`describe` / `export` sub-commands | | |
-| **Prevention Policies: Write** | | | X
`import` sub-command | | |
-| **Real Time Response: Read** | | X | | | |
-| **Real Time Response: Write** | | X | | | |
-| **Real Time Response: Admin** | | X
*for admin commands* | | | |
-| **Response Policies: Read** | | | | X
`describe` / `export` sub-commands | |
-| **Response Policies: Write** | | | | X
`import` sub-command | |
+| ↓ API Scopes // Commands → | `host_search` | `shell` | `policies`
(Prevention) | `policies`
(Response) | `containment`
Host Containment | `maintenance_token`
Maintenance Tokens |
+|--------------------------------------|:-------------:|:-------:|:--------------------------:|:-------------------------:|:---------------------------------:|:-----------------------------------------:|
+| **Falcon Flight Control: Read** | X
*When using parent
CID API Keys* | X
*When using parent
CID API Keys* | X
*When using parent
CID API Keys* | X
*When using parent
CID API Keys* | | |
+| **Hosts: Read** | X | X | | | X | X |
+| **Hosts: Write** | | | | | X | |
+| **Prevention Policies: Read** | | | X
`describe` / `export` sub-commands | | | |
+| **Prevention Policies: Write** | | | X
`import` sub-command | | | |
+| **Real Time Response: Read** | | X | | | | |
+| **Real Time Response: Write** | | X | | | | |
+| **Real Time Response: Admin** | | X
*for admin commands* | | | | |
+| **Response Policies: Read** | | | | X
`describe` / `export` sub-commands | | |
+| **Response Policies: Write** | | | | X
`import` sub-command | | |
+| **Sensor Update Policies: Write** | | | | | | X |
### Showing Your Profiles
@@ -357,6 +358,33 @@ Some example usages of this functionality are as follows:
+## Maintenance Tokens
+
+You can fetch maintenance tokens for systems within your Falcon tenant, or retrieve the bulk maintenance token.
+
+### Examples
+
+Show the bulk maintenance token:
+
+```shell
+$ falcon -p MyCompany maintenance_token -b
+Getting the bulk maintenance token
+Bulk maintenance token: redactedexample12345
+WARNING: this token must be kept safe, as it can uninstall all Falcon sensors!
+```
+
+Show a token for a specific system located by hostname:
+
+```shell
+$ falcon -p MyCompany maintenance_token -f Hostname=MY-TEST-BOX-1
+```
+
+Show maintenance tokens for a list of device IDs provided on the command line:
+
+```shell
+$ falcon -p MyCompany maintenance_token -d aid1,aid2,...
+```
+
## Network Containment
You can `contain` and `uncontain` systems through Falcon Toolkit. Network containment restricts the network connectivity of matching systems to just the Falcon Platform.
diff --git a/falcon_toolkit/containment/cli.py b/falcon_toolkit/containment/cli.py
index 614fef5..08e6a2e 100644
--- a/falcon_toolkit/containment/cli.py
+++ b/falcon_toolkit/containment/cli.py
@@ -131,6 +131,7 @@ def cli_containment(
sys.exit(1)
ctx.obj['device_ids'] = device_ids
+ logging.debug(device_ids)
def check_empty_device_ids(client) -> List[str]:
diff --git a/falcon_toolkit/falcon.py b/falcon_toolkit/falcon.py
index 23cd7d4..ce666bc 100755
--- a/falcon_toolkit/falcon.py
+++ b/falcon_toolkit/falcon.py
@@ -51,6 +51,7 @@
from falcon_toolkit.common.utils import configure_data_dir
from falcon_toolkit.containment.cli import cli_containment
from falcon_toolkit.hosts.cli import cli_host_search
+from falcon_toolkit.maintenance_token.cli import cli_maintenance_token
from falcon_toolkit.policies.cli import cli_policies
from falcon_toolkit.shell.cli import cli_shell
@@ -294,5 +295,6 @@ def cli_list_filters():
cli.add_command(cli_containment)
cli.add_command(cli_host_search)
cli.add_command(cli_list_filters)
+cli.add_command(cli_maintenance_token)
cli.add_command(cli_policies)
cli.add_command(cli_shell)
diff --git a/falcon_toolkit/maintenance_token/__init__.py b/falcon_toolkit/maintenance_token/__init__.py
new file mode 100644
index 0000000..9897a9d
--- /dev/null
+++ b/falcon_toolkit/maintenance_token/__init__.py
@@ -0,0 +1,4 @@
+"""Falcon Toolkit: Maintenance Token Retrieval.
+
+This sub-module contains the logic required to fetch device maintenance tokens.
+"""
diff --git a/falcon_toolkit/maintenance_token/cli.py b/falcon_toolkit/maintenance_token/cli.py
new file mode 100644
index 0000000..7726808
--- /dev/null
+++ b/falcon_toolkit/maintenance_token/cli.py
@@ -0,0 +1,179 @@
+"""Falcon Toolkit: Maintenance Token Retrieval.
+
+This file contains the CLI options for the falcon maintenance_token command.
+"""
+import logging
+
+from typing import List
+
+import click
+
+from caracara import Client
+from click_option_group import (
+ optgroup,
+ MutuallyExclusiveOptionGroup,
+)
+
+from falcon_toolkit.common.cli import (
+ get_instance,
+ parse_cli_filters,
+)
+from falcon_toolkit.maintenance_token.device_tokens import show_device_maintenance_tokens
+
+
+@click.command(
+ name="maintenance_token",
+ help="Get the maintenance token for a device, or get the bulk maintenance token"
+)
+@click.pass_context
+@optgroup.group(
+ "Specify devices to get the token for",
+ cls=MutuallyExclusiveOptionGroup,
+ help="Choose no more than one method to choose systems to fetch the maintenance tokens for",
+)
+@optgroup.option(
+ '-b',
+ '--bulk',
+ 'bulk_token',
+ type=click.BOOL,
+ is_flag=True,
+ default=False,
+ help="Get the CID-wide bulk maintenance token",
+)
+@optgroup.option(
+ '-d',
+ '--device-id-list',
+ 'device_id_list',
+ type=click.STRING,
+ help="Specify a list of Device IDs (AIDs), comma delimited"
+)
+@optgroup.option(
+ '-df',
+ '--device-id-file',
+ 'device_id_file',
+ type=click.STRING,
+ help=(
+ "Specify a list of Device IDs (AIDs) in an external file, one per line; "
+ "this can help you get round command line length limits in your workstation's shell,"
+ ),
+)
+@optgroup.option(
+ '-f',
+ '--filter',
+ 'filter_kv_string',
+ type=click.STRING,
+ multiple=True,
+ help="Filter hosts based on standard Falcon filters",
+)
+def cli_maintenance_token(
+ ctx: click.Context,
+ bulk_token: bool,
+ device_id_list: str,
+ device_id_file: str,
+ filter_kv_string: List[str],
+):
+ """Get system maintenance tokens from Falcon."""
+ instance = get_instance(ctx)
+ client: Client = instance.auth_backend.authenticate()
+ ctx.obj['client'] = client
+
+ # Bulk token is a special case we can handle here.
+ # Device tokens need to be handled elsewhere.
+ if bulk_token:
+ click.echo(click.style(
+ "Getting the bulk maintenance token",
+ fg='magenta',
+ bold=True,
+ ))
+ token = client.sensor_update_policies.get_bulk_maintenance_token(
+ audit_message="Fetched via Falcon Toolkit",
+ )
+ click.echo("Bulk maintenance token: ", nl=False)
+ click.echo(click.style(token, bold=True, fg='blue'))
+ click.echo(click.style(
+ "WARNING: this token must be kept safe, as it can uninstall all Falcon sensors!",
+ bold=True,
+ fg='red',
+ ))
+
+ return
+
+ if filter_kv_string:
+ click.echo(click.style(
+ "Getting the maintenance tokens for all hosts that match the provided Falcon filters",
+ fg='magenta',
+ bold=True,
+ ))
+ logging.info("Getting maintenance tokens for all devices that match the provided filters")
+
+ filters = parse_cli_filters(filter_kv_string, client).get_fql()
+ click.echo(click.style("FQL filter string: ", bold=True), nl=False)
+ click.echo(filters)
+ logging.info(filters)
+
+ device_ids = client.hosts.get_device_ids(filters=filters)
+
+ elif device_id_list:
+ click.echo(click.style(
+ "Getting the maintenance tokens for the devices identified by the IDs provided on "
+ "the command line",
+ fg='magenta',
+ bold=True,
+ ))
+ logging.info(
+ "Getting the maintenance tokens for the devices identified by the IDs "
+ "provided on the command line"
+ )
+
+ device_ids = set()
+ for device_id in device_id_list.split(","):
+ device_id = device_id.strip()
+ if device_id:
+ device_ids.add(device_id)
+
+ elif device_id_file:
+ click.echo(click.style(
+ "Getting the maintenance tokens for the devices identified by the IDs listed in a file",
+ fg='magenta',
+ bold=True,
+ ))
+ click.echo(click.style("File path: ", bold=True), nl=False)
+ click.echo(device_id_file)
+ logging.info(
+ "Getting the maintenance tokens for the devices identified by the IDs listed in %s",
+ device_id_file
+ )
+
+ with open(device_id_file, 'rt', encoding='ascii') as device_id_file_handle:
+ device_ids = set()
+ for line in device_id_file_handle:
+ line = line.strip()
+ if line:
+ device_ids.add(line)
+
+ else:
+ click.echo(click.style(
+ "Getting the maintenance token for all systems in the tenant!",
+ bold=True,
+ fg='yellow',
+ ))
+ click.echo("You must enter the string \"I AM SURE!\" to proceed.")
+ confirmation = input("Are you sure? ")
+ if confirmation != "I AM SURE!":
+ print("You did not confirm you were sure. Aborting!")
+ return
+
+ device_ids = client.hosts.get_device_ids()
+
+ logging.debug(device_ids)
+ if device_ids:
+ show_device_maintenance_tokens(
+ device_ids=device_ids,
+ client=client,
+ )
+ else:
+ click.echo(click.style(
+ "No devices matched the provided filters",
+ fg='red',
+ bold=True,
+ ))
diff --git a/falcon_toolkit/maintenance_token/device_tokens.py b/falcon_toolkit/maintenance_token/device_tokens.py
new file mode 100644
index 0000000..f0844da
--- /dev/null
+++ b/falcon_toolkit/maintenance_token/device_tokens.py
@@ -0,0 +1,49 @@
+"""Falcon Toolkit: Maintenance Token Retrieval.
+
+This file contains the logic required to fetch tokens for many devices and write them to screen.
+"""
+import logging
+
+from operator import itemgetter
+from typing import List
+
+import click
+import click_spinner
+import tabulate
+
+from caracara import Client
+
+
+def show_device_maintenance_tokens(
+ device_ids: List[str],
+ client: Client,
+):
+ """Get maintenance tokens for many devices and print them to screen."""
+ click.echo("Fetching requested maintenance tokens. This may take a while.")
+
+ tokens = {}
+ header_row = [
+ click.style("Device ID", bold=True, fg='blue'),
+ click.style("Maintenance Token", bold=True, fg='blue'),
+ ]
+ tokens_table = []
+
+ with click_spinner.spinner():
+ for device_id in device_ids:
+ token = client.sensor_update_policies.get_maintenance_token(
+ device_id=device_id,
+ audit_message="Fetched via Falcon Toolkit",
+ )
+ logging.debug("%s -> %s", device_id, token)
+ tokens[device_id] = token
+ tokens_table.append([
+ device_id,
+ token,
+ ])
+
+ tokens_table = sorted(tokens_table, key=itemgetter(1, 0))
+ tokens_table.insert(0, header_row)
+ click.echo(tabulate.tabulate(
+ tokens_table,
+ tablefmt='fancy_grid',
+ ))
diff --git a/poetry.lock b/poetry.lock
index cd2e51c..f2b4eac 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand.
[[package]]
name = "astroid"
@@ -181,19 +181,19 @@ cffi = ">=1.0.0"
[[package]]
name = "caracara"
-version = "0.6.1"
+version = "0.7.0"
description = "The CrowdStrike Falcon Developer Toolkit"
optional = false
-python-versions = ">=3.8.2,<4.0.0"
+python-versions = "<4.0.0,>=3.8.2"
files = [
- {file = "caracara-0.6.1-py3-none-any.whl", hash = "sha256:41a3cc2f68b46d2534d925ac1222a8282b58b0e07d1b51dacd6db1259f0a7dc6"},
- {file = "caracara-0.6.1.tar.gz", hash = "sha256:3e5047350ffe5c9e329dab5a1396d5e641766611550af96156660f3b5cb13b53"},
+ {file = "caracara-0.7.0-py3-none-any.whl", hash = "sha256:9a947b1f7272363baad46d3e995def0143f7748a143f2efed76e014556583569"},
+ {file = "caracara-0.7.0.tar.gz", hash = "sha256:22fd8ef2532dabb73ee76e67a237ca5626700def9133c8b0d577c780d13e0b25"},
]
[package.dependencies]
caracara-filters = ">=0.2,<0.3"
crowdstrike-falconpy = ">=1.4.0,<2.0.0"
-py7zr = ">=0.20,<0.21"
+py7zr = ">=0.20,<0.22"
setuptools = ">=69.0,<70.0"
[[package]]
@@ -917,13 +917,13 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"]
[[package]]
name = "py7zr"
-version = "0.20.8"
+version = "0.21.0"
description = "Pure python 7-zip library"
optional = false
python-versions = ">=3.7"
files = [
- {file = "py7zr-0.20.8-py3-none-any.whl", hash = "sha256:c74d957a0d32a2368854d1721b4ca20e614ea116d733352a115ca1c789b2c42e"},
- {file = "py7zr-0.20.8.tar.gz", hash = "sha256:2a6b0db0441e63a2dd74cbd18f5d9ae7e08dc0e54685aa486361d0db6a0b4f78"},
+ {file = "py7zr-0.21.0-py3-none-any.whl", hash = "sha256:ea6ded2e5c6d8539e3406cb3b0317192b32af59cff13eaf87702acc36a274da6"},
+ {file = "py7zr-0.21.0.tar.gz", hash = "sha256:213a9cc46940fb8f63b4163643a8f5b36bbc798134746c3992d3bc6b14edab87"},
]
[package.dependencies]
@@ -939,7 +939,7 @@ pyzstd = ">=0.15.9"
texttable = "*"
[package.extras]
-check = ["black (>=23.1.0)", "check-manifest", "flake8 (<7)", "flake8-black (>=0.3.6)", "flake8-deprecated", "flake8-isort", "isort (>=5.0.3)", "lxml", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine", "types-psutil"]
+check = ["black (>=23.1.0)", "check-manifest", "flake8 (<8)", "flake8-black (>=0.3.6)", "flake8-deprecated", "flake8-isort", "isort (>=5.0.3)", "lxml", "mypy (>=0.940)", "mypy-extensions (>=0.4.1)", "pygments", "readme-renderer", "twine", "types-psutil"]
debug = ["pytest", "pytest-leaks", "pytest-profiling"]
docs = ["docutils", "sphinx (>=5.0)", "sphinx-a4doc", "sphinx-py3doc-enhanced-theme"]
test = ["coverage[toml] (>=5.2)", "coveralls (>=2.1.1)", "py-cpuinfo", "pyannotate", "pytest", "pytest-benchmark", "pytest-cov", "pytest-remotedata", "pytest-timeout"]
@@ -1571,4 +1571,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
-content-hash = "1997fe828240d399611c33ee33202de417546cde48c358f4d0e9759bd3813108"
+content-hash = "d17a3896a5ac62d923eab4e6a83406e1402032108ab5d937e98a50facd46bf8a"
diff --git a/pyproject.toml b/pyproject.toml
index 604b29d..c3faabb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,7 +30,7 @@ classifiers = [
[tool.poetry.dependencies]
python = "^3.9"
-caracara = "^0.6.1"
+caracara = "^0.7.0"
click = "^8.1.3"
click-option-group = "^0.5.6"
click-spinner = "^0.1.10"