Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/code-quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v3
- name: Install Poetry via pipx
Expand Down
30 changes: 30 additions & 0 deletions falcon_toolkit/shell/parsers.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from falcon_toolkit.common.namespace import FalconRecursiveNamespace

CLOUD_SCRIPT_CHOICES = []
FALCON_SCRIPT_CHOICES = []
PUT_FILE_CHOICES = []


Expand Down Expand Up @@ -141,6 +142,34 @@
eventlog_parser_view.add_argument(
"source_name", nargs="?", help="Name of the event source, e.g. 'WinLogon'"
)

falconscript_parser = Cmd2ArgumentParser()
falconscript_parser.add_argument(
"-Name",
dest="falcon_script_name",
help="Name of the Falcon script to run (tab completes)",
choices=FALCON_SCRIPT_CHOICES,
)
falconscript_json_group = falconscript_parser.add_mutually_exclusive_group(required=False)
falconscript_json_group.add_argument(
"-JsonInput",
dest="json_input",
help=(
"JSON input for the Falcon script. You may wrap the JSON in single quotes which will be "
"handled by Falcon Toolkit correctly. You may alternative use -JsonInputFile to provide "
"a path to a .JSON input file on disk instead."
),
)
falconscript_json_group.add_argument(
"-JsonInputFile",
dest="json_input_file_path",
help=(
"Provide a path to a .json file containing input data for the Falcon script, instead of "
"passing JSON data directly on the command line via -JsonInput."
),
completer=Cmd.path_complete,
)

