diff --git a/.github/wordlist.txt b/.github/wordlist.txt
index 11bbdba0..1fb1474e 100644
--- a/.github/wordlist.txt
+++ b/.github/wordlist.txt
@@ -1588,4 +1588,6 @@ RunIntegrationTaskAdmin
RunIntegrationTaskV
queryPinnableContentVersions
ContentUpdatePolicies
-jimmyruann
\ No newline at end of file
+jimmyruann
+executeactiverespondercommand
+combineddevicesbyfilter
\ No newline at end of file
diff --git a/samples/README.md b/samples/README.md
index 5077bac4..9b451b86 100644
--- a/samples/README.md
+++ b/samples/README.md
@@ -69,7 +69,7 @@ The following samples are categorized by CrowdStrike product, and further catego
| [ML Exclusions](#ml-exclusions-samples) | ML Exclusion Audit |
| [Prevention Policies](#prevention-policies-samples) | Clone Prevention Policy
Create Host Group and attach Prevention Policies
Prevention Policy Hawk |
| [Incidents](#incidents-samples) | CrowdScore QuickChart
Incident Triage |
-| [Real Time Response](#real-time-response-samples) | Bulk execute a command
Bulk execute a command (queued)
Get file from multiple hosts
Get host uptime
Get RTR result
Dump memory for a running process
My Little RTR
Remotely restart a sensor while taking a capture
RTR Script Manager |
+| [Real Time Response](#real-time-response-samples) | Bulk execute a command
Bulk execute a command (queued)
Get file from multiple hosts
Get host uptime
Get RTR result
Dump memory for a running process
My Little RTR
Remotely restart a sensor while taking a capture
RTR Script Manager
Stream file download |
| [Sensor Visibility Exclusions](#sensor-visibility-exclusions-samples) | Sensor Visibility Exclusion Audit |
| [Firewall Management](#firewall-management-samples) | Export Firewall events to a file |
@@ -1277,6 +1277,7 @@ These samples focus on CrowdStrike's Real Time Response and Real Time Response A
- [My Little RTR](#my-little-rtr)
- [Remotely restart a sensor while taking a capture](#remotely-restart-a-sensor-while-taking-a-capture)
- [Script Manager](#script-manager)
+- [Streaming file download](#streaming-file-download)
#### Bulk execute a command
Using this [demonstration](rtr#bulk-execute-a-command-on-matched-hosts), you can execute a command on multiple hosts that have a hostname matching a search string you provide.
@@ -1480,6 +1481,37 @@ This sample demonstrates the following CrowdStrike Flight Control API operations
| [getChildren](https://www.falconpy.io/Service-Collections/MSSP.html#getchildren) | Get child customer detail by child CID(s). |
| [queryChildren](https://www.falconpy.io/Service-Collections/MSSP.html#querychildren) | Query for customers linked as children. |
+---
+
+#### Streaming file download
+This [example](rtr#streaming-file-download) demonstrates stream downloading a target binary file from a host.
+
+[](rtr#streaming-file-download)
+[](rtr#streaming-file-download)
+
+
+##### Hosts API operations discussed
+This sample demonstrates the following CrowdStrike Hosts API operation:
+
+| Operation | Description |
+| :--- | :--- |
+| [CombinedDevicesByFilter](https://falconpy.io/Service-Collections/Hosts.html#combineddevicesbyfilter) | Search for hosts in your environment by platform, hostname, IP, and other criteria. Returns full device records. |
+
+##### Real Time Response API operations discussed
+This sample demonstrates the following CrowdStrike Real Time Response API operations:
+
+| Operation | Description |
+| :--- | :--- |
+| [RTR_InitSession](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_initsession) | Initialize a new session with the RTR cloud. |
+| [RTR_DeleteSession](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_deletesession) | Delete a session. |
+| [RTR_ExecuteActiveResponderCommand](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_executeactiverespondercommand) | Execute an active responder command on a single host. |
+| [RTR_CheckActiveResponderCommandStatus](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_executeactiverespondercommand) | Get status of an executed active-responder command on a single host. |
+| [RTR_ListFilesV2](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_listfilesv2) | Get a list of files for the specified RTR session. |
+| [RTR_GetExtractedFileContents](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_getextractedfilecontents) | Get RTR extracted file contents for specified session and sha256. |
+| [RTR_DeleteFileV2](https://falconpy.io/Service-Collections/Real-Time-Response.html#rtr_deletefilev2) | Delete a RTR session file. |
+
+
+---
diff --git a/samples/rtr/README.md b/samples/rtr/README.md
index 88c19ba8..34058301 100644
--- a/samples/rtr/README.md
+++ b/samples/rtr/README.md
@@ -14,6 +14,7 @@ The examples within this folder focus on leveraging CrowdStrike's Real Time Resp
- [Script Manager](#script-manager) - Upload and delete RTR scripts for use on endpoints.
- [Dump Process Memory](pid-dump) - Dumps the memory for a running process on a target system.
- [My Little RTR](pony) - Retrieve System Information and draws ASCII art.
+- [Streaming File Download](#streaming-file-download) - Stream download a file from a target host.
## Bulk execute a command on matched hosts
@@ -761,4 +762,148 @@ Required arguments:
### Example source code
The source code for this example can be found [here](script_manager.py).
+---
+
+## Streaming File Download
+This sample creates an RTR session with a target host, and stream downloads the specified file.
+
+### Running the program
+In order to run this demonstration, you you will need access to CrowdStrike API keys with the following scopes:
+
+| Service Collection | Scope |
+| :---- | :---- |
+| Hosts | __READ__ |
+| Real Time Response | __READ__, __WRITE__ |
+
+> [!NOTE]
+> This program can be executed using an API key that is not scoped for the Hosts service collection. Users will need to provide an AID value for the target host instead of a hostname.
+
+### Execution syntax
+This sample leverages simple command-line arguments to implement functionality.
+
+#### Basic usage
+Streaming download a specified file from a host by hostname.
+
+```shell
+python3 streaming_download_service.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -n TARGET_HOSTNAME -f TARGET_FILENAME
+```
+
+Streaming download a specified file from a host by host AID.
+
+```shell
+python3 streaming_download_service.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -a TARGET_HOST_AID -f TARGET_FILENAME
+```
+
+> [!TIP]
+> This sample supports [Environment Authentication](https://falconpy.io/Usage/Authenticating-to-the-API.html#environment-authentication), meaning you can execute any of the command lines shown without providing credentials if you have the values `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` defined in your environment.
+
+```shell
+python3 streaming_download_service.py -n TARGET_HOSTNAME -f TARGET_FILENAME
+```
+
+Specify the name of the save file used to store the resulting download.
+
+```shell
+python3 streaming_download_service.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -n TARGET_HOSTNAME -f TARGET_FILENAME -sf SAVE_FILENAME
+```
+
+Disable the pre-existence check for the save file.
+
+> [!NOTE]
+> This will overwrite the existing save file with the newly downloaded file.
+
+```shell
+python3 streaming_download_service.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -n TARGET_HOSTNAME -f TARGET_FILENAME -o
+```
+
+Adjust the chunk size used for streaming the download.
+
+```shell
+python3 streaming_download_service.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -n TARGET_HOSTNAME -f TARGET_FILENAME -c CHUNK_SIZE
+```
+
+> Activate debugging with the `-d` argument.
+
+```shell
+python3 streaming_download_service.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -n TARGET_HOSTNAME -f TARGET_FILENAME -d
+```
+
+#### Command-line help
+Command-line help is available via the `-h` argument.
+
+```shell
+usage: streaming_download_service.py [-h] [-c CHUNK_SIZE] [-o] [-d] -f FILENAME [-sf SAVE_FILE]
+ (-n HOSTNAME | -a AID) [-k FALCON_CLIENT_ID]
+ [-s FALCON_CLIENT_SECRET]
+
+Real Time Response API streaming download sample.
+
+._____________._.______ ._______.______ ._____.___ .___ .______ ._____
+| ___/\__ _:|: __ \ : .____/: \ : |: __|: \ :_ ___\
+|___ \ | :|| \____|| : _/\ | . || \ / || : || || |___
+| / | || : \ | / \| : || |\/ || || | || / |
+|__:___/ | || |___\|_.: __/|___| ||___| | || ||___| ||. __ |
+ : |___||___| :/ |___| |___||___| |___| :/ |. |
+ : :/
+ :
+.______ ._______ ___ .______ .___ ._______ .______ .______ .________
+:_ _ \ : .___ \ .___ | |: \ | | : .___ \ : \ :_ _ \ | ___/
+| | || : | |: | /\| || || | | : | || . || | ||___ \
+| . | || : || |/ : || | || |/\ | : || : || . | || /
+|. ____/ \_. ___/ | / ||___| || / \ \_. ___/ |___| ||. ____/ |__:___/
+ :/ :/ |______/|___| |___||______/ :/ |___| :/ :
+ : : : : :
+ :
+ FalconPy v1.5.0
+
+This sample demonstrates how to perform a streaming download from the
+CrowdStrike Real Time Response API. Files are saved as 7-zip archives.
+
+Requirements:
+ crowdstrike-falconpy v1.5.0+
+
+Creation: 04.23.2025 - jshcodes@CrowdStrike
+
+options:
+ -h, --help show this help message and exit
+
+behavior:
+ Download and API behavior arguments.
+
+ -c, --chunk_size CHUNK_SIZE
+ Streaming download chunk size
+ -o, --overwrite Force overwritting of a pre-existing save file
+ -d, --debug Enable API debugging
+
+filename:
+ You must specify a filename to download.
+ If you do not specify a save filename, it will be saved as "result.7z".
+
+ -f, --filename FILENAME
+ Target filename
+ -sf, --save_file SAVE_FILE
+ Name of the saved file
+
+host:
+ One of the two following arguments must be specified.
+
+ -n, --hostname HOSTNAME
+ Target hostname (use instead of AID)
+ -a, --aid AID Target host AID (use instead of hostname)
+
+authentication:
+ If these arguments are not specified, Environment Authentication will be attempted.
+ Environment Authentication: https://falconpy.io/Usage/Authenticating-to-the-API.html#environment-authentication
+
+ -k, --falcon_client_id FALCON_CLIENT_ID
+ CrowdStrike Falcon API Client ID
+ -s, --falcon_client_secret FALCON_CLIENT_SECRET
+ CrowdStrike Falcon API Client Secret
+```
+
+### Example source code
+The source code for this example can be found [here](streaming_download_service.py).
+
+The source code for the Uber Class version of this example can be found [here](streaming_download_uber.py).
+
---
\ No newline at end of file
diff --git a/samples/rtr/streaming_download_service.py b/samples/rtr/streaming_download_service.py
new file mode 100644
index 00000000..1d8feef3
--- /dev/null
+++ b/samples/rtr/streaming_download_service.py
@@ -0,0 +1,350 @@
+r"""Real Time Response API streaming download sample.
+
+._____________._.______ ._______.______ ._____.___ .___ .______ ._____
+| ___/\__ _:|: __ \ : .____/: \ : |: __|: \ :_ ___\
+|___ \ | :|| \____|| : _/\ | . || \ / || : || || |___
+| / | || : \ | / \| : || |\/ || || | || / |
+|__:___/ | || |___\|_.: __/|___| ||___| | || ||___| ||. __ |
+ : |___||___| :/ |___| |___||___| |___| :/ |. |
+ : :/
+ :
+.______ ._______ ___ .______ .___ ._______ .______ .______ .________
+:_ _ \ : .___ \ .___ | |: \ | | : .___ \ : \ :_ _ \ | ___/
+| | || : | |: | /\| || || | | : | || . || | ||___ \
+| . | || : || |/ : || | || |/\ | : || : || . | || /
+|. ____/ \_. ___/ | / ||___| || / \ \_. ___/ |___| ||. ____/ |__:___/
+ :/ :/ |______/|___| |___||______/ :/ |___| :/ :
+ : : : : :
+ :
+ FalconPy v1.5.0
+
+This sample demonstrates how to perform a streaming download from the
+CrowdStrike Real Time Response API. Files are saved as 7-zip archives.
+
+Requirements:
+ crowdstrike-falconpy v1.5.0+
+
+Creation: 04.23.2025 - jshcodes@CrowdStrike
+"""
+import logging
+import os
+from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
+from typing import Tuple, List
+from requests.exceptions import HTTPError
+from falconpy import APIError, BaseURL, Hosts, RealTimeResponse
+
+
+class Indicator:
+ """Over-architected progress indicator ðĪŠ."""
+
+ _indicator = ["ð", "ð", "ð", "ð", "ð", "ð", "ð§", "ð", "ð", "ð", "ð", "ð"]
+
+ def __init__(self):
+ """Initialize the class and set the starting position."""
+ self._position = -1
+
+ def __repr__(self) -> str:
+ """Increment the position and display the current progress indicator value."""
+ self.position += 1
+ if self.position > len(self.indicator) - 1:
+ self.position = 0
+ return self.indicator[self.position]
+
+ @property
+ def indicator(self) -> List[str]:
+ """Progress indicator graphical elements."""
+ return self._indicator
+
+ @property
+ def position(self) -> int:
+ """Progress indicator position."""
+ return self._position
+
+ @position.setter
+ def position(self, value: int):
+ """Set the indicator position."""
+ self._position = value
+
+
+class Arrow(Indicator):
+ """Animated download emoji, I might have played with this sample for too long."""
+
+ _indicator = []
+ for index, element in enumerate(["ðū âŽ
", "ðū âŽ
", "ðūâŽ
", "â
"]):
+ count = 500
+ if index == 3:
+ count = count * 3
+ for i in range(count):
+ _indicator.append(element)
+
+
+def parse_command_line() -> Namespace:
+ """Ingest the provided command line parameters and handle any input errors."""
+ parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
+ behave = parser.add_argument_group("behavior", "Download and API behavior arguments.")
+ behave.add_argument("-c", "--chunk_size",
+ help="Streaming download chunk size",
+ default=8192
+ )
+ behave.add_argument("-o", "--overwrite",
+ help="Force overwritting of a pre-existing save file",
+ action="store_true",
+ default=False
+ )
+ behave.add_argument("-d", "--debug",
+ help="Enable API debugging",
+ action="store_true",
+ default=False
+ )
+ file_group = parser.add_argument_group("filename",
+ "You must specify a filename to download.\nIf you do "
+ "not specify a save filename, it will be saved as "
+ "\"result.7z\"."
+ )
+ file_group.add_argument("-f", "--filename",
+ help="Target filename",
+ required=True
+ )
+ file_group.add_argument("-sf", "--save_file",
+ help="Name of the saved file",
+ default="result.7z"
+ )
+ mut_group = parser.add_argument_group("host",
+ "One of the two following arguments must be specified."
+ )
+ mutual = mut_group.add_mutually_exclusive_group(required=True)
+ mutual.add_argument("-n", "--hostname",
+ help="Target hostname (use instead of AID)"
+ )
+ mutual.add_argument("-a", "--aid",
+ help="Target host AID (use instead of hostname)"
+ )
+ auth = parser.add_argument_group("authentication",
+ "If these arguments are not specified, "
+ "Environment Authentication will be attempted.\n"
+ "Environment Authentication: https://falconpy.io/Usage/"
+ "Authenticating-to-the-API.html#environment-authentication"
+ )
+ auth.add_argument("-k", "--falcon_client_id",
+ help="CrowdStrike Falcon API Client ID",
+ default=None
+ )
+ auth.add_argument("-s", "--falcon_client_secret",
+ help="CrowdStrike Falcon API Client Secret",
+ default=None
+ )
+
+ parsed = parser.parse_args()
+ if not isinstance(parsed.chunk_size, int):
+ parsed.chunk_size = 8192
+
+ return parsed
+
+
+def open_sdk(debug: bool,
+ client_id: str,
+ client_secret: str
+ ) -> Tuple[RealTimeResponse, Hosts]:
+ """Create instances of the necessary Service Classes from the FalconPy SDK."""
+ real_time_response_api = RealTimeResponse(debug=debug,
+ client_id=client_id,
+ client_secret=client_secret,
+ pythonic=True
+ )
+ hosts_api = Hosts(auth_object=real_time_response_api)
+ region_name = ""
+ for region in BaseURL:
+ if region.value == hosts_api.auth_object.base_url.replace("https://", ""):
+ region_name = f" {region.name}"
+
+ print(f" ð Connection to CrowdStrike API{region_name} established", end="\r")
+
+ return real_time_response_api, hosts_api
+
+
+def get_host_aid(hostname: str, hosts_sdk: Hosts) -> str:
+ """Retrieve the host AID."""
+ try:
+ returned = hosts_sdk.query_devices_by_filter_combined(filter=f"hostname:'{hostname}'"
+ ).data
+ if not returned:
+ raise SystemExit("No hosts using this hostname were identified.")
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ð Host AID identified", end=f"{' '*30}\r")
+
+ return returned[0]["device_id"]
+
+
+def create_rtr_session(host_aid: str, rtr_api: RealTimeResponse) -> str:
+ """Initialize an RTR session with the target host."""
+ try:
+ session = rtr_api.init_session(device_id=host_aid)
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ð Real Time Response connection established", end=f"{' '*20}\r")
+
+ return session.data[0]["session_id"]
+
+
+def close_rtr_session(session: str, rtr_api: RealTimeResponse) -> None:
+ """Close the RTR session with the target host."""
+ try:
+ rtr_api.delete_session(session_id=session)
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" âïļâðĨ Real Time Response connection disconnected", end=f"{' '*20}\r")
+
+
+def get_target_file(filename: str, session: str, rtr_api: RealTimeResponse) -> str:
+ """Execute a get command for the target file and upload it to the CrowdStrike cloud."""
+ try:
+ get_request = rtr_api.execute_active_responder_command(base_command="get",
+ session_id=session,
+ command_string=f"get {filename}"
+ ).data
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ðĪ Get file command sent", end=f"{' '*30}\r")
+
+ return get_request[0]["cloud_request_id"]
+
+
+def wait_for_upload(cloud_request_id: str, rtr_api: RealTimeResponse) -> None:
+ """Wait for the upload to complete and return the file SHA256 identifier."""
+ status = False
+ while not status:
+ try:
+ result = rtr_api.check_active_responder_command_status(
+ cloud_request_id=cloud_request_id
+ )
+ status = result.data[0]["complete"]
+ print(" âģ Waiting for get command to process", end=f"{' '*30}\r")
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+ if result.data[0]["stderr"]:
+ print(f"{' '*80}")
+ raise SystemExit(f"ERROR: {result.data[0]['stderr']}")
+
+
+def get_uploaded_file_id(filename: str, session: str, rtr_api: RealTimeResponse) -> str:
+ """Retrieve the SHA256 ID for the upload file."""
+ sha = None
+ fileid = None
+ while not sha:
+ try:
+ result = rtr_api.list_files_v2(session_id=session).data
+ for item in result:
+ if item["name"] == filename and item["sha256"]:
+ sha = item["sha256"]
+ fileid = item["id"]
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ð File unique ID retrieved", end=f"{' '*30}\r")
+
+ return sha, fileid
+
+
+def stream_download_file(sha256: str,
+ session: str,
+ chunk_size: int,
+ save_filename: str,
+ rtr_api: RealTimeResponse
+ ) -> None:
+ """Perform a streaming download of the target file from the CrowdStrike cloud."""
+ try:
+ progress = Indicator()
+ arrow = Arrow()
+ not_ready = True
+ while not_ready:
+ try:
+ with rtr_api.get_extracted_file_contents(sha256=sha256,
+ session_id=session,
+ filename=save_filename,
+ stream=True
+ ) as request:
+ request.raise_for_status()
+ print(f"{' '*58}", end="\r")
+ with open(save_filename, "wb") as save_file:
+ chk = 0
+ for chunk in request.iter_content(chunk_size=chunk_size):
+ chk += len(chunk)
+ save_file.write(chunk)
+ print(f" {arrow} {chk:.0f} bytes downloaded", end=f"{' '*36}\r")
+ not_ready = False
+ except HTTPError:
+ print(f" âĄïļ Waiting for file to be moved to the CrowdStrike cloud {progress}",
+ end="\r"
+ )
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(f"ðŊ Download complete, {chk} bytes downloaded "
+ f"to the 7-zip archive \"{save_filename}\" "
+ )
+
+
+def delete_file_from_cloud(sha256: str, session: str, rtr_api: RealTimeResponse) -> None:
+ """Remove the get file from the CrowdStrike cloud."""
+ try:
+ rtr_api.delete_file_v2(ids=sha256, session_id=session)
+ print(" ðïļ File removed from the CrowdStrike cloud", end="\r")
+ except APIError as api_error:
+ # Don't end the process so that we can still close out our RTR session.
+ print(f"NON FATAL {api_error}")
+
+
+def check_for_existing_file(filename: str, overwrite: bool) -> None:
+ """Check for the existence of our save file and inform the user it will be overwritten."""
+ if not overwrite:
+ if os.path.exists(filename):
+ keep_going = False
+ answer = input(f"The save file {filename} already exists and will be overwritten. "
+ "Continue (Y/N)? "
+ )
+ if answer in ["Y", "y", "yes", "Yes", "YES"]:
+ keep_going = True
+ if not keep_going:
+ raise SystemExit("File download procedure cancelled by user")
+
+
+def main_routine(cmdline: Namespace) -> None:
+ """Execute the process based upon specified command line arguments."""
+ # Check for the save file and inform the user it will be overwritten.
+ check_for_existing_file(cmdline.save_file, cmdline.overwrite)
+ # Enable debugging if it has been specified on the command line.
+ if cmdline.debug:
+ logging.basicConfig(level=logging.DEBUG)
+ # Create instances of our three necessary FalconPy Service Classes.
+ rtr, hosts = open_sdk(cmdline.debug,
+ cmdline.falcon_client_id,
+ cmdline.falcon_client_secret
+ )
+ # Retrieve our host's AID.
+ device_id = cmdline.aid
+ if not device_id:
+ device_id = get_host_aid(cmdline.hostname, hosts)
+ # Initialize a Real Time Response session with the host.
+ session_id = create_rtr_session(device_id, rtr)
+ # Execute a get command for our target file.
+ task_id = get_target_file(cmdline.filename, session_id, rtr)
+ # Wait for the file to upload to the CrowdStrike cloud.
+ wait_for_upload(task_id, rtr)
+ # Retrieve the SHA256 file identifier.
+ file_sha, file_id = get_uploaded_file_id(cmdline.filename, session_id, rtr)
+ # Perform a streaming download of the target file.
+ stream_download_file(file_sha, session_id, cmdline.chunk_size, cmdline.save_file, rtr)
+ # Delete the file from the CrowdStrike cloud.
+ delete_file_from_cloud(file_id, session_id, rtr)
+ # Close the Real Time Response session.
+ close_rtr_session(session_id, rtr)
+
+
+if __name__ == "__main__":
+ # Parse any provided command line arguments and execute the main routine
+ main_routine(parse_command_line())
diff --git a/samples/rtr/streaming_download_uber.py b/samples/rtr/streaming_download_uber.py
new file mode 100644
index 00000000..e66e300d
--- /dev/null
+++ b/samples/rtr/streaming_download_uber.py
@@ -0,0 +1,364 @@
+r"""Real Time Response API streaming download sample.
+
+._____________._.______ ._______.______ ._____.___ .___ .______ ._____
+| ___/\__ _:|: __ \ : .____/: \ : |: __|: \ :_ ___\
+|___ \ | :|| \____|| : _/\ | . || \ / || : || || |___
+| / | || : \ | / \| : || |\/ || || | || / |
+|__:___/ | || |___\|_.: __/|___| ||___| | || ||___| ||. __ |
+ : |___||___| :/ |___| |___||___| |___| :/ |. |
+ : :/
+ :
+.______ ._______ ___ .______ .___ ._______ .______ .______ .________
+:_ _ \ : .___ \ .___ | |: \ | | : .___ \ : \ :_ _ \ | ___/
+| | || : | |: | /\| || || | | : | || . || | ||___ \
+| . | || : || |/ : || | || |/\ | : || : || . | || /
+|. ____/ \_. ___/ | / ||___| || / \ \_. ___/ |___| ||. ____/ |__:___/
+ :/ :/ |______/|___| |___||______/ :/ |___| :/ :
+ : : : : :
+ :
+ FalconPy v1.5.0
+
+ âĶ âĶââ ââââŽââ âââ⎠âââââââââ âĶ âĶââââŽââââââŽââââââ
+ â âââīâââĪ ââŽâ â â âââĪââââââ ââââââĪ ââŽââââââ ââââ
+ ââââââââââīââ ââââīâââī âīââââââ ââ ââââīââââââīââââââ
+
+This sample demonstrates how to perform a streaming download from the
+CrowdStrike Real Time Response API. Files are saved as 7-zip archives.
+
+Requirements:
+ crowdstrike-falconpy v1.5.0+
+
+Creation: 04.23.2025 - jshcodes@CrowdStrike
+"""
+import logging
+import os
+from argparse import ArgumentParser, RawTextHelpFormatter, Namespace
+from typing import List
+from requests.exceptions import HTTPError
+from falconpy import APIError, APIHarnessV2, BaseURL
+
+
+class Indicator:
+ """Over-architected progress indicator ðĪŠ."""
+
+ _indicator = ["ð", "ð", "ð", "ð", "ð", "ð", "ð§", "ð", "ð", "ð", "ð", "ð"]
+
+ def __init__(self):
+ """Initialize the class and set the starting position."""
+ self._position = -1
+
+ def __repr__(self) -> str:
+ """Increment the position and display the current progress indicator value."""
+ self.position += 1
+ if self.position > len(self.indicator) - 1:
+ self.position = 0
+ return self.indicator[self.position]
+
+ @property
+ def indicator(self) -> List[str]:
+ """Progress indicator graphical elements."""
+ return self._indicator
+
+ @property
+ def position(self) -> int:
+ """Progress indicator position."""
+ return self._position
+
+ @position.setter
+ def position(self, value: int):
+ """Set the indicator position."""
+ self._position = value
+
+
+class Arrow(Indicator):
+ """Animated download emoji, I might have played with this sample for too long."""
+
+ _indicator = []
+ for index, element in enumerate(["ðū âŽ
", "ðū âŽ
", "ðūâŽ
", "â
"]):
+ count = 500
+ if index == 3:
+ count = count * 3
+ for i in range(count):
+ _indicator.append(element)
+
+
+def parse_command_line() -> Namespace:
+ """Ingest the provided command line parameters and handle any input errors."""
+ parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter)
+ behave = parser.add_argument_group("behavior", "Download and API behavior arguments.")
+ behave.add_argument("-c", "--chunk_size",
+ help="Streaming download chunk size",
+ default=8192
+ )
+ behave.add_argument("-o", "--overwrite",
+ help="Force overwritting of a pre-existing save file",
+ action="store_true",
+ default=False
+ )
+ behave.add_argument("-d", "--debug",
+ help="Enable API debugging",
+ action="store_true",
+ default=False
+ )
+ file_group = parser.add_argument_group("filename",
+ "You must specify a filename to download.\nIf you do "
+ "not specify a save filename, it will be saved as "
+ "\"result.7z\"."
+ )
+ file_group.add_argument("-f", "--filename",
+ help="Target filename",
+ required=True
+ )
+ file_group.add_argument("-sf", "--save_file",
+ help="Name of the saved file",
+ default="result.7z"
+ )
+ mut_group = parser.add_argument_group("host",
+ "One of the two following arguments must be specified."
+ )
+ mutual = mut_group.add_mutually_exclusive_group(required=True)
+ mutual.add_argument("-n", "--hostname",
+ help="Target hostname (use instead of AID)"
+ )
+ mutual.add_argument("-a", "--aid",
+ help="Target host AID (use instead of hostname)"
+ )
+ auth = parser.add_argument_group("authentication",
+ "If these arguments are not specified, "
+ "Environment Authentication will be attempted.\n"
+ "Environment Authentication: https://falconpy.io/Usage/"
+ "Authenticating-to-the-API.html#environment-authentication"
+ )
+ auth.add_argument("-k", "--falcon_client_id",
+ help="CrowdStrike Falcon API Client ID",
+ default=None
+ )
+ auth.add_argument("-s", "--falcon_client_secret",
+ help="CrowdStrike Falcon API Client Secret",
+ default=None
+ )
+
+ parsed = parser.parse_args()
+ if not isinstance(parsed.chunk_size, int):
+ parsed.chunk_size = 8192
+
+ return parsed
+
+
+def open_sdk(debug: bool,
+ client_id: str,
+ client_secret: str
+ ) -> APIHarnessV2:
+ """Create an instance of Uber Class from the FalconPy SDK."""
+ uber_class = APIHarnessV2(debug=debug,
+ client_id=client_id,
+ client_secret=client_secret,
+ pythonic=True
+ )
+ region_name = ""
+ for region in BaseURL:
+ if region.value == uber_class.base_url.replace("https://", ""):
+ region_name = f" {region.name}"
+
+ print(f" ð Connection to CrowdStrike API{region_name} established", end="\r")
+
+ return uber_class
+
+
+def get_host_aid(hostname: str, sdk: APIHarnessV2) -> str:
+ """Retrieve the host AID."""
+ try:
+ returned = sdk.command("CombinedDevicesByFilter", filter=f"hostname:'{hostname}'").data
+ if not returned:
+ raise SystemExit("No hosts using this hostname were identified.")
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ð Host AID identified", end=f"{' '*30}\r")
+
+ return returned[0]["device_id"]
+
+
+def create_rtr_session(host_aid: str, sdk: APIHarnessV2) -> str:
+ """Initialize an RTR session with the target host."""
+ try:
+ body_payload = {"device_id": host_aid}
+ session = sdk.command("RTR_InitSession", body=body_payload)
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ð Real Time Response connection established", end=f"{' '*20}\r")
+
+ return session.data[0]["session_id"]
+
+
+def close_rtr_session(session: str, sdk: APIHarnessV2) -> None:
+ """Close the RTR session with the target host."""
+ try:
+ sdk.command("RTR_DeleteSession", session_id=session)
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" âïļâðĨ Real Time Response connection disconnected", end=f"{' '*20}\r")
+
+
+def get_target_file(filename: str, session: str, sdk: APIHarnessV2) -> str:
+ """Execute a get command for the target file and upload it to the CrowdStrike cloud."""
+ try:
+ body_payload = {
+ "base_command": "get",
+ "session_id": session,
+ "command_string": f"get {filename}"
+ }
+ get_request = sdk.command("RTR_ExecuteActiveResponderCommand", body=body_payload).data
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ðĪ Get file command sent", end=f"{' '*30}\r")
+
+ return get_request[0]["cloud_request_id"]
+
+
+def wait_for_upload(cloud_request_id: str, sdk: APIHarnessV2) -> None:
+ """Wait for the upload to complete and return the file SHA256 identifier."""
+ status = False
+ while not status:
+ try:
+ result = sdk.command("RTR_CheckActiveResponderCommandStatus",
+ cloud_request_id=cloud_request_id,
+ sequence_id=0
+ )
+ status = result.data[0]["complete"]
+ print(" âģ Waiting for get command to process", end=f"{' '*30}\r")
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+ if result.data[0]["stderr"]:
+ print(f"{' '*80}")
+ raise SystemExit(f"ERROR: {result.data[0]['stderr']}")
+
+
+def get_uploaded_file_id(filename: str, session: str, sdk: APIHarnessV2) -> str:
+ """Retrieve the SHA256 ID for the upload file."""
+ sha = None
+ fileid = None
+ while not sha:
+ try:
+ result = sdk.command("RTR_ListFilesV2", session_id=session).data
+ for item in result:
+ if item["name"] == filename and item["sha256"]:
+ sha = item["sha256"]
+ fileid = item["id"]
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(" ð File unique ID retrieved", end=f"{' '*30}\r")
+
+ return sha, fileid
+
+
+def wait_indicator(value: int = -1) -> int:
+ """Create a simple progress indicator."""
+ indicator = ["|", "/", "â", "\\"]
+ value += 1
+ if value > 3:
+ value = 0
+
+ return value, indicator[value]
+
+
+def stream_download_file(sha256: str,
+ session: str,
+ chunk_size: int,
+ save_filename: str,
+ sdk: APIHarnessV2
+ ) -> None:
+ """Perform a streaming download of the target file from the CrowdStrike cloud."""
+ try:
+ progress = Indicator()
+ arrow = Arrow()
+ not_ready = True
+ while not_ready:
+ try:
+ with sdk.command("RTR_GetExtractedFileContents",
+ sha256=sha256,
+ session_id=session,
+ filename=save_filename,
+ stream=True
+ ) as request:
+ request.raise_for_status()
+ print(f"{' '*58}", end="\r")
+ with open(save_filename, "wb") as save_file:
+ chk = 0
+ for chunk in request.iter_content(chunk_size=chunk_size):
+ chk += len(chunk)
+ save_file.write(chunk)
+ print(f" {arrow} {chk:.0f} bytes downloaded", end=f"{' '*36}\r")
+ not_ready = False
+ except HTTPError:
+ print(f" âĄïļ Waiting for file to be moved to the CrowdStrike cloud {progress}",
+ end="\r"
+ )
+ except APIError as api_error:
+ raise SystemExit(api_error) from api_error
+
+ print(f"ðŊ Download complete, {chk} bytes downloaded "
+ f"to the 7-zip archive \"{save_filename}\" "
+ )
+
+
+def delete_file_from_cloud(sha256: str, session: str, sdk: APIHarnessV2) -> None:
+ """Remove the get file from the CrowdStrike cloud."""
+ try:
+ sdk.command("RTR_DeleteFileV2", ids=sha256, session_id=session)
+ print(" ðïļ File removed from the CrowdStrike cloud", end="\r")
+ except APIError as api_error:
+ # Don't end the process so that we can still close out our RTR session.
+ print(f"NON-FATAL {api_error}")
+
+
+def check_for_existing_file(filename: str, overwrite: bool) -> None:
+ """Check for the existence of our save file and inform the user it will be overwritten."""
+ if not overwrite:
+ if os.path.exists(filename):
+ keep_going = False
+ answer = input(f"The save file {filename} already exists and will be overwritten. "
+ "Continue (Y/N)? "
+ )
+ if answer in ["Y", "y", "yes", "Yes", "YES"]:
+ keep_going = True
+ if not keep_going:
+ raise SystemExit("File download procedure cancelled by user")
+
+
+def main_routine(cmdline: Namespace) -> None:
+ """Execute the process based upon specified command line arguments."""
+ # Check for the save file and inform the user it will be overwritten.
+ check_for_existing_file(cmdline.save_file, cmdline.overwrite)
+ # Enable debugging if it has been specified on the command line.
+ if cmdline.debug:
+ logging.basicConfig(level=logging.DEBUG)
+ # Create instances of our three necessary FalconPy Service Classes.
+ uber = open_sdk(cmdline.debug, cmdline.falcon_client_id, cmdline.falcon_client_secret)
+ # Retrieve our host's AID.
+ device_id = cmdline.aid
+ if not device_id:
+ device_id = get_host_aid(cmdline.hostname, uber)
+ # Initialize a Real Time Response session with the host.
+ session_id = create_rtr_session(device_id, uber)
+ # Execute a get command for our target file.
+ task_id = get_target_file(cmdline.filename, session_id, uber)
+ # Wait for the file to upload to the CrowdStrike cloud.
+ wait_for_upload(task_id, uber)
+ # Retrieve the SHA256 file identifier.
+ file_sha, file_id = get_uploaded_file_id(cmdline.filename, session_id, uber)
+ # Perform a streaming download of the target file.
+ stream_download_file(file_sha, session_id, cmdline.chunk_size, cmdline.save_file, uber)
+ # Delete the file from the CrowdStrike cloud.
+ delete_file_from_cloud(file_id, session_id, uber)
+ # Close the Real Time Response session.
+ close_rtr_session(session_id, uber)
+
+
+if __name__ == "__main__":
+ # Parse any provided command line arguments and execute the main routine
+ main_routine(parse_command_line())
diff --git a/samples/spotlight/README.md b/samples/spotlight/README.md
index c88af8ff..116f505c 100644
--- a/samples/spotlight/README.md
+++ b/samples/spotlight/README.md
@@ -217,7 +217,7 @@ python3 spotlight_quick_report.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET
```
#### Adjusting the date range
-Specify the number of days backwards in time to check using the `-d` argument.
+Specify the number of days backwards in time for hosts last seen using the `-d` argument.
```shell
python3 spotlight_quick_report.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -d 5
diff --git a/samples/spotlight/spotlight_quick_report.py b/samples/spotlight/spotlight_quick_report.py
index 68c4bd7d..1aded544 100644
--- a/samples/spotlight/spotlight_quick_report.py
+++ b/samples/spotlight/spotlight_quick_report.py
@@ -99,7 +99,7 @@ def consume_arguments() -> Namespace:
return parsed
-def query_spotlight(key: str, secret: str, days: str, aft: str = None):
+def query_spotlight(key: str, secret: str, days: str, aft: str = None, dbg: bool = False):
"""Retrieve a batch of Spotlight Vulnerability matches."""
def do_query(qfilter: str):
@@ -113,7 +113,7 @@ def do_query(qfilter: str):
return returned["status_code"], returned
- spotlight = SpotlightVulnerabilities(client_id=key, client_secret=secret)
+ spotlight = SpotlightVulnerabilities(client_id=key, client_secret=secret, debug=dbg)
global HOST_AUTH # pylint: disable=W0603
HOST_AUTH = spotlight # Save this here so we can use it to auth to hosts
@@ -185,7 +185,8 @@ def process_matches(arg: Namespace):
total, after, returned, result = query_spotlight(key=arg.client_id,
secret=arg.client_secret,
days=arg.days,
- aft=after
+ aft=after,
+ dbg=arg.debug
)
retrieved += returned
for match in result: