From c95c8d3142b097a373bc82621b17d99745a19722 Mon Sep 17 00:00:00 2001 From: Chris Hammond Date: Mon, 4 May 2026 12:40:53 -0400 Subject: [PATCH] Resolves logic issues with online-state and properly configures the shell for this type of filtering --- falcon_toolkit/falcon.py | 1 + falcon_toolkit/shell/cli.py | 94 +++++++++++++++++++++++++++++++------ pyproject.toml | 2 +- 3 files changed, 81 insertions(+), 16 deletions(-) diff --git a/falcon_toolkit/falcon.py b/falcon_toolkit/falcon.py index 3517a29..e1199f6 100755 --- a/falcon_toolkit/falcon.py +++ b/falcon_toolkit/falcon.py @@ -59,6 +59,7 @@ @click.group() +@click.version_option(version=__version__, prog_name="Falcon Toolkit") @click.pass_context @click.option( "-c", diff --git a/falcon_toolkit/shell/cli.py b/falcon_toolkit/shell/cli.py index 94c1466..d3b98a1 100644 --- a/falcon_toolkit/shell/cli.py +++ b/falcon_toolkit/shell/cli.py @@ -94,6 +94,18 @@ required=False, help="Set the timeout for RTR (default: 30s)", ) +@optgroup.option( + "-o", + "--online-state", + "online_state", + type=click.Choice(OnlineState.VALUES), + multiple=False, + required=False, + help=( + "Filter hosts by online state (online, offline, or unknown). " + "If not specified, attempts to connect to all hosts regardless of state" + ), +) def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-positional-arguments ctx: click.Context, device_id_list: str, @@ -102,6 +114,7 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po queueing: bool, startup_script: str, timeout: int, + online_state: str, ): """Implement the falcon shell command. @@ -114,8 +127,11 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po -> Falcon, based on a list of filters provided via one or many -f switches -> Falcon, based on no filters or restrictions at all (i.e., connect to all hosts in a tenant) -> The CLI, based on a comma delimited list of Device IDs passed via a -d switch - -> A file, based on a new-lien delimited list of Device IDs within a file, the name of which - is passed to the CLI via the the -df switch + -> A file, based on a new-line delimited list of Device IDs within a file, the name of which + is passed to the CLI via the -df switch + - Optionally filtering devices by online state (online, offline, unknown) via -o/--online-state. + If not specified, all systems are included regardless of state, as systems in "unknown" state + may still accept RTR connections, and systems can transition states between query and connection - Configuring the output CSV for all commands to be logged to Once we have all the required information together, we configure an RTRPrompt object then @@ -124,9 +140,39 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po instance = get_instance(ctx) client = instance.auth_backend.authenticate(ctx) - # Show online hosts only if queueing is false - online_state = None if queueing else OnlineState.ONLINE - online_string = "" if queueing else "online " + # Determine online state filtering logic: + # Only filter by online_state if explicitly provided by the user via --online-state + # If not provided, we attempt to connect to all systems regardless of state because: + # - Systems can come online between search and shell start + # - Systems with flaky connectivity may show as "unknown" but still accept RTR commands + should_filter_online_state = online_state is not None + online_string = "" if not should_filter_online_state else f"{online_state} " + + # Warn if user is queueing to online systems (unusual usage pattern) + if queueing and online_state == OnlineState.ONLINE.value: + click.echo( + click.style( + "WARNING: Queueing to online systems is unusual. " + "Online systems can receive commands immediately without queueing.", + fg="yellow", + bold=True, + ) + ) + logging.warning("Queueing enabled with online_state=online (unusual configuration)") + + # Block --online-state offline without --queueing: offline hosts require queue_offline=true + # to receive RTR commands; without it the API skips all of them and nothing connects. + if not queueing and online_state == OnlineState.OFFLINE.value: + click.echo( + click.style( + "ERROR: --online-state offline requires --queueing. " + "Offline hosts can only receive RTR commands via queued sessions. " + "Without queueing, no connections will succeed.", + fg="red", + bold=True, + ) + ) + sys.exit(1) if filter_kv_strings: click.echo( @@ -143,7 +189,11 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po click.echo(filters) logging.info(filters) - device_ids = client.hosts.get_device_ids(filters=filters, online_state=online_state) + # Only filter by online_state if explicitly provided + if should_filter_online_state: + device_ids = client.hosts.get_device_ids(filters=filters, online_state=online_state) + else: + device_ids = client.hosts.get_device_ids(filters=filters) elif device_id_list: click.echo( click.style( @@ -160,10 +210,15 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po if device_id: device_ids.add(device_id) - device_ids = client.hosts.filter_device_ids_by_online_state( - list(device_ids), - online_state=online_state, - ) + # Only filter by online_state if explicitly provided + if should_filter_online_state: + device_ids = client.hosts.filter_device_ids_by_online_state( + list(device_ids), + online_state=online_state, + ) + else: + # Use all provided device IDs without filtering by online state + device_ids = list(device_ids) elif device_id_file: click.echo( click.style( @@ -182,10 +237,15 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po line = line.strip() if line: device_ids.add(line) - device_ids = client.hosts.filter_device_ids_by_online_state( - list(device_ids), - online_state=online_state, - ) + # Only filter by online_state if explicitly provided + if should_filter_online_state: + device_ids = client.hosts.filter_device_ids_by_online_state( + list(device_ids), + online_state=online_state, + ) + else: + # Use all device IDs from file without filtering by online state + device_ids = list(device_ids) else: click.echo( click.style( @@ -194,7 +254,11 @@ def cli_shell( # pylint: disable=too-many-arguments,too-many-locals,too-many-po ) ) logging.info("Connecting to all %shosts in the Falcon instance", online_string) - device_ids = client.hosts.get_device_ids(online_state=online_state) + # Only filter by online_state if explicitly provided + if should_filter_online_state: + device_ids = client.hosts.get_device_ids(online_state=online_state) + else: + device_ids = client.hosts.get_device_ids() if not device_ids: click.echo( diff --git a/pyproject.toml b/pyproject.toml index f7e8de1..cbe51f8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "falcon-toolkit" -version = "3.6.1" +version = "3.6.3" description = "Toolkit to interface with CrowdStrike Falcon via the API" license = "MIT" authors = [