ls_argparser = Cmd2ArgumentParser()
ls_argparser.add_argument(
"directory",
Expand Down Expand Up @@ -677,6 +706,7 @@
"env": env_argparser,
"eventlog": eventlog_argparser,
"ls": ls_argparser,
"falconscript": falconscript_parser,
"filehash": filehash_argparser,
"get": get_argparser,
"get_status": get_status_argparser,
Expand Down
100 changes: 83 additions & 17 deletions falcon_toolkit/shell/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import concurrent.futures
import csv
import json
import os
import sys

Expand All @@ -31,11 +32,12 @@
from falcon_toolkit.shell.cmd_generators.reg import reg_builder
from falcon_toolkit.shell.parsers import (
CLOUD_SCRIPT_CHOICES,
FALCON_SCRIPT_CHOICES,
PARSERS,
PUT_FILE_CHOICES,
)
from falcon_toolkit.shell.refresh import SessionRefreshTimer
from falcon_toolkit.shell.utils import output_file_name
from falcon_toolkit.shell.utils import output_falcon_script_result, output_file_name


# pylint: disable=too-many-instance-attributes
Expand Down Expand Up @@ -129,18 +131,6 @@ def __init__(
spinner = click_spinner.Spinner()
spinner.start()

def _grab_put_files():
"""Load a list of RTR PUT files into the put command's parser.

This is for tab completion.
"""
put_files = self.client.rtr.describe_put_files()
put_file_names = []
for put_file_id in put_files.keys():
put_file_names.append(put_files[put_file_id]["name"])
PUT_FILE_CHOICES.clear()
PUT_FILE_CHOICES.extend(sorted(put_file_names))

def _grab_custom_scripts():
"""Load a list of RTR cloud scripts into the runscript command's parser.

Expand All @@ -153,17 +143,43 @@ def _grab_custom_scripts():
CLOUD_SCRIPT_CHOICES.clear()
CLOUD_SCRIPT_CHOICES.extend(sorted(script_names))

def _grab_falcon_scripts():
"""Load a list of Falcon scripts into the falconscript command's parser.

This is for tab completion.
"""
falcon_scripts = self.client.rtr.describe_falcon_scripts()
script_names = []
for script_id in falcon_scripts.keys():
script_names.append(falcon_scripts[script_id]["name"])
FALCON_SCRIPT_CHOICES.clear()
FALCON_SCRIPT_CHOICES.extend(script_names)

def _grab_put_files():
"""Load a list of RTR PUT files into the put command's parser.

This is for tab completion.
"""
put_files = self.client.rtr.describe_put_files()
put_file_names = []
for put_file_id in put_files.keys():
put_file_names.append(put_files[put_file_id]["name"])
PUT_FILE_CHOICES.clear()
PUT_FILE_CHOICES.extend(sorted(put_file_names))

# Parallelise all data retrieval tasks
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
grab_custom_scripts_future = executor.submit(_grab_custom_scripts)
grab_falcon_scripts_future = executor.submit(_grab_falcon_scripts)
grab_put_files_future = executor.submit(_grab_put_files)
grab_scripts_future = executor.submit(_grab_custom_scripts)
device_data_future = executor.submit(
self.client.hosts.get_device_data,
device_ids,
)

_ = grab_custom_scripts_future.result()
_ = grab_falcon_scripts_future.result()
_ = grab_put_files_future.result()
_ = grab_scripts_future.result()
self.device_data = device_data_future.result()

spinner.stop()
Expand Down Expand Up @@ -405,7 +421,9 @@ def write_result_row(self, command: str, aid: str, complete: bool, stdout: str,
self.csv_writer.writerow(row)
self.output_line_n += 1

def send_generic_command(self, command: str) -> Tuple[Optional[str], Optional[str]]:
def send_generic_command(
self, command: str, skip_stdout_print=False
) -> Tuple[Optional[str], Optional[str]]:
"""Execute an arbitrary RTR command on the hosts within the session set.

This function is used by other RTR commands to implement simple shell -> RTR command
Expand Down Expand Up @@ -447,7 +465,7 @@ def send_generic_command(self, command: str) -> Tuple[Optional[str], Optional[st
# one host and stderr from another
outputs = (stdout, stderr)

if not printed_first:
if not printed_first and not skip_stdout_print:
hostname = self.device_data[aid].get("hostname", "<NO HOSTNAME>")
self.poutput(f"{hostname}: {stdout}")
self.perror(f"{Fore.RED}{hostname}: {stderr}{Fore.RESET}")
Expand Down Expand Up @@ -612,6 +630,54 @@ def do_eventlog(self, args):

self.send_generic_command(command)

@with_argparser(PARSERS.falconscript, preserve_quotes=False)
def do_falconscript(self, args):
"""Execute a Falcon-provided script."""
if not args.falcon_script_name:
self.perror(Fore.RED + "You must provide the name of a Falcon script to execute.")
return

falcon_script_name: str = args.falcon_script_name
command = f"falconscript -Name=```{falcon_script_name}```"

if args.json_input:
# Check if the user provided valid JSON before we send it to the Cloud
try:
json.loads(args.json_input)
except json.decoder.JSONDecodeError as e:
self.perror(
Fore.RED
+ "The JSON you provided is not valid. The JSON decoder returned the following "
f"error: {e}"
)
return
command += f" -JsonInput=```'{args.json_input}'```"

elif args.json_input_file_path:
with open(args.json_input_file_path, "r", encoding="utf-8") as json_input_file:
try:
json_input_data = json.load(json_input_file)
except json.decoder.JSONDecodeError as e:
self.perror(
Fore.RED
+ "You JSON within the file at the path you provided is not valid. "
f"The JSON decoder returned the following error: {e}"
)
return
json_output_flat = json.dumps(json_input_data)
command += f" -JsonInput=```'{json_output_flat}'```"

else:
self.poutput(Fore.YELLOW + "Running Falcon script with no JSON parameters provided")

stdout, _ = self.send_generic_command(command, skip_stdout_print=True)

if stdout:
printed_successfully = output_falcon_script_result(stdout)
if not printed_successfully:
self.perror("Could not successfully decode the JSON response:")
self.perror(stdout)

@with_argparser(PARSERS.filehash, preserve_quotes=True)
def do_filehash(self, args):
"""Generate the MD5, SHA1, and SHA256 hashes of a file."""
Expand Down
Loading
Loading