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"