From 69c867743b2a43ebe01881b46e78fa08b88fd133 Mon Sep 17 00:00:00 2001 From: Joshua Hiller Date: Tue, 25 Jun 2024 15:02:30 -0400 Subject: [PATCH 01/30] Add debug command line argument, Linting --- samples/discover/spyglass.py | 37 ++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/samples/discover/spyglass.py b/samples/discover/spyglass.py index 9f1a2b2c3..b65b2a2b6 100644 --- a/samples/discover/spyglass.py +++ b/samples/discover/spyglass.py @@ -29,6 +29,7 @@ # pylint: disable=R0902,R0904,R0912,R0914,R0915 import os import json +import logging from datetime import datetime from argparse import ArgumentParser, RawTextHelpFormatter from concurrent.futures import ThreadPoolExecutor @@ -66,6 +67,8 @@ class Application: """Class to store configuration and performance detail.""" + _debug = False + _timing = { "start_time": datetime.now().timestamp(), "end_time": 0, @@ -88,6 +91,7 @@ class Application: _status = {"running": True, "cancelled": False} def __init__(self): + """Construct an instance of the application.""" self.configure_application() if not self.show_updates: print("Depending on the size of your environment, " @@ -214,6 +218,11 @@ def configure_application(self): default=None, required=False ) + parser.add_argument("--debug", + help="Enable API debugging", + action="store_true", + default=False + ) parsed = parser.parse_args() cats = parsed.categories.split(",") if "all" in cats: @@ -243,11 +252,16 @@ def configure_application(self): self.applications_filter = parsed.applications_filter if parsed.applications_sort: self.applications_sort = parsed.applications_sort + if parsed.debug: + self.debug = True + self.show_updates = False + logging.basicConfig(level=logging.DEBUG) # Everything before this moment happens within milliseconds self.sdk = Discover(client_id=parsed.falcon_client_id, client_secret=parsed.falcon_client_secret, - base_url=parsed.region + base_url=parsed.region, + debug=self.debug ) self.hosts = Hosts(auth_object=self.sdk) @@ -288,6 +302,16 @@ def extra(self) -> dict: return self._configuration["extra"] # Mutable + @property + def debug(self) -> bool: + """Retrieve the end time property.""" + return self._debug + + @debug.setter + def debug(self, val: bool): + """Set the end time property.""" + self._debug = val + @property def end_time(self) -> int: """Retrieve the end time property.""" @@ -463,7 +487,7 @@ def batch_get_details(sdk, cat): def get_detail(ids): """Retrieve detail information for the ID list provided.""" returned = False - if APP.running: + if APP.running: # pylint: disable=E0606 APP.api_calls += 1 returned = cmd(ids=ids)["body"]["resources"] return returned @@ -492,9 +516,9 @@ def get_detail(ids): details.extend(get_detail(returned)) if APP.show_updates: print(f" Details for {len(details)} {cat} retrieved.", - end=f"{' '*40}\r", - flush=True - ) + end=f"{' '*40}\r", + flush=True + ) if running_total >= total or not APP.running: running = False else: @@ -670,7 +694,7 @@ def display_accounts(account_list: list): row_break() -def display_hosts(hosts_list: list): +def display_hosts(hosts_list: list): # noqa """Display the hosts results.""" print(category_header("Hosts"), end="\r") row_break(23) @@ -697,6 +721,7 @@ def display_hosts(hosts_list: list): for host in hosts_list: first_seen = '' last_seen = '' + managed = '' if host.get("first_seen_timestamp", None): first_seen = datetime.strptime(host["first_seen_timestamp"], "%Y-%m-%dT%H:%M:%SZ") first_seen = bold(first_seen.strftime("%m-%d-%Y %H:%M:%S")) From aaba0de61050c0d6ba3c26cda5bb050184db1cc9 Mon Sep 17 00:00:00 2001 From: alhumaw Date: Tue, 25 Jun 2024 14:07:47 -0700 Subject: [PATCH 02/30] implemented API debug logging, inserted application requirements to specify crowdstrike-falconpy versioning, linting for whitespace and disabled linting on batch_get for "using variable before assignment" --- samples/intel/intel_search.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/samples/intel/intel_search.py b/samples/intel/intel_search.py index e4b4353da..f5dbb3267 100644 --- a/samples/intel/intel_search.py +++ b/samples/intel/intel_search.py @@ -18,8 +18,15 @@ A maximum of 50,000 results per category will be returned. Creation date: 03.30.23 - jshcodes@CrowdStrike + +This application requires: + pyfiglet + termcolor + tabulate + crowdstrike-falconpy v1.3.0+ """ +import logging from argparse import ArgumentParser, RawTextHelpFormatter, Namespace from concurrent.futures import ThreadPoolExecutor from csv import writer, QUOTE_ALL @@ -109,11 +116,19 @@ def parse_command_line(): help="Output filename prefix for storing results (CSV format).", default=None ) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) parsed = parser.parse_args() allow = ["indicator", "report", "actor"] parsed.types = [t for t in parsed.types.split(",") if t in allow] if parsed.types else allow + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + return parsed @@ -122,6 +137,7 @@ def bold(val: str): return colored(val, attrs=["bold"]) +# pylint: disable=E0606 def batch_get(func: object, filt: str, catg: str): """Asynchronously retrieve Falcon Intelligence API results.""" offset = 0 @@ -167,7 +183,7 @@ def chunk_long_description(desc: str, col_width: int = 120) -> str: def simple_list_display(keyval: str, record: dict, title: str, no_val: bool = False): - """Generic handler for displaying information provided as simple lists.""" + """Dynamic handler for displaying information provided as simple lists.""" if keyval in record: if len(record[keyval]): if no_val: @@ -178,12 +194,12 @@ def simple_list_display(keyval: str, record: dict, title: str, no_val: bool = Fa def large_list_display(keyval: str, record: dict, title: str): - """Generic handler for displaying list information with an underlined header.""" + """Dynamic handler for displaying list information with an underlined header.""" if keyval in record: if len(record[keyval]): res = ", ".join(t["value"].title() for t in record[keyval]) res = f"{chunk_long_description(res)}" - res = f"{colored(title, attrs=['bold','underline'])}\n{res}" + res = f"{colored(title, attrs=['bold', 'underline'])}\n{res}" print(f"{res}\n") @@ -565,7 +581,7 @@ def show_result_totals(act_cnt: int, ind_cnt: int, rep_cnt: int, typ_list: list) def main(args: Namespace): """Search for a specified string and identify if it matches an indicator, report, or actor.""" # Perform the search using an authenticated instance of the Intel Service Class - ret = perform_search(Intel(client_id=args.client_id, client_secret=args.client_secret), + ret = perform_search(Intel(client_id=args.client_id, client_secret=args.client_secret, debug=args.debug), args.find, # Search string args.types, # Types to display args.table_format, # Table format From 15630837da9258e42fe8b8ee5162aa736875c872 Mon Sep 17 00:00:00 2001 From: alhumaw Date: Thu, 27 Jun 2024 16:44:05 -0700 Subject: [PATCH 03/30] init kube map project --- samples/kubernetes/kube_map.py | 303 +++++++++++++++++++++++++++++++++ 1 file changed, 303 insertions(+) create mode 100644 samples/kubernetes/kube_map.py diff --git a/samples/kubernetes/kube_map.py b/samples/kubernetes/kube_map.py new file mode 100644 index 000000000..22deec2ec --- /dev/null +++ b/samples/kubernetes/kube_map.py @@ -0,0 +1,303 @@ +r""" + _______ __ _______ __ __ __ +| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----. +|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__| +|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____| +|: 1 | |: 1 | +|::.. . | |::.. . | FalconPy +`-------' `-------' + + _ ___ _ ____ _____ + | |/ / | | | __ )| ____| + | ' /| | | | _ \| _| + | . \| |_| | |_) | |___ + __ __|_|\_\\___/|____/|_____|__ ____ + | \/ | / \ | _ \| _ \| ____| _ \ + | |\/| | / _ \ | |_) | |_) | _| | |_) | + | | | |/ ___ \| __/| __/| |___| _ < + |_| |_/_/ \_\_| |_| |_____|_| \_\ + + +This sample utilizes the KubernetesProtection sample to map out +your kubenetes assets. Kubernetes assets are found via the Falcon Sensor. + +Creation date: 06.26.23 - alhumaw + + +""" + + +import logging +from argparse import ArgumentParser, RawTextHelpFormatter, Namespace +from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor +from datetime import datetime +from dataclasses import dataclass +from typing import Dict +try: + from termcolor import colored # type: ignore +except ImportError as no_termcolor: + raise SystemExit("The termcolor library must be installed.\n" + "Install it with `python3 -m pip install termcolor" + ) from no_termcolor +try: + from tabulate import tabulate # type: ignore +except ImportError as no_tabulate: + raise SystemExit("The tabulate library must be installed.\n" + "Install it with `python3 -m pip install tabulate`." + ) from no_tabulate +try: + from falconpy import KubernetesProtection, APIError, RealTimeResponseAdmin, Hosts +except ImportError as no_falconpy: + raise SystemExit("The CrowdStrike FalconPy library must be installed.\n" + "Install it with `python3 -m pip install crowdstrike-falconpy`." + ) from no_falconpy + + +def parse_command_line() -> Namespace: + """Parse any provided command line arguments and return the namespace.""" + parser = ArgumentParser(description=__doc__, + formatter_class=RawTextHelpFormatter + ) + require = parser.add_argument_group("required arguments") + require.add_argument("-k", "--client_id", + required=True, + help="CrowdStrike API client ID" + ) + require.add_argument("-s", "--client_secret", + required=True, + help="CrowdStrike API client secret" + ) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) + parser.add_argument("-c", "--cluster_id", + help="Display cluster and it's nodes/pods" + ) + + parsed = parser.parse_args() + + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + + return parsed + + +@dataclass +class Cluster: + """Kubernetes cluster dataclass""" + cluster_id: str + cluster_name: str + version: str + cloud_type: str + node_count: int + + +@dataclass +class Node: + """Kubernetes nodes dataclass""" + node_id: str + node_name: str + parent_cluster_id: str + ip: str + architecture: str + operating_system: str + cpu: str + storage: str + pod_count: int + + +@dataclass +class Pod: + """Kubernetes pods dataclass""" + pod_id: str + pod_name: str + parent_node_name: str + containers: list + namespace: str + container_count: int + + +class KubernetesEnvironment: + """Kubernetes comprehensive environment""" + def __init__(self): + self.clusters: list[Cluster] = [] + self.nodes: list[Node] = [] + self.pods: list[Pod] = [] + + def add_clusters(self, cluster: Cluster) -> None: + """Appends cluster to list of clusters""" + self.clusters = cluster + + def add_nodes(self, node: Node) -> None: + """Appends node to list of nodes""" + self.nodes = node + + def add_pods(self, pod: Pod) -> None: + """Appends pod to list of pods""" + self.pods = pod + + +def generate_clusters(falcon: KubernetesProtection) -> list: + """Retrieves and returns a list of clusters""" + limit = 100 + total = 1 + offset = 0 + all_resp = [] + clusters = [] + while len(all_resp) < total: + resp = falcon.ReadClusterCombined(limit=limit, offset=offset)['body']['resources'] + if resp['status_code'] == 200: + page = resp['body']['meta']['pagination'] + total = page['total'] + offset += 100 + all_resp.extend(resp['body']['resources']) + print(resp['body']['meta']) + print(len(all_resp)) + else: + total = 0 + errors = resp['body']['resources'] + for err in errors: + ecode = err['code'] + emsg = err['message'] + print(f"[{ecode}] {emsg}") + + for batch in all_resp: + new_cluster = Cluster( + cluster_id=batch.get('cluster_id'), + cluster_name=batch.get('cluster_name'), + version=batch.get('kubernetes_version'), + cloud_type=batch.get('cloud_name'), + node_count=batch.get('node_count') + ) + clusters.append(new_cluster) + return clusters + + +def generate_nodes(falcon: KubernetesProtection) -> list: + """Retrieves and returns a list of nodes""" + total = falcon.ReadNodeCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadNodeCombined', total) + nodes = [] + print(total) + print(len(all_resp)) + for batch in all_resp: + new_node = Node( + node_id=batch.get('node_id'), + node_name=batch.get('node_name'), + parent_cluster_id=batch.get('cluster_id'), + ip=batch.get('ipv4'), + architecture=batch.get('architecture'), + operating_system=batch.get('operating_system'), + cpu=batch.get('cpu'), + storage=batch.get('storage'), + pod_count=int(batch.get('pod_count')) + ) + nodes.append(new_node) + return nodes + + +def generate_pods(falcon: KubernetesProtection) -> list: + """Retrieves and returns a list of pods""" + pods = [] + total = falcon.ReadPodCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadPodCombined', total) + print(total) + print(len(all_resp)) + for batch in all_resp: + new_pod = Pod( + pod_id=batch.get('pod_id'), + pod_name=batch.get('pod_name'), + parent_node_name=batch.get('node_name'), + containers=[], + namespace=batch.get('namespace'), + container_count=batch.get('countainer_count') + ) + container_list = batch.get('containers') + if container_list: + for container in container_list: + new_pod.containers.append(container.get('name')) + pods.append(new_pod) + + return pods + + +def response_processing(falcon: KubernetesProtection, endpoint: str, filt: str, limit: int, offset: int) -> list: + """Dynamic API caller for multi-proccessing""" + method = getattr(falcon, endpoint, None) + print("worker generated") + if method is None: + raise AttributeError(f"API object has no method named '{endpoint}'") + if filt: + resp = method(filter=filt, limit=limit, offset=offset)['body']['resources'] + else: + resp = method(limit=limit, offset=offset)['body']['resources'] + return resp + + +def concurent_response(falcon: KubernetesProtection, endpoint: str, total: str, filt=None) -> list: + """Utilizes current futures to asynchronously handle paginated API calls at once""" + chunk_size = 200 + workers = int(total / chunk_size) + 1 + offsets = [i * chunk_size for i in range(workers)] + print(offsets) + all_resp = [] + with ThreadPoolExecutor(max_workers=workers) as e: + future = { + e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) + for offset in offsets + } + for f in future: + all_resp.extend(f.result()) + return all_resp + + +def generate_containers(falcon: KubernetesProtection) -> list: + """Retrieves and returns a list of RUNNING containers""" + running_containers = {} + filt = "running_status:'true'" + total = falcon.ReadContainerCombined()['body']['meta']['pagination']['total'] + + all_resp = concurent_response(falcon, "ReadContainerCombined", filt, total) + for container in all_resp: + container_id = container.get('container_id') + container_name = container.get('container_name') + running_containers[container_id] = container_name + return running_containers.keys() + + +def aggregate_kube(clusters: list, nodes: list, pods: list) -> KubernetesEnvironment: + """Organizes clusters, nodes, and pods into a data structure""" + kube = KubernetesEnvironment() + kube.add_clusters(clusters) + kube.add_nodes(nodes) + kube.add_pods(pods) + + return kube + + +def connect_api(key: str, secret: str, debug: bool) -> KubernetesProtection: + """Connects and returns an instance of the Uber class.""" + try: + if debug: + logging.basicConfig(level=logging.DEBUG) + return KubernetesProtection(client_id=key, client_secret=secret, debug=debug) + except APIError as e: + print(f"Failed to connect to API: {e}") + return e + + +def main(): + """Start Main Execution Routine""" + args = parse_command_line() + falcon = connect_api(key=args.client_id, secret=args.client_secret, debug=args.debug) + #clusters = generate_clusters(falcon) + #nodes = generate_nodes(falcon) + #pods = generate_pods(falcon) + #kube = aggregate_kube(clusters, nodes, pods) + generate_nodes(falcon) + + +if __name__ == "__main__": + main() From 637597f249ff4bff296dc52d3ef93ff8957a24cc Mon Sep 17 00:00:00 2001 From: alhumaw Date: Fri, 28 Jun 2024 11:01:18 -0700 Subject: [PATCH 04/30] working on printing assets --- samples/kubernetes/kube_map.py | 246 +++++++++++++++++++++++---------- 1 file changed, 174 insertions(+), 72 deletions(-) diff --git a/samples/kubernetes/kube_map.py b/samples/kubernetes/kube_map.py index 22deec2ec..961baaeb9 100644 --- a/samples/kubernetes/kube_map.py +++ b/samples/kubernetes/kube_map.py @@ -46,7 +46,7 @@ "Install it with `python3 -m pip install tabulate`." ) from no_tabulate try: - from falconpy import KubernetesProtection, APIError, RealTimeResponseAdmin, Hosts + from falconpy import KubernetesProtection, APIError except ImportError as no_falconpy: raise SystemExit("The CrowdStrike FalconPy library must be installed.\n" "Install it with `python3 -m pip install crowdstrike-falconpy`." @@ -72,8 +72,23 @@ def parse_command_line() -> Namespace: action="store_true", default=False ) - parser.add_argument("-c", "--cluster_id", - help="Display cluster and it's nodes/pods" + parser.add_argument("-c", "--cluster", + help="Display clusters and it's nodes", + action="store_true", + default=False + ) + parser.add_argument("-n", "--node", + help="Display nodes and it's pods", + action="store_true", + default=False + ) + parser.add_argument("-p", "--pod_id", + help="Display specific pod information" + ) + parser.add_argument("-t", "--thread", + help="Enables asynchronous API calls for faster returns", + action="store_true", + default=False ) parsed = parser.parse_args() @@ -99,7 +114,7 @@ class Node: """Kubernetes nodes dataclass""" node_id: str node_name: str - parent_cluster_id: str + parent_cluster_name: str ip: str architecture: str operating_system: str @@ -139,72 +154,61 @@ def add_pods(self, pod: Pod) -> None: self.pods = pod -def generate_clusters(falcon: KubernetesProtection) -> list: +def generate_clusters(falcon: KubernetesProtection, thread: bool) -> list: """Retrieves and returns a list of clusters""" - limit = 100 - total = 1 - offset = 0 - all_resp = [] + print("Locating Clusters...") + if thread: + total = falcon.ReadClusterCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadClusterCombined', total) + else: + all_resp = normal_response(falcon, "ReadClusterCombined") clusters = [] - while len(all_resp) < total: - resp = falcon.ReadClusterCombined(limit=limit, offset=offset)['body']['resources'] - if resp['status_code'] == 200: - page = resp['body']['meta']['pagination'] - total = page['total'] - offset += 100 - all_resp.extend(resp['body']['resources']) - print(resp['body']['meta']) - print(len(all_resp)) - else: - total = 0 - errors = resp['body']['resources'] - for err in errors: - ecode = err['code'] - emsg = err['message'] - print(f"[{ecode}] {emsg}") - for batch in all_resp: new_cluster = Cluster( - cluster_id=batch.get('cluster_id'), - cluster_name=batch.get('cluster_name'), - version=batch.get('kubernetes_version'), - cloud_type=batch.get('cloud_name'), - node_count=batch.get('node_count') + cluster_id=batch['cluster_id'], + cluster_name=batch['cluster_name'], + version=batch['kubernetes_version'], + cloud_type=batch['cloud_name'], + node_count=batch['node_count'] ) clusters.append(new_cluster) return clusters -def generate_nodes(falcon: KubernetesProtection) -> list: +def generate_nodes(falcon: KubernetesProtection, thread: bool) -> list: """Retrieves and returns a list of nodes""" - total = falcon.ReadNodeCombined()['body']['meta']['pagination']['total'] - all_resp = concurent_response(falcon, 'ReadNodeCombined', total) + print("Discovering Nodes...") + if thread: + total = falcon.ReadNodeCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadNodeCombined', total) + else: + all_resp = normal_response(falcon, "ReadNodeCombined") nodes = [] - print(total) - print(len(all_resp)) for batch in all_resp: new_node = Node( - node_id=batch.get('node_id'), - node_name=batch.get('node_name'), - parent_cluster_id=batch.get('cluster_id'), - ip=batch.get('ipv4'), - architecture=batch.get('architecture'), - operating_system=batch.get('operating_system'), - cpu=batch.get('cpu'), - storage=batch.get('storage'), - pod_count=int(batch.get('pod_count')) + node_id=batch['node_id'], + node_name=batch['node_name'], + parent_cluster_name=batch['cluster_name'], + ip=batch['ipv4'], + architecture=batch['architecture'], + operating_system=batch['os'], + cpu=batch['cpu'], + storage=batch['storage'], + pod_count=int(batch['pod_count']) ) nodes.append(new_node) return nodes -def generate_pods(falcon: KubernetesProtection) -> list: +def generate_pods(falcon: KubernetesProtection, thread: bool) -> list: """Retrieves and returns a list of pods""" + print("Finding Pods...") + if thread: + total = falcon.ReadPodCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadPodCombined', total) + else: + all_resp = normal_response(falcon, "ReadPodCombined") pods = [] - total = falcon.ReadPodCombined()['body']['meta']['pagination']['total'] - all_resp = concurent_response(falcon, 'ReadPodCombined', total) - print(total) - print(len(all_resp)) for batch in all_resp: new_pod = Pod( pod_id=batch.get('pod_id'), @@ -226,7 +230,6 @@ def generate_pods(falcon: KubernetesProtection) -> list: def response_processing(falcon: KubernetesProtection, endpoint: str, filt: str, limit: int, offset: int) -> list: """Dynamic API caller for multi-proccessing""" method = getattr(falcon, endpoint, None) - print("worker generated") if method is None: raise AttributeError(f"API object has no method named '{endpoint}'") if filt: @@ -236,30 +239,64 @@ def response_processing(falcon: KubernetesProtection, endpoint: str, filt: str, return resp +def normal_response(falcon: KubernetesProtection, endpoint: str, filt=None): + """Normal API caller""" + limit = 200 + all_resp = [] + total = 1 + offset = 0 + method = getattr(falcon, endpoint, None) + if method is None: + raise AttributeError(f"API object has no method named '{endpoint}'") + + while len(all_resp) < total: + resp = method(limit=limit, offset=offset, filt=(filt if filt else None)) + if resp['status_code'] == 200: + total = resp['body']['meta']['pagination']['total'] + offset += 200 + all_resp.extend(resp['body']['resources']) + + return all_resp + + def concurent_response(falcon: KubernetesProtection, endpoint: str, total: str, filt=None) -> list: - """Utilizes current futures to asynchronously handle paginated API calls at once""" + """Utilizes concurrent futures to asynchronously handle paginated API calls at once""" chunk_size = 200 workers = int(total / chunk_size) + 1 offsets = [i * chunk_size for i in range(workers)] - print(offsets) all_resp = [] - with ThreadPoolExecutor(max_workers=workers) as e: - future = { - e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) - for offset in offsets - } - for f in future: - all_resp.extend(f.result()) + if workers > 10: + batches = [offsets[x: x + 10] for x in range(0, len(offsets), 10)] + for batch in batches: + with ThreadPoolExecutor(max_workers=workers) as e: + future = { + e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) + for offset in batch + } + for f in future: + all_resp.extend(f.result()) + else: + with ThreadPoolExecutor(max_workers=workers) as e: + future = { + e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) + for offset in offsets + } + for f in future: + all_resp.extend(f.result()) + return all_resp -def generate_containers(falcon: KubernetesProtection) -> list: +def generate_containers(falcon: KubernetesProtection, thread: bool) -> list: """Retrieves and returns a list of RUNNING containers""" + print("Generating Containers...") running_containers = {} filt = "running_status:'true'" - total = falcon.ReadContainerCombined()['body']['meta']['pagination']['total'] - - all_resp = concurent_response(falcon, "ReadContainerCombined", filt, total) + if thread: + total = falcon.ReadContainerCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, "ReadContainerCombined", filt, total) + else: + all_resp = normal_response(falcon, "ReadContainerCombined", filt) for container in all_resp: container_id = container.get('container_id') container_name = container.get('container_name') @@ -267,12 +304,13 @@ def generate_containers(falcon: KubernetesProtection) -> list: return running_containers.keys() -def aggregate_kube(clusters: list, nodes: list, pods: list) -> KubernetesEnvironment: +def aggregate_kube(clusters: list, nodes: list, pods=None) -> KubernetesEnvironment: """Organizes clusters, nodes, and pods into a data structure""" kube = KubernetesEnvironment() kube.add_clusters(clusters) kube.add_nodes(nodes) - kube.add_pods(pods) + if pods: + kube.add_pods(pods) return kube @@ -288,16 +326,80 @@ def connect_api(key: str, secret: str, debug: bool) -> KubernetesProtection: return e +def form_relations(kube: KubernetesEnvironment, args: Namespace) -> None: + """Prints out the kubernetes environment""" + cluster_groups = {} + node_groups = {} + if args.cluster: + for cluster in kube.clusters: + cluster_groups[cluster.cluster_name] = { + 'cluster id': cluster.cluster_id, + 'cluster name': cluster.cluster_name, + 'version': cluster.version, + 'cloud type': cluster.cloud_type, + 'connected nodes': cluster.node_count + } + if cluster.node_count > 0: + cluster_groups[cluster.cluster_name]['nodes'] = [] + for node in kube.nodes: + if node.parent_cluster_name == cluster.cluster_name: + cluster_groups[cluster.cluster_name]['nodes'].append(node.node_id) + return cluster_groups + if args.node: + for node in kube.nodes: + node_groups[node.node_name] = { + 'node id': node.node_id, + 'node name': node.node_name, + 'ip': node.ip, + 'architecture': node.architecture, + 'operating system': node.operating_system, + 'cpu': node.cpu, + 'storage': node.storage, + 'pod count': node.pod_count + } + if node.pod_count > 0: + node_groups[node.node_name]['pods'] = [] + for pod in kube.pods: + if pod.pod_name == node.node_name: + node_groups[node.node_name]['pods'].append(pod.pod_id) + return node_groups + + +def find_asset_count(falcon: KubernetesProtection) -> dict: + """Find and output the initial count of all assets""" + env = {} + containers = falcon.ReadContainerCount()['body']['resources'][0]['count'] + pods = falcon.ReadPodCount()['body']['resources'][0]['count'] + nodes = falcon.ReadNodeCount()['body']['resources'][0]['count'] + clusters = falcon.ReadClusterCount()['body']['resources'][0]['count'] + env = { + 'containers': containers, + 'pods': pods, + 'nodes': nodes, + 'clusters': clusters + } + headers = ['containers', 'pods', 'nodes', 'clusters'] + print(tabulate(env, headers="keys")) + + return env + + def main(): """Start Main Execution Routine""" args = parse_command_line() falcon = connect_api(key=args.client_id, secret=args.client_secret, debug=args.debug) - #clusters = generate_clusters(falcon) - #nodes = generate_nodes(falcon) - #pods = generate_pods(falcon) - #kube = aggregate_kube(clusters, nodes, pods) - generate_nodes(falcon) - + if args.cluster or args.node: + clusters = generate_clusters(falcon, args.thread) + nodes = generate_nodes(falcon, args.thread) + if args.node: + pods = generate_pods(falcon, args.thread) + kube = aggregate_kube(clusters, nodes, pods) + kube = aggregate_kube(clusters, nodes) + kube_dictionary = form_relations(kube, args) + print(kube_dictionary) + else: + env = find_asset_count(falcon) + print(env) if __name__ == "__main__": main() From e2d6fa41272fad5f13e03c751a34d7e4f0b32999 Mon Sep 17 00:00:00 2001 From: alhumaw Date: Fri, 28 Jun 2024 11:36:09 -0700 Subject: [PATCH 05/30] remove kube_map sample --- samples/kubernetes/kube_map.py | 405 --------------------------------- 1 file changed, 405 deletions(-) delete mode 100644 samples/kubernetes/kube_map.py diff --git a/samples/kubernetes/kube_map.py b/samples/kubernetes/kube_map.py deleted file mode 100644 index 961baaeb9..000000000 --- a/samples/kubernetes/kube_map.py +++ /dev/null @@ -1,405 +0,0 @@ -r""" - _______ __ _______ __ __ __ -| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----. -|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__| -|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____| -|: 1 | |: 1 | -|::.. . | |::.. . | FalconPy -`-------' `-------' - - _ ___ _ ____ _____ - | |/ / | | | __ )| ____| - | ' /| | | | _ \| _| - | . \| |_| | |_) | |___ - __ __|_|\_\\___/|____/|_____|__ ____ - | \/ | / \ | _ \| _ \| ____| _ \ - | |\/| | / _ \ | |_) | |_) | _| | |_) | - | | | |/ ___ \| __/| __/| |___| _ < - |_| |_/_/ \_\_| |_| |_____|_| \_\ - - -This sample utilizes the KubernetesProtection sample to map out -your kubenetes assets. Kubernetes assets are found via the Falcon Sensor. - -Creation date: 06.26.23 - alhumaw - - -""" - - -import logging -from argparse import ArgumentParser, RawTextHelpFormatter, Namespace -from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor -from datetime import datetime -from dataclasses import dataclass -from typing import Dict -try: - from termcolor import colored # type: ignore -except ImportError as no_termcolor: - raise SystemExit("The termcolor library must be installed.\n" - "Install it with `python3 -m pip install termcolor" - ) from no_termcolor -try: - from tabulate import tabulate # type: ignore -except ImportError as no_tabulate: - raise SystemExit("The tabulate library must be installed.\n" - "Install it with `python3 -m pip install tabulate`." - ) from no_tabulate -try: - from falconpy import KubernetesProtection, APIError -except ImportError as no_falconpy: - raise SystemExit("The CrowdStrike FalconPy library must be installed.\n" - "Install it with `python3 -m pip install crowdstrike-falconpy`." - ) from no_falconpy - - -def parse_command_line() -> Namespace: - """Parse any provided command line arguments and return the namespace.""" - parser = ArgumentParser(description=__doc__, - formatter_class=RawTextHelpFormatter - ) - require = parser.add_argument_group("required arguments") - require.add_argument("-k", "--client_id", - required=True, - help="CrowdStrike API client ID" - ) - require.add_argument("-s", "--client_secret", - required=True, - help="CrowdStrike API client secret" - ) - parser.add_argument("-d", "--debug", - help="Enable API debugging", - action="store_true", - default=False - ) - parser.add_argument("-c", "--cluster", - help="Display clusters and it's nodes", - action="store_true", - default=False - ) - parser.add_argument("-n", "--node", - help="Display nodes and it's pods", - action="store_true", - default=False - ) - parser.add_argument("-p", "--pod_id", - help="Display specific pod information" - ) - parser.add_argument("-t", "--thread", - help="Enables asynchronous API calls for faster returns", - action="store_true", - default=False - ) - - parsed = parser.parse_args() - - if parsed.debug: - logging.basicConfig(level=logging.DEBUG) - - return parsed - - -@dataclass -class Cluster: - """Kubernetes cluster dataclass""" - cluster_id: str - cluster_name: str - version: str - cloud_type: str - node_count: int - - -@dataclass -class Node: - """Kubernetes nodes dataclass""" - node_id: str - node_name: str - parent_cluster_name: str - ip: str - architecture: str - operating_system: str - cpu: str - storage: str - pod_count: int - - -@dataclass -class Pod: - """Kubernetes pods dataclass""" - pod_id: str - pod_name: str - parent_node_name: str - containers: list - namespace: str - container_count: int - - -class KubernetesEnvironment: - """Kubernetes comprehensive environment""" - def __init__(self): - self.clusters: list[Cluster] = [] - self.nodes: list[Node] = [] - self.pods: list[Pod] = [] - - def add_clusters(self, cluster: Cluster) -> None: - """Appends cluster to list of clusters""" - self.clusters = cluster - - def add_nodes(self, node: Node) -> None: - """Appends node to list of nodes""" - self.nodes = node - - def add_pods(self, pod: Pod) -> None: - """Appends pod to list of pods""" - self.pods = pod - - -def generate_clusters(falcon: KubernetesProtection, thread: bool) -> list: - """Retrieves and returns a list of clusters""" - print("Locating Clusters...") - if thread: - total = falcon.ReadClusterCombined()['body']['meta']['pagination']['total'] - all_resp = concurent_response(falcon, 'ReadClusterCombined', total) - else: - all_resp = normal_response(falcon, "ReadClusterCombined") - clusters = [] - for batch in all_resp: - new_cluster = Cluster( - cluster_id=batch['cluster_id'], - cluster_name=batch['cluster_name'], - version=batch['kubernetes_version'], - cloud_type=batch['cloud_name'], - node_count=batch['node_count'] - ) - clusters.append(new_cluster) - return clusters - - -def generate_nodes(falcon: KubernetesProtection, thread: bool) -> list: - """Retrieves and returns a list of nodes""" - print("Discovering Nodes...") - if thread: - total = falcon.ReadNodeCombined()['body']['meta']['pagination']['total'] - all_resp = concurent_response(falcon, 'ReadNodeCombined', total) - else: - all_resp = normal_response(falcon, "ReadNodeCombined") - nodes = [] - for batch in all_resp: - new_node = Node( - node_id=batch['node_id'], - node_name=batch['node_name'], - parent_cluster_name=batch['cluster_name'], - ip=batch['ipv4'], - architecture=batch['architecture'], - operating_system=batch['os'], - cpu=batch['cpu'], - storage=batch['storage'], - pod_count=int(batch['pod_count']) - ) - nodes.append(new_node) - return nodes - - -def generate_pods(falcon: KubernetesProtection, thread: bool) -> list: - """Retrieves and returns a list of pods""" - print("Finding Pods...") - if thread: - total = falcon.ReadPodCombined()['body']['meta']['pagination']['total'] - all_resp = concurent_response(falcon, 'ReadPodCombined', total) - else: - all_resp = normal_response(falcon, "ReadPodCombined") - pods = [] - for batch in all_resp: - new_pod = Pod( - pod_id=batch.get('pod_id'), - pod_name=batch.get('pod_name'), - parent_node_name=batch.get('node_name'), - containers=[], - namespace=batch.get('namespace'), - container_count=batch.get('countainer_count') - ) - container_list = batch.get('containers') - if container_list: - for container in container_list: - new_pod.containers.append(container.get('name')) - pods.append(new_pod) - - return pods - - -def response_processing(falcon: KubernetesProtection, endpoint: str, filt: str, limit: int, offset: int) -> list: - """Dynamic API caller for multi-proccessing""" - method = getattr(falcon, endpoint, None) - if method is None: - raise AttributeError(f"API object has no method named '{endpoint}'") - if filt: - resp = method(filter=filt, limit=limit, offset=offset)['body']['resources'] - else: - resp = method(limit=limit, offset=offset)['body']['resources'] - return resp - - -def normal_response(falcon: KubernetesProtection, endpoint: str, filt=None): - """Normal API caller""" - limit = 200 - all_resp = [] - total = 1 - offset = 0 - method = getattr(falcon, endpoint, None) - if method is None: - raise AttributeError(f"API object has no method named '{endpoint}'") - - while len(all_resp) < total: - resp = method(limit=limit, offset=offset, filt=(filt if filt else None)) - if resp['status_code'] == 200: - total = resp['body']['meta']['pagination']['total'] - offset += 200 - all_resp.extend(resp['body']['resources']) - - return all_resp - - -def concurent_response(falcon: KubernetesProtection, endpoint: str, total: str, filt=None) -> list: - """Utilizes concurrent futures to asynchronously handle paginated API calls at once""" - chunk_size = 200 - workers = int(total / chunk_size) + 1 - offsets = [i * chunk_size for i in range(workers)] - all_resp = [] - if workers > 10: - batches = [offsets[x: x + 10] for x in range(0, len(offsets), 10)] - for batch in batches: - with ThreadPoolExecutor(max_workers=workers) as e: - future = { - e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) - for offset in batch - } - for f in future: - all_resp.extend(f.result()) - else: - with ThreadPoolExecutor(max_workers=workers) as e: - future = { - e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) - for offset in offsets - } - for f in future: - all_resp.extend(f.result()) - - return all_resp - - -def generate_containers(falcon: KubernetesProtection, thread: bool) -> list: - """Retrieves and returns a list of RUNNING containers""" - print("Generating Containers...") - running_containers = {} - filt = "running_status:'true'" - if thread: - total = falcon.ReadContainerCombined()['body']['meta']['pagination']['total'] - all_resp = concurent_response(falcon, "ReadContainerCombined", filt, total) - else: - all_resp = normal_response(falcon, "ReadContainerCombined", filt) - for container in all_resp: - container_id = container.get('container_id') - container_name = container.get('container_name') - running_containers[container_id] = container_name - return running_containers.keys() - - -def aggregate_kube(clusters: list, nodes: list, pods=None) -> KubernetesEnvironment: - """Organizes clusters, nodes, and pods into a data structure""" - kube = KubernetesEnvironment() - kube.add_clusters(clusters) - kube.add_nodes(nodes) - if pods: - kube.add_pods(pods) - - return kube - - -def connect_api(key: str, secret: str, debug: bool) -> KubernetesProtection: - """Connects and returns an instance of the Uber class.""" - try: - if debug: - logging.basicConfig(level=logging.DEBUG) - return KubernetesProtection(client_id=key, client_secret=secret, debug=debug) - except APIError as e: - print(f"Failed to connect to API: {e}") - return e - - -def form_relations(kube: KubernetesEnvironment, args: Namespace) -> None: - """Prints out the kubernetes environment""" - cluster_groups = {} - node_groups = {} - if args.cluster: - for cluster in kube.clusters: - cluster_groups[cluster.cluster_name] = { - 'cluster id': cluster.cluster_id, - 'cluster name': cluster.cluster_name, - 'version': cluster.version, - 'cloud type': cluster.cloud_type, - 'connected nodes': cluster.node_count - } - if cluster.node_count > 0: - cluster_groups[cluster.cluster_name]['nodes'] = [] - for node in kube.nodes: - if node.parent_cluster_name == cluster.cluster_name: - cluster_groups[cluster.cluster_name]['nodes'].append(node.node_id) - return cluster_groups - if args.node: - for node in kube.nodes: - node_groups[node.node_name] = { - 'node id': node.node_id, - 'node name': node.node_name, - 'ip': node.ip, - 'architecture': node.architecture, - 'operating system': node.operating_system, - 'cpu': node.cpu, - 'storage': node.storage, - 'pod count': node.pod_count - } - if node.pod_count > 0: - node_groups[node.node_name]['pods'] = [] - for pod in kube.pods: - if pod.pod_name == node.node_name: - node_groups[node.node_name]['pods'].append(pod.pod_id) - return node_groups - - -def find_asset_count(falcon: KubernetesProtection) -> dict: - """Find and output the initial count of all assets""" - env = {} - containers = falcon.ReadContainerCount()['body']['resources'][0]['count'] - pods = falcon.ReadPodCount()['body']['resources'][0]['count'] - nodes = falcon.ReadNodeCount()['body']['resources'][0]['count'] - clusters = falcon.ReadClusterCount()['body']['resources'][0]['count'] - env = { - 'containers': containers, - 'pods': pods, - 'nodes': nodes, - 'clusters': clusters - } - headers = ['containers', 'pods', 'nodes', 'clusters'] - print(tabulate(env, headers="keys")) - - return env - - -def main(): - """Start Main Execution Routine""" - args = parse_command_line() - falcon = connect_api(key=args.client_id, secret=args.client_secret, debug=args.debug) - if args.cluster or args.node: - clusters = generate_clusters(falcon, args.thread) - nodes = generate_nodes(falcon, args.thread) - if args.node: - pods = generate_pods(falcon, args.thread) - kube = aggregate_kube(clusters, nodes, pods) - kube = aggregate_kube(clusters, nodes) - kube_dictionary = form_relations(kube, args) - print(kube_dictionary) - else: - env = find_asset_count(falcon) - print(env) - -if __name__ == "__main__": - main() From 83ceb6fd11a525736ce87f8ee4849b18c1bd7da9 Mon Sep 17 00:00:00 2001 From: Alexander Moomaw <108152211+alhumaw@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:10:28 -0700 Subject: [PATCH 06/30] Update README.md --- samples/intel/README.md | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/samples/intel/README.md b/samples/intel/README.md index ab740eb87..132059f6b 100644 --- a/samples/intel/README.md +++ b/samples/intel/README.md @@ -416,12 +416,42 @@ Total indicators: 1 Execution time: 2.86 seconds ``` +Show the debug output. +```shell +python3 intel_search.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -f "FROZEN SPIDER" -d +``` +##### Result +```shell +DEBUG:falconpy._auth_object._falcon_interface:CREATED: OAuth2 interface class +DEBUG:falconpy._auth_object._falcon_interface:AUTH: Configured for Direct Authentication +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Base URL set to https://api.crowdstrike.com +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: SSL verification is set to True +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Timeout set to None seconds +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Proxy dictionary: None +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: User-Agent string set to: None +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Token renewal window set to 120 seconds +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Maximum number of records to log: 100 +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Log sanitization is enabled +DEBUG:falconpy._auth_object._falcon_interface:CONFIG: Pythonic responses are disabled +DEBUG:falconpy._auth_object._falcon_interface:OPERATION: oauth2AccessToken +DEBUG:falconpy._auth_object._falcon_interface:ENDPOINT: https://api.crowdstrike.com/oauth2/token (POST) +DEBUG:falconpy._auth_object._falcon_interface:HEADERS: {'User-Agent': 'crowdstrike-falconpy/1.4.4', 'CrowdStrike-SDK': 'crowdstrike-falconpy/1.4.4'} +DEBUG:falconpy._auth_object._falcon_interface:PARAMETERS: None +DEBUG:falconpy._auth_object._falcon_interface:BODY: None +DEBUG:falconpy._auth_object._falcon_interface:DATA: {'client_id': 'REDACTED', 'client_secret': 'REDACTED'} +DEBUG:urllib3.connectionpool:Starting new HTTPS connection (1): api.crowdstrike.com:443 +``` + #### Command-line help Command-line help is available via the `-h` argument. ```shell python3 intel_search.py -h -usage: intel_search.py [-h] -f FIND -k CLIENT_ID -s CLIENT_SECRET [-r] [-t TYPES] [-tf TABLE_FORMAT] [-o OUTPUT_PREFIX] + +CrowdStrike Falcon Intel API search example using the FalconPy library. + +usage: intel_search.py [-h] -f FIND -k CLIENT_ID -s CLIENT_SECRET [-r] [-t TYPES] [-tf TABLE_FORMAT] + [-o OUTPUT_PREFIX] [-d] CrowdStrike Falcon Intel API search example using the FalconPy library. @@ -443,7 +473,13 @@ A maximum of 50,000 results per category will be returned. Creation date: 03.30.23 - jshcodes@CrowdStrike -optional arguments: +This application requires: + pyfiglet + termcolor + tabulate + crowdstrike-falconpy v1.3.0+ + +options: -h, --help show this help message and exit -r, --reverse Reverse the sort. -t TYPES, --types TYPES @@ -452,6 +488,7 @@ optional arguments: Set the table format. -o OUTPUT_PREFIX, --output_prefix OUTPUT_PREFIX Output filename prefix for storing results (CSV format). + -d, --debug Enable API debugging required arguments: -f FIND, --find FIND Search string to identify From 6c7bacaa0180e4ad0cd649ddded0103aefa04251 Mon Sep 17 00:00:00 2001 From: okewoma Date: Fri, 28 Jun 2024 09:42:34 -0700 Subject: [PATCH 07/30] Add debug functionality --- samples/spotlight/spotlight_quick_report.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/samples/spotlight/spotlight_quick_report.py b/samples/spotlight/spotlight_quick_report.py index dfead5bcd..68c4bd7dc 100644 --- a/samples/spotlight/spotlight_quick_report.py +++ b/samples/spotlight/spotlight_quick_report.py @@ -19,10 +19,11 @@ \___\_\_,_/_/\__/_/\_\ /_/|_|\__/ .__/\___/_/ \__/ /_/ -This example requires crowdstrike-falconpy v1.2.2 or greater. +This example requires crowdstrike-falconpy v1.3.0 or greater. Easy Object Authentication is also demonstrated in this sample. """ +import logging import json import time from datetime import datetime @@ -64,6 +65,11 @@ def consume_arguments() -> Namespace: help="CrowdStrike Falcon API Client Secret.", required=True ) + parser.add_argument("--debug", + help="Enable API debugging", + action="store_true", + default=False + ) parser.add_argument("-d", "--days", help="Include days from X days backwards (3-45).", default=0 @@ -83,8 +89,14 @@ def consume_arguments() -> Namespace: default=False, action="store_true" ) + + + parsed = parser.parse_args() + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + - return parser.parse_args() + return parsed def query_spotlight(key: str, secret: str, days: str, aft: str = None): @@ -274,7 +286,7 @@ def process_results(output_file: str, matches: dict, total_matched: int): # pyl start_time = datetime.now().timestamp() args = consume_arguments() if args.file: - HOST_AUTH = Hosts(client_id=args.client_id, client_secret=args.client_secret) + HOST_AUTH = Hosts(client_id=args.client_id, client_secret=args.client_secret, debug=args.debug) process_results(args.output, *process_matches(args)) total_run_time = datetime.now().timestamp() - start_time print(f"\nReport generated in {total_run_time:,.2f} seconds.") From 67dcc5bc4eb750d8d9b87b6a428b35257196a9cf Mon Sep 17 00:00:00 2001 From: okewoma <78776399+okewoma@users.noreply.github.com> Date: Fri, 28 Jun 2024 12:22:38 -0700 Subject: [PATCH 08/30] Update README.md in spotlight_quick_report to show debugging functionality --- samples/spotlight/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/samples/spotlight/README.md b/samples/spotlight/README.md index 4873e69d5..9441a29f5 100644 --- a/samples/spotlight/README.md +++ b/samples/spotlight/README.md @@ -227,6 +227,12 @@ If you wish to allow duplicate matches to be present within your report, pass th ```shell python3 spotlight_quick_report.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -a ``` +#### Debugging +If you want to debug code and quickly find errors within code `--debug` argument. + +```shell +python3 spotlight_quick_report.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET --debug +``` #### Command-line help Command-line help is available via the `-h` argument. @@ -275,6 +281,7 @@ required arguments: CrowdStrike Falcon API Client ID. -s CLIENT_SECRET, --client_secret CLIENT_SECRET CrowdStrike Falcon API Client Secret. + --debug Enables code debugging ``` ### Example source code From 66dcdf2b9229d4bffa335a4d53274f4a3b5d7369 Mon Sep 17 00:00:00 2001 From: exk200006 Date: Fri, 28 Jun 2024 10:47:24 -0500 Subject: [PATCH 09/30] Add debugging functionality --- samples/discover/list_discovered_hosts.py | 68 ++++++++++++----------- 1 file changed, 37 insertions(+), 31 deletions(-) diff --git a/samples/discover/list_discovered_hosts.py b/samples/discover/list_discovered_hosts.py index abea9c413..28b01fa73 100644 --- a/samples/discover/list_discovered_hosts.py +++ b/samples/discover/list_discovered_hosts.py @@ -25,36 +25,36 @@ Creation date: 02.08.2022 - jshcodes@CrowdStrike """ import os +import logging from argparse import ArgumentParser, RawTextHelpFormatter from tabulate import tabulate try: - from falconpy import Discover + from falconpy import Discover, Hosts except ImportError as no_falconpy: raise SystemExit("The crowdstrike-falconpy package must be installed " - "in order to run this progrem.\n\nInstall with the command: " + "in order to run this program.\n\nInstall with the command: " "python3 -m pip install crowdstrike-falconpy") from no_falconpy - def parse_command_line() -> object: """Parse any received inbound command line parameters.""" parser = ArgumentParser( description=__doc__, formatter_class=RawTextHelpFormatter - ) + ) parser.add_argument( '-k', '--client_id', help='CrowdStrike Falcon API key ID.\n' 'You can also use the `FALCON_CLIENT_ID` environment variable to specify this value.', required=False - ) + ) parser.add_argument( '-s', '--client_secret', help='CrowdStrike Falcon API key secret.\n' 'You can also use the `FALCON_CLIENT_SECRET` environment variable to specify this value.', required=False - ) + ) parser.add_argument( '-b', '--base_url', @@ -68,7 +68,15 @@ def parse_command_line() -> object: help='Reverse sort (defaults to ASC)', required=False, action="store_true" - ) + ) + parser.add_argument( + '-d', + '--debug', + help='Enable API debugging', + required=False, + default=False, + action="store_true" + ) parser.add_argument( '-f', '--format', @@ -77,19 +85,13 @@ def parse_command_line() -> object: 'pretty, psql, rst, mediawiki, moinmoin, youtrack, html, unsafehtml, \n' 'latext, latex_raw, latex_booktabs, latex_longtable, textile, tsv)', required=False - ) - + ) return parser.parse_args() - - def get_sort_key(sorting) -> list: - """Return the sort colum value for sorting operations.""" + """Return the sort column value for sorting operations.""" return sorting["hostname"] - - -# Retrieve all inbound command line parameters +# Retrieve all inbound command line parameters if args debug is present args = parse_command_line() - # Set constants based upon received inputs BASE_URL = "auto" if args.base_url: @@ -106,6 +108,10 @@ def get_sort_key(sorting) -> list: SORT = False else: SORT = bool(args.reverse) +# add debug with logging put after parser +if args.debug: + logging.basicConfig(level=logging.DEBUG) + TABLE_FORMATS = [ "plain", "simple", "github", "grid", "fancy_grid", "pipe", "orgtbl", "jira", "presto", "pretty", "psql", "rst", "mediawiki", "moinmoin", "youtrack", "html", "unsafehtml", @@ -116,7 +122,6 @@ def get_sort_key(sorting) -> list: table_format = args.format.strip().lower() if table_format in TABLE_FORMATS: TABLE_FORMAT = table_format - # Headers used in our result display table HEADERS = { "hostname": "Hostname", @@ -125,18 +130,18 @@ def get_sort_key(sorting) -> list: "plat": "Platform", "osver": "Version" } - +hosts = Hosts( + client_id=CLIENT_ID, + client_secret=CLIENT_SECRET, + debug=args.debug +) # Connect to the Discover API -discover = Discover(client_id=CLIENT_ID, - client_secret=CLIENT_SECRET, - base_url=BASE_URL - ) - +discover = Discover(auth_object=hosts) # Empty list to hold our results identified = [] # Query for a complete list of discovered hosts. Maxes out at 100. host_lookup = discover.query_hosts() -if host_lookup["status_code"] == 200: +if host_lookup.get("status_code") == 200: identified_hosts = host_lookup["body"]["resources"] if not identified_hosts: # No hosts returned for this search @@ -144,14 +149,15 @@ def get_sort_key(sorting) -> list: else: # Retrieve all details for all discovered hosts host_detail = discover.get_hosts(ids=identified_hosts)["body"]["resources"] - # Add each hosts relevant detail to our `identified` list so we can display it + # Add each host's relevant detail to our `identified` list so we can display it for host in host_detail: - found = {} - found["hostname"] = host.get("hostname", "Not identified") - found["current_local"] = host.get("current_local_ip", "Unknown") - found["current_external"] = host.get("external_ip", "Unknown") - found["plat"] = host.get("platform_name", "Unknown") - found["osver"] = host.get("os_version", "Unknown") + found = { + "hostname": host.get("hostname", "Not identified"), + "current_local": host.get("current_local_ip", "Unknown"), + "current_external": host.get("external_ip", "Unknown"), + "plat": host.get("platform_name", "Unknown"), + "osver": host.get("os_version", "Unknown") + } # Append this result to our display list identified.append(found) # All findings have been tabulated, show the results From 199c7598de495c42aa6b5f7eabbf625bc0278fd0 Mon Sep 17 00:00:00 2001 From: David Michael Berry <168669748+David-M-Berry@users.noreply.github.com> Date: Mon, 1 Jul 2024 20:38:05 -0600 Subject: [PATCH 10/30] Update find_hosts_by_cve.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added -i --include option to only include output from specific columns. Added `seen` set in the `get_match_details` function to track unique entries based on "hostname" and "local_ip". Modified the loop to skip duplicate entries by checking the seen set. Added a `—deduplicate` (-d) option. When this argument is provided, the script will remove duplicates based on hostname and local_ip. --- samples/spotlight/find_hosts_by_cve.py | 45 ++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/samples/spotlight/find_hosts_by_cve.py b/samples/spotlight/find_hosts_by_cve.py index 6d9d5f016..6617eb12a 100644 --- a/samples/spotlight/find_hosts_by_cve.py +++ b/samples/spotlight/find_hosts_by_cve.py @@ -1,5 +1,4 @@ -"""Retrieve hosts by CVE vulnerability. - +""" ______ __ _______ __ __ __ | |.----.-----.--.--.--.--| | __| |_.----.|__| |--.-----. | ---|| _| _ | | | | _ |__ | _| _|| | <| -__| @@ -23,6 +22,7 @@ """ from argparse import ArgumentParser, RawTextHelpFormatter import json +import sys try: from tabulate import tabulate except ImportError as no_tabulate: @@ -181,6 +181,15 @@ def parse_command_line() -> object: 'hostname, local_ip, os_version, service_provider, remediation)', required=False ) + parser.add_argument( + '-i', + '--include', + help='List of columns to include in the display, comma-separated.\n' + 'If specified, only these columns will be displayed.\n' + '(cve, score, severity, cve_description, created_on, updated_on,\n' + 'hostname, local_ip, os_version, service_provider, remediation)', + required=False + ) parser.add_argument( '-f', '--format', @@ -212,6 +221,13 @@ def parse_command_line() -> object: action="store_false", required=False ) + parser.add_argument( + '-d', + '--deduplicate', + help='Remove duplicate entries based on hostname and local_ip.', + action="store_true", + required=False + ) return parser.parse_args() @@ -219,7 +235,7 @@ def parse_command_line() -> object: def inform(msg: str): """Provide informational updates to the user as the program progresses.""" if PROGRESS: - print(" %-80s" % msg, end="\r", flush=True) # pylint: disable=C0209 + print(f"\r{' ' * 80}\r{msg}", end='', flush=True) def get_spotlight_matches(cves: list) -> list: @@ -237,6 +253,9 @@ def get_spotlight_matches(cves: list) -> list: def remove_exclusions(resultset: dict) -> dict: """Remove requested columns from the table display.""" + if INCLUDE: + return [{key: result[key] for key in INCLUDE} for result in resultset] + for result in resultset: for exclusion in EXCLUDE: del result[exclusion] @@ -247,6 +266,7 @@ def remove_exclusions(resultset: dict) -> dict: def get_match_details(match_list: list) -> list: """Retrieve details for individual matches to the specified CVEs.""" returned = [] + seen = set() inform("[ Retrieve matches ]") match_results = spotlight.get_vulnerabilities(ids=match_list) if match_results["status_code"] >= 400: @@ -254,8 +274,15 @@ def get_match_details(match_list: list) -> list: for result in match_results["body"]["resources"]: row = SpotlightCVEMatch(result).to_object() - inform(f"[ {row['cve']} ] Found {row['hostname']}/{row['local_ip']}") - returned.append(row) + if args.deduplicate: + unique_id = (row['hostname'], row['local_ip']) + if unique_id not in seen: + seen.add(unique_id) + inform(f"[ {row['cve']} ] Found {row['hostname']}/{row['local_ip']}") + returned.append(row) + else: + inform(f"[ {row['cve']} ] Found {row['hostname']}/{row['local_ip']}") + returned.append(row) reversing = False if SORT_REVERSE: @@ -292,6 +319,10 @@ def get_match_details(match_list: list) -> list: if args.exclude: EXCLUDE = args.exclude.split(",") +INCLUDE = [] +if args.include: + INCLUDE = args.include.split(",") + TABLE_FORMAT = "fancy_grid" if args.format: table_format = args.format.strip().lower() @@ -338,8 +369,10 @@ def get_match_details(match_list: list) -> list: inform("[ Process startup ]") details = get_match_details(get_spotlight_matches(CVE_LIST)) +# Clear the progress message +print("\r" + " " * 80 + "\r", end='', flush=True) + # Display results -inform("[ Results display ]") print( tabulate( tabular_data=remove_exclusions(details), From 54b176904d7c385764d5e19fb00999c0ae38aaa5 Mon Sep 17 00:00:00 2001 From: exk200006 Date: Tue, 2 Jul 2024 12:51:13 -0500 Subject: [PATCH 11/30] Update README.md to list debugging arguments --- samples/discover/README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/samples/discover/README.md b/samples/discover/README.md index 3c6f5df71..bbaf59e85 100644 --- a/samples/discover/README.md +++ b/samples/discover/README.md @@ -55,6 +55,12 @@ python3 list_discovered_hosts.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET - python3 list_discovered_hosts.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -f simple ``` +> Activate API debugging with the `-d` argument. + +```shell +python3 list_discovered_hosts.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -d +``` + ##### Available table formats Tabular results may be formatted using any of the format options listed below. @@ -87,7 +93,7 @@ Command-line help is available via the `-h` argument. ```shell python3 list_discovered_hosts.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -h -usage: list_discovered_hosts.py [-h] [-k CLIENT_ID] [-s CLIENT_SECRET] [-b BASE_URL] [-r] [-f FORMAT] +usage: list_discovered_hosts.py [-h] [-k CLIENT_ID] [-s CLIENT_SECRET] [-b BASE_URL] [-r] [-d] [-f FORMAT] CrowdStrike Falcon Discover simple example. @@ -115,7 +121,7 @@ CrowdStrike Falcon Discover simple example. Creation date: 02.08.2022 - jshcodes@CrowdStrike -optional arguments: +options: -h, --help show this help message and exit -k CLIENT_ID, --client_id CLIENT_ID CrowdStrike Falcon API key ID. @@ -126,6 +132,7 @@ optional arguments: -b BASE_URL, --base_url BASE_URL CrowdStrike API region (us1, us2, eu1, usgov1) NOT required unless you are using `usgov1`. -r, --reverse Reverse sort (defaults to ASC) + -d, --debug Enable API debugging -f FORMAT, --format FORMAT Table format to use for display. (plain, simple, github, grid, fancy_grid, pipe, orgtbl, jira, presto, From 2069d3d00ba538bc7a0ef31cba9a69daca4fcf72 Mon Sep 17 00:00:00 2001 From: alhumaw Date: Fri, 28 Jun 2024 15:12:46 -0700 Subject: [PATCH 12/30] add kube_map sample --- samples/containers/README.md | 105 +++++++ samples/containers/kube_map.py | 529 +++++++++++++++++++++++++++++++++ 2 files changed, 634 insertions(+) create mode 100644 samples/containers/README.md create mode 100644 samples/containers/kube_map.py diff --git a/samples/containers/README.md b/samples/containers/README.md new file mode 100644 index 000000000..8b8f1cd5a --- /dev/null +++ b/samples/containers/README.md @@ -0,0 +1,105 @@ +![CrowdStrike Falcon](https://raw.githubusercontent.com/CrowdStrike/falconpy/main/docs/asset/cs-logo.png) +[![CrowdStrike Subreddit](https://img.shields.io/badge/-r%2Fcrowdstrike-white?logo=reddit&labelColor=gray&link=https%3A%2F%2Freddit.com%2Fr%2Fcrowdstrike)](https://reddit.com/r/crowdstrike) + +# Container examples +The examples in this folder focus on leveraging CrowdStrike's Container APIs to discover and manage your container assets. +- [kube_map - Discover your Kubernetes Attack Surface](#Discover-your-Kubernetes-Attack-Surface) + +## Discover your Kubernetes Attack Surface +Discovers Kubernetes assets that are monitored by the Falcon Sensor (clusters, nodes, pods, and containers). + +> [!IMPORTANT] +> Installing the __Kubernetes Protection Agent (KPA)__ on your clusters will result in the most accurate information. + + +### Running the program +In order to run this demonstration, you will need access to CrowdStrike API keys with the following scopes: +| Service Collection | Scope | +| :---- | :---- | +| Kubernetes Protection | __READ__| + +### Execution syntax +This example accepts the following input parameters. +| Parameter | Purpose | +| :--- | :--- | +| `-d`, `--debug` | Enable API debugging. | +| `-c`, `--cluster` | Display all clusters and the number of attached nodes. | +| `-n`, `--node` | Display all nodes including the number of attached, active pods. | +| `-nn`, `--node_name` | Displays pods connected to a specific node. | +| `-t`, `--thread` | Enables asynchronous API calls for faster returns. | +| `-k`, `--key` | Your CrowdStrike Falcon API Client ID | +| `-s`, `--secret` | Your CrowdStrike Falcon API Client Secret | + +Displays the number of clusters, nodes, pods, and containers detected by the Falcon Sensor. +```shell +python3 kube_map.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET +``` + +Displays a table of cluster information. +```shell +python3 kube_map.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -c +``` + +Displays a table of node information. +```shell +python3 kube_map.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -n +``` + +Displays a table of pods based on it's parent node name using the optional threading feature. +```shell +python3 kube_map.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -nn "node_name" -t +``` + +Displays API debug logging. +```shell +python3 kube_map.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -d +``` + +#### Command-line help +Command-line help is available using the `-h` or `--help` parameters. + +```shell +% python3 kube_map.py -h +usage: kube_map.py [-h] -k CLIENT_ID -s CLIENT_SECRET [-d] [-c] [-n] [-nn NODE_NAME] [-t] + + _______ __ _______ __ __ __ +| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----. +|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__| +|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____| +|: 1 | |: 1 | +|::.. . | |::.. . | FalconPy +`-------' `-------' + + _ ___ _ ____ _____ + | |/ / | | | __ )| ____| + | ' /| | | | _ \| _| + | . \| |_| | |_) | |___ + __ __|_|\_\\___/|____/|_____|__ ____ + | \/ | / \ | _ \| _ \| ____| _ \ + | |\/| | / _ \ | |_) | |_) | _| | |_) | + | | | |/ ___ \| __/| __/| |___| _ < + |_| |_/_/ \_\_| |_| |_____|_| \_\ + +This sample utilizes the Kubernetes Protection service collection to map out +your kubernetes assets. Kubernetes assets are found via the Falcon Sensor. + +Creation date: 06.26.23 - alhumaw + +options: + -h, --help show this help message and exit + -d, --debug Enable API debugging + -c, --cluster Display clusters and it's nodes + -n, --node Display nodes and it's pods + -nn NODE_NAME, --node_name NODE_NAME + Display pods connected to a specific node + -t, --thread Enables asynchronous API calls for faster returns + +required arguments: + -k CLIENT_ID, --client_id CLIENT_ID + CrowdStrike API client ID + -s CLIENT_SECRET, --client_secret CLIENT_SECRET + CrowdStrike API client secret +``` + +### Example source code +The source code for this example can be found [here](kube_map.py). diff --git a/samples/containers/kube_map.py b/samples/containers/kube_map.py new file mode 100644 index 000000000..95de90260 --- /dev/null +++ b/samples/containers/kube_map.py @@ -0,0 +1,529 @@ +r""" + + _______ __ _______ __ __ __ +| _ .----.-----.--.--.--.--| | _ | |_.----|__| |--.-----. +|. 1___| _| _ | | | | _ | 1___| _| _| | <| -__| +|. |___|__| |_____|________|_____|____ |____|__| |__|__|__|_____| +|: 1 | |: 1 | +|::.. . | |::.. . | FalconPy +`-------' `-------' + + _ ___ _ ____ _____ + | |/ / | | | __ )| ____| + | ' /| | | | _ \| _| + | . \| |_| | |_) | |___ + __ __|_|\_\\___/|____/|_____|__ ____ + | \/ | / \ | _ \| _ \| ____| _ \ + | |\/| | / _ \ | |_) | |_) | _| | |_) | + | | | |/ ___ \| __/| __/| |___| _ < + |_| |_/_/ \_\_| |_| |_____|_| \_\ + + +This sample utilizes the KubernetesProtection sample to map out +your kubenetes assets. Kubernetes assets are found via the Falcon Sensor. + +Creation date: 06.26.23 - alhumaw + + +""" + + +import logging +from argparse import ArgumentParser, RawTextHelpFormatter, Namespace +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from operator import itemgetter +try: + from termcolor import colored # type: ignore +except ImportError as no_termcolor: + raise SystemExit("The termcolor library must be installed.\n" + "Install it with `python3 -m pip install termcolor" + ) from no_termcolor +try: + from tabulate import tabulate # type: ignore +except ImportError as no_tabulate: + raise SystemExit("The tabulate library must be installed.\n" + "Install it with `python3 -m pip install tabulate`." + ) from no_tabulate +try: + from falconpy import KubernetesProtection, APIError +except ImportError as no_falconpy: + raise SystemExit("The CrowdStrike FalconPy library must be installed.\n" + "Install it with `python3 -m pip install crowdstrike-falconpy`." + ) from no_falconpy + +KUBE = r""" + + .... + .-========-. + .-================-. + .:-========================-:. + .:-================================-:. + .:-===================+====================-:. + .:-======================: .-======================-:. + .-=========================== ===========================-. + :==============================+: :+==============================: + =================================: :================================= + -============================+==-- --==+============================- + ==========================-:. .:-========================== + :==========: .-========:. .:========-. :==========: + ============. .-=+=- .::--: :--:.. .-=+=-. .============ + :==============-: :-======: :====+=-. :-==============: + ================== :=========: :=========: =================- + .=================- .:======+. .+======:. -=================. + =================- : .-=+==. .==+=-. : -================= + :================= ===: .:. .:. :=== =================: + ================+: -=====- -=====: :+================ + .================= ========- .. .-======== =================. + -================= .=========: :====: :=+=======. =================- + ================== :===-::.. ====== .::-==+: ================== + -================== .-==-. ==================- + =============----: . .. ::---============= + :==========- ::--===+++- -+=+===---: -==========: + ============:::-==+==. :+======== .. ========+: .==+==-:::============ + -===================== :======= ==== =======: =====================- + -=====================. .=====. ====== .=====. .=====================- + .====================+- .:=: ======== :=-. -+====================. + :=====================: .==========. :=====================: + -=====================: .:::--:::. :=====================- + :======================= -======================: + -===================+- --:.. ..:-- -+===================- + .=================+: ======++====++====== :==================. + :===============. -====================- .===============: + -=============. :+=====================: .=============- + :============+=+========================+=+============: + .-==================================================-. + .================================================. + :============================================: + .==========================================. + .::::::::::::::::::::::::::::::::::::. +""" + + +def parse_command_line() -> Namespace: + """Parse any provided command line arguments and return the namespace.""" + parser = ArgumentParser(description=__doc__, + formatter_class=RawTextHelpFormatter + ) + require = parser.add_argument_group("required arguments") + require.add_argument("-k", "--client_id", + required=True, + help="CrowdStrike API client ID" + ) + require.add_argument("-s", "--client_secret", + required=True, + help="CrowdStrike API client secret" + ) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) + exclusive_group = parser.add_mutually_exclusive_group() + exclusive_group.add_argument("-c", "--cluster", + help="Display clusters and it's nodes", + action="store_true", + default=False + ) + exclusive_group.add_argument("-n", "--node", + help="Display nodes and it's pods", + action="store_true", + default=False + ) + exclusive_group.add_argument("-nn", "--node_name", + help="Display pods connected to a specific node" + ) + parser.add_argument("-t", "--thread", + help="Enables asynchronous API calls for faster returns", + action="store_true", + default=False + ) + + parsed = parser.parse_args() + + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + + return parsed + + +@dataclass +class Cluster: + """Kubernetes cluster dataclass.""" + + cluster_id: str + cluster_name: str + agent: str + cloud_type: str + node_count: int + + +@dataclass +class Node: + """Kubernetes nodes dataclass.""" + + node_name: str + parent_cluster_name: str + ip: str + architecture: str + cpu: str + storage: str + pod_count: int + + +@dataclass +class Pod: + """Kubernetes pods dataclass.""" + + pod_id: str + pod_name: str + parent_node_name: str + containers: list + namespace: str + container_count: int + + +class KubernetesEnvironment: + """Kubernetes comprehensive environment.""" + + def __init__(self): + self.clusters: list[Cluster] = [] + self.nodes: list[Node] = [] + self.pods: list[Pod] = [] + + def add_clusters(self, cluster: Cluster) -> None: + """Append cluster to list of clusters.""" + self.clusters = cluster + + def add_nodes(self, node: Node) -> None: + """Append node to list of nodes.""" + self.nodes = node + + def add_pods(self, pod: Pod) -> None: + """Append pod to list of pods.""" + self.pods = pod + + +def generate_clusters(falcon: KubernetesProtection, thread: bool) -> list: + """Retrieve and return a list of clusters.""" + print("Locating Clusters...") + if thread: + print("Threading....") + total = falcon.ReadClusterCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadClusterCombined', total) + else: + all_resp = normal_response(falcon, "ReadClusterCombined") + clusters = [] + for batch in all_resp: + new_cluster = Cluster( + cluster_id=batch['cluster_id'], + cluster_name=batch['cluster_name'], + agent=batch['agent_status'], + cloud_type=batch['cloud_name'], + node_count=batch['node_count'] + ) + clusters.append(new_cluster) + + return clusters + + +def generate_nodes(falcon: KubernetesProtection, thread: bool) -> list: + """Retrieve and return a list of nodes.""" + print("Discovering Nodes...") + if thread: + print("Threading....") + total = falcon.ReadNodeCombined()['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, 'ReadNodeCombined', total) + else: + all_resp = normal_response(falcon, "ReadNodeCombined") + nodes = [] + real_cpu = "" + for batch in all_resp: + new_node = Node( + node_name=batch['node_name'], + parent_cluster_name=batch['cluster_name'], + ip=batch['ipv4'], + architecture=batch['architecture'], + cpu=batch['cpu'], + storage=batch['storage'], + pod_count=(batch['pod_count']) + ) + if len(new_node.cpu) > 0: + cpu_set = new_node.cpu.split("/") + real_cpu = f"{cpu_set[1]}/{cpu_set[0]}" + new_node.cpu = real_cpu + nodes.append(new_node) + + return nodes + + +def generate_pods(falcon: KubernetesProtection, thread: bool) -> list: + """Retrieve and return a list of pods.""" + print("Finding Pods...") + if thread: + total = falcon.ReadPodCombined()['body']['meta']['pagination']['total'] + print("Threading....") + all_resp = concurent_response(falcon, 'ReadPodCombined', total, filt="container_count:>'0'") + else: + all_resp = normal_response(falcon, "ReadPodCombined") + pods = [] + for batch in all_resp: + new_pod = Pod( + pod_id=batch['pod_id'], + pod_name=batch['pod_name'], + parent_node_name=batch['node_name'], + containers=[], + namespace=batch['namespace'], + container_count=batch['container_count'] + ) + container_list = batch['containers'] + if container_list: + for container in container_list: + new_pod.containers.append(container['id']) + pods.append(new_pod) + + return pods + + +def find_active_pods(pods: list[Pod], containers: dict) -> list: + """Cross-references running containers to find active pods.""" + active_pods = [] + for pod in pods: + has_match = False + for container in pod.containers: + if container in containers: + has_match = True + break + if has_match: + active_pods.append(pod) + + return active_pods + + +def generate_containers(falcon: KubernetesProtection, thread: bool) -> dict: + """Retrieve and return a list of RUNNING containers.""" + print("Tracking Down Containers...") + running_containers = {} + filt = "running_status:'true'" + if thread: + total = falcon.ReadContainerCombined(filter=filt)['body']['meta']['pagination']['total'] + all_resp = concurent_response(falcon, "ReadContainerCombined", total, filt) + else: + all_resp = normal_response(falcon, "ReadContainerCombined", filt) + for container in all_resp: + container_id = container.get('container_id') + container_name = container.get('container_name') + running_containers[container_id] = container_name + + return running_containers + + +def response_processing(falcon: KubernetesProtection, + endpoint: str, filt: str, + limit: int, offset: int) -> list: + """Dynamic API caller for multi-proccessing.""" + method = getattr(falcon, endpoint, None) + if method is None: + raise AttributeError(f"API object has no method named '{endpoint}'") + if filt: + resp = method(filter=filt if filt else None, + limit=limit, + offset=offset)['body']['resources'] + else: + resp = method(limit=limit, offset=offset)['body']['resources'] + + return resp + + +def normal_response(falcon: KubernetesProtection, endpoint: str, filt=None) -> list: + """Caller to handle pagination.""" + limit = 200 + all_resp = [] + total = 1 + offset = 0 + method = getattr(falcon, endpoint, None) + if method is None: + raise AttributeError(f"API object has no method named '{endpoint}'") + + while len(all_resp) < total: + resp = method(limit=limit, offset=offset, filter=filt) + if resp['status_code'] == 200: + total = resp['body']['meta']['pagination']['total'] + offset += 200 + all_resp.extend(resp['body']['resources']) + + return all_resp + + +def concurent_response(falcon: KubernetesProtection, endpoint: str, total: str, filt=None) -> list: + """Utilizes concurrent futures to asynchronously handle paginated API calls at once.""" + chunk_size = 200 + # Determine how many workers are needed to handle all chunks at once + workers = int(total / chunk_size) + 1 + # Create a list of chunks of the total, incremented by the max limit (chunk_size) + # (range(0,total), each list index is incremented by 200) + offsets = [i * chunk_size for i in range(workers)] + all_resp = [] + if workers > 10: + # Splits the list into batches of 10 to distribute load + batches = [offsets[x: x + 10] for x in range(0, len(offsets), 10)] + for batch in batches: + with ThreadPoolExecutor(max_workers=workers) as e: + future = { + e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) + for offset in batch + } + for f in future: + all_resp.extend(f.result()) + else: + with ThreadPoolExecutor(max_workers=workers) as e: + future = { + e.submit(response_processing, falcon, endpoint, filt, chunk_size, offset) + for offset in offsets + } + for f in future: + all_resp.extend(f.result()) + + return all_resp + + +def aggregate_kube(clusters: list, nodes: list, pods=None) -> KubernetesEnvironment: + """Organizes clusters, nodes, and pods into a data structure.""" + kube = KubernetesEnvironment() + kube.add_clusters(clusters) + kube.add_nodes(nodes) + if pods: + kube.add_pods(pods) + + return kube + + +def connect_api(key: str, secret: str, debug: bool) -> KubernetesProtection: + """Connect and return an instance of the Uber class.""" + try: + if debug: + logging.basicConfig(level=logging.DEBUG) + return KubernetesProtection(client_id=key, client_secret=secret, debug=debug) + except APIError as e: + print(f"Failed to connect to API: {e}") + return e + + +def form_relations(kube: KubernetesEnvironment, args: Namespace) -> dict: + """Return a tabulated kubernetes environment.""" + if args.cluster: + cluster_data = [] + for cluster in kube.clusters: + for node in kube.nodes: + if node.parent_cluster_name == cluster.cluster_name: + cluster.node_count += 1 + cluster_data.append([ + cluster.cluster_name if cluster.cluster_name else cluster.cluster_id, + cluster.agent, + cluster.cloud_type, + cluster.node_count + ]) + headers = ['Cluster Name || ID', 'KPA Status', 'Cloud Type', 'Node Count'] + # Sort by node count + cluster_data.sort(key=itemgetter(3)) + + return tabulate(cluster_data, headers, tablefmt='grid') + + if args.node: + node_data = [] + for node in kube.nodes: + for pod in kube.pods: + if pod.parent_node_name == node.node_name: + node.pod_count += 1 + node_data.append([ + node.node_name, + node.ip, + node.architecture, + node.cpu, + node.storage, + node.pod_count + ]) + headers = ['Node Name', 'IP', 'Arch', 'CPU', 'Storage', 'Active\nPod Count'] + # Sort by pod count + node_data.sort(key=itemgetter(5)) + + return tabulate(node_data, headers, tablefmt='grid') + + pod_data = [] + for pod in kube.pods: + if pod.parent_node_name == args.node_name: + pod_data.append([ + pod.pod_id, + pod.pod_name, + pod.namespace, + pod.container_count + ]) + headers = ['Pod ID', 'Pod Name', 'Namespace', 'Container Count'] + if len(pod_data) < 1: + return f"Found 0 pods related to '{args.node_name}'" + + return tabulate(pod_data, headers, tablefmt='grid') + + +def find_asset_count(falcon: KubernetesProtection) -> dict: + """Find and output the initial count of all assets.""" + env = {} + containers = colored(falcon.ReadContainerCount( + filter="running_status: 'true'")['body']['resources'][0]['count'], 'red') + + pods = colored(falcon.ReadPodCount()['body']['resources'][0]['count'], 'red') + nodes = colored(falcon.ReadNodeCount()['body']['resources'][0]['count'], 'red') + clusters = colored(falcon.ReadClusterCount()['body']['resources'][0]['count'], 'red') + env = { + 'Asset': [colored('Clusters', "yellow"), + colored('Nodes', "blue"), + colored('Pods', 'green'), + 'Containers'], + 'Count': [clusters, nodes, pods, containers] + } + + return (tabulate(env, headers="keys", tablefmt='heavy_grid', colalign=("left", "left"))) + + +def print_kube(args: Namespace, falcon: KubernetesProtection) -> None: + """Print the kubernetes environment to the terminal.""" + if args.cluster or args.node or args.node_name: + clusters = generate_clusters(falcon, args.thread) + nodes = generate_nodes(falcon, args.thread) + + if args.node or args.node_name: + # Compare all pods against active containers to find active pods + pods = generate_pods(falcon, args.thread) + containers = generate_containers(falcon, args.thread) + active_pods = find_active_pods(pods, containers) + # Creates kube instance, prints out table + kube = aggregate_kube(clusters, nodes, active_pods) + kube_table = form_relations(kube, args) + print(kube_table) + + else: + kube = aggregate_kube(clusters, nodes) + kube_table = form_relations(kube, args) + print(kube_table) + + else: + print(KUBE) + print(find_asset_count(falcon)) + cluster_info = f"Use {colored('-c', 'yellow')} to print cluster information" + node_info = f"Use {colored('-n', 'blue')} to print node information" + pod_info = (f"Use {colored('-nn', 'green')}" + " with a node name to print\nactive pods linked to that node") + hint = (f"HINT: use {colored('-t', 'magenta')}" + " along with any command to\nimprove speed using asynchronous processing") + print(tabulate([[cluster_info], [node_info], [pod_info], [hint]], tablefmt="mixed_grid")) + + +def main(): + """Start Main Execution Routine.""" + args = parse_command_line() + falcon = connect_api(key=args.client_id, secret=args.client_secret, debug=args.debug) + print_kube(args, falcon) + + +if __name__ == "__main__": + main() From 148b0d64fcf29ce62b034858cbca2e7a901bca33 Mon Sep 17 00:00:00 2001 From: okewoma Date: Wed, 10 Jul 2024 10:25:41 -0700 Subject: [PATCH 13/30] Added debugging and baseurl funtionality, Related to #1181 --- .../token_authentication_example.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/samples/authentication/token_authentication_example.py b/samples/authentication/token_authentication_example.py index 1a3c450ec..fcd1eb787 100644 --- a/samples/authentication/token_authentication_example.py +++ b/samples/authentication/token_authentication_example.py @@ -34,9 +34,11 @@ This sample should run using any version of FalconPy and requires the colorama and click libraries. """ +import logging import os import click import colorama +from argparse import ArgumentParser, RawTextHelpFormatter, Namespace from falconpy import ( CloudConnectAWS, Detects, @@ -54,6 +56,20 @@ BOLD = colorama.Style.BRIGHT ENDMARK = colorama.Style.RESET_ALL +def consume_arguments() -> Namespace: + parser = ArgumentParser(description=__doc__, fromatter_class=RawTextHelpFormatter) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) + parser.add_argument("-b", "--base-url", + dest="base_url", + help="CrowdStrike cloud region. (auto or usgov1, Default: auto)", + required=False, + default="auto" + ) + return parser.parse_args # ### BEGIN token simulation def get_token(): From 0e0b4891726b0f412c7e5ef88de63e4ee89d8853 Mon Sep 17 00:00:00 2001 From: okewoma <78776399+okewoma@users.noreply.github.com> Date: Tue, 2 Jul 2024 10:13:10 -0700 Subject: [PATCH 14/30] Update AUTHORS.md --- AUTHORS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index 84a4e73f6..ac318d10e 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -97,6 +97,9 @@ This has been a critical element in the development of the FalconPy project. + Nick, `nickforsythbarr` + `nesies` + `David-M-Berry` ++ Oke Okumo, `@okewoma` ++ Alexander Moomaw, `@alhumaw` ++ Esha Kumar, `@exk200006` ## Sponsors From 8a111e3862e6aecae694757c3d0eb3f59882fe2f Mon Sep 17 00:00:00 2001 From: okewoma <78776399+okewoma@users.noreply.github.com> Date: Thu, 11 Jul 2024 19:48:15 -0700 Subject: [PATCH 15/30] Update README.md for sensor_download.py --- samples/sensor_download/README.md | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/samples/sensor_download/README.md b/samples/sensor_download/README.md index 18846bb74..d2409fce2 100644 --- a/samples/sensor_download/README.md +++ b/samples/sensor_download/README.md @@ -41,7 +41,9 @@ This program accepts the following command-line arguments. | `-o` _OS_ | `--os` _OS_ | Sensor operating system | | `-v` _OSVER_ | `--osver` _OSVER_ | Sensor operating system version | | `-f` _FILENAME_ | `--filename` _FILENAME_ | Name to use for downloaded file | -| `-t` _TABLE_FORMAT_ | `--table_format` _TABLE_FORMAT_ | Table format to use for display.
  • plain
  • simple
  • github
  • grid
  • fancy_grid
  • pipe
  • orgtbl
  • jira
  • presto
  • pretty
  • psql
  • rst
  • mediawiki
  • moinmoin
  • youtrack
  • html
  • unsafehtml
  • latext
  • latex_raw
  • latex_booktabs
  • latex_longtable
  • textile
  • tsv
| +| `-t` _TABLE_FORMAT_ | `--table_format` _TABLE_FORMAT_ | Table format to use for display. +|`-debug`|`--debug`|`Enable API debugging`| +|`-b`|`--base-url`|`GovCloud access to Crowdstrike API`|
  • plain
  • simple
  • github
  • grid
  • fancy_grid
  • pipe
  • orgtbl
  • jira
  • presto
  • pretty
  • psql
  • rst
  • mediawiki
  • moinmoin
  • youtrack
  • html
  • unsafehtml
  • latext
  • latex_raw
  • latex_booktabs
  • latex_longtable
  • textile
  • tsv
| #### Basic usage The only required command line arguments are `-k` (CrowdStrike Falcon API Client ID) and `-s` (CrowdStrike Falcon API Client Secret). @@ -89,7 +91,18 @@ Filters described above are applied to select the appropriate version to downloa ```shell python3 download_sensor.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -o centos -v 7 -d ``` +##### Activating Debugging +This exmaple shows how you can activate debugging functionality when you run download_senor.py. +```shell +python3 download_sensor.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -debug +``` +##### Allowing Access to GovCloud Users +This exmaple shows how you GovCloud user can access sensor_download.py. + +```shell +python3 download_sensor.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -b +``` ##### Specifying `N-1` or `N-2` versions. You can specify the previous, or 2nd previous version to download by leveraging the `-n` argument. @@ -138,11 +151,13 @@ optional arguments: CrowdStrike API Secret -a, --all Show all columns / Download all versions -d, --download Shortcut for '--command download' + -b, --base-url Allows access to usgov1 -n NMINUS, --nminus NMINUS Download previous version (n-1, n-2, 0 = current, 2 = n-2) -c COMMAND, --command COMMAND Command to perform. (list or download, defaults to list) -o OS, --os OS Sensor operating system + -debug, --debug Command to activate debugging -v OSVER, --osver OSVER Sensor operating system version -f FILENAME, --filename FILENAME @@ -152,7 +167,8 @@ optional arguments: (plain, simple, github, grid, fancy_grid, pipe, orgtbl, jira, presto, pretty, psql, rst, mediawiki, moinmoin, youtrack, html, unsafehtml, latext, latex_raw, latex_booktabs, latex_longtable, textile, tsv) + ``` ### Example source code -Source code for this example can be found [here](download_sensor.py). \ No newline at end of file +Source code for this example can be found [here](download_sensor.py). From 0cceb172775bd3dba851acbdb1d00e0677d7e5d0 Mon Sep 17 00:00:00 2001 From: okewoma Date: Thu, 11 Jul 2024 20:21:36 -0700 Subject: [PATCH 16/30] Added debugging, minor linting and also fixed up syntax. --- .../cspm_registration/get_cspm_policies.py | 61 +++++++++++++------ 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/samples/cspm_registration/get_cspm_policies.py b/samples/cspm_registration/get_cspm_policies.py index 7efb1e73f..3d071ab52 100644 --- a/samples/cspm_registration/get_cspm_policies.py +++ b/samples/cspm_registration/get_cspm_policies.py @@ -39,7 +39,7 @@ import os import sys import logging -from argparse import ArgumentParser, RawTextHelpFormatter +from argparse import ArgumentParser, RawTextHelpFormatter, Namespace from tabulate import tabulate try: from falconpy import CSPMRegistration @@ -49,17 +49,42 @@ ) from no_falconpy +def consume_arguments() -> Namespace: # Capture command line arguments -parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) -parser.add_argument("-f", "--falcon_client_id", - help="Falcon Client ID", default=None, required=False) -parser.add_argument("-s", "--falcon_client_secret", - help="Falcon Client Secret", default=None, required=False) -parser.add_argument("-o", "--output_file", - help="Policy report output file (CSV format)", required=False) -parser.add_argument( - "-c", "--cloud", help="Cloud provider (aws, azure, gcp)", required=False) -args = parser.parse_args() + parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) + parser.add_argument("-f", "--falcon_client_id", + help="Falcon Client ID", + default=None, + required=False) + parser.add_argument("-s", "--falcon_client_secret", + help="Falcon Client Secret", + default=None, + required=False) + parser.add_argument("-o", "--output_file", + help="Policy report output file (CSV format)", + required=False) + parser.add_argument("-c", "--cloud", + help="Cloud provider (aws, azure, gcp)", + required=False) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) + + parsed = parser.parse_args() + return parsed + + +cmd_line = consume_arguments() + +# Activate debugging if requested +if cmd_line.debug: + logging.basicConfig(level=logging.DEBUG) + + + +# pylint: disable=E0606 # Grab our client_id and client_secret or exit CONFIG_FILE = '../config.json' @@ -68,20 +93,22 @@ config = json.loads(file_config.read()) falcon_client_id = config['falcon_client_id'] falcon_client_secret = config['falcon_client_secret'] -elif args.falcon_client_id is not None and args.falcon_client_secret is not None: - falcon_client_id = args.falcon_client_id - falcon_client_secret = args.falcon_client_secret +elif cmd_line.falcon_client_id is not None and cmd_line.falcon_client_secret is not None: + falcon_client_id = cmd_line.falcon_client_id + falcon_client_secret = cmd_line.falcon_client_secret + debug = cmd_line.debug if cmd_line.debug else False # Set debug mode based on argument else: logging.error( " Please specify Falcon API Credentials with config.json or script arguments") sys.exit() -data_file = args.output_file -cloud = args.cloud +data_file = cmd_line.output_file +cloud = cmd_line.cloud # Instantiate CSPM_Registration service class falcon = CSPMRegistration(client_id=falcon_client_id, - client_secret=falcon_client_secret + client_secret=falcon_client_secret, + debug=debug ) From 87936cd3344a951f489a581407d195e30e4badaf Mon Sep 17 00:00:00 2001 From: okewoma <78776399+okewoma@users.noreply.github.com> Date: Thu, 11 Jul 2024 20:20:32 -0700 Subject: [PATCH 17/30] Update README.md for cspm_registration.py --- samples/cspm_registration/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/samples/cspm_registration/README.md b/samples/cspm_registration/README.md index a398816b8..c960ee01d 100644 --- a/samples/cspm_registration/README.md +++ b/samples/cspm_registration/README.md @@ -48,6 +48,10 @@ python3 get_cspm_policies.py -f $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -o fi python3 get_cspm_policies.py -f $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -c aws ``` +```shell +python3 get_cspm_policies.py -f $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -d +``` +> To activate debugging, use the `-d` argument. #### Command-line help Command-line help is available via the `-h` argument. @@ -98,7 +102,8 @@ optional arguments: Policy report output file (CSV format) -c CLOUD, --cloud CLOUD Cloud provider (aws, azure, gcp) + -d, --debug, Activates debugging ``` ### Example source code -The source code for this example can be found [here](get_cspm_policies.py). \ No newline at end of file +The source code for this example can be found [here](get_cspm_policies.py). From 9a7a96b5115eff67ba0cea6cd2f59b2167349bc4 Mon Sep 17 00:00:00 2001 From: okewoma Date: Thu, 11 Jul 2024 20:26:36 -0700 Subject: [PATCH 18/30] requiremets.txt for get_cspm_policies.py and download_sensor.py --- samples/cspm_registration/requirements_get_cspm_policies.txt | 2 ++ samples/sensor_download/requirements_download_sensor.txt | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 samples/cspm_registration/requirements_get_cspm_policies.txt create mode 100644 samples/sensor_download/requirements_download_sensor.txt diff --git a/samples/cspm_registration/requirements_get_cspm_policies.txt b/samples/cspm_registration/requirements_get_cspm_policies.txt new file mode 100644 index 000000000..598e20cb7 --- /dev/null +++ b/samples/cspm_registration/requirements_get_cspm_policies.txt @@ -0,0 +1,2 @@ +crowdstrike-falconpy +tabulate \ No newline at end of file diff --git a/samples/sensor_download/requirements_download_sensor.txt b/samples/sensor_download/requirements_download_sensor.txt new file mode 100644 index 000000000..790cddc1f --- /dev/null +++ b/samples/sensor_download/requirements_download_sensor.txt @@ -0,0 +1,2 @@ +crowdstrike-falconpy +tabulate From 2bd07be32d5a365fbc5a25ae674ea22f95fb1bda Mon Sep 17 00:00:00 2001 From: okewoma Date: Thu, 11 Jul 2024 20:41:41 -0700 Subject: [PATCH 19/30] Added debugging to prevention_policy_hawk.py and included a requirements.txt --- .../prevention_policy_hawk.py | 29 ++++++++++++++----- .../requiremts_prevention_policy_hawk.txt | 4 +++ 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 samples/prevention_policy/requiremts_prevention_policy_hawk.txt diff --git a/samples/prevention_policy/prevention_policy_hawk.py b/samples/prevention_policy/prevention_policy_hawk.py index f7d76aaf8..319b3f234 100644 --- a/samples/prevention_policy/prevention_policy_hawk.py +++ b/samples/prevention_policy/prevention_policy_hawk.py @@ -20,7 +20,7 @@ | | | | | | | | \ / | |\ \ |___| |___| |___| |___| \____/\/\____/ |___| \___\ |0\/0| - \/\/ FalconPy v1.0 + \/\/ FalconPy v1.4.4 \/ Creation date: 2022.02.11 Modification: 2022.05.11 @@ -33,6 +33,7 @@ This solution requires the FalconPy SDK. This project can be accessed here: https://github.com/CrowdStrike/falconpy """ +import logging from argparse import ArgumentParser, RawTextHelpFormatter from enum import Enum from tabulate import tabulate @@ -352,6 +353,11 @@ def consume_command_line(): parser = ArgumentParser(description=shiny_description(__doc__), formatter_class=RawTextHelpFormatter ) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) # Display view = parser.add_argument_group("optional display arguments") view.add_argument("-r", "--show_settings", @@ -414,11 +420,19 @@ def consume_command_line(): ) # Always required req = parser.add_argument_group("required arguments") - req.add_argument("-f", "--falcon_client_id", help="Falcon Client ID", required=True) - req.add_argument("-s", "--falcon_client_secret", help="Falcon Client Secret", required=True) + req.add_argument("-f", "--falcon_client_id", + help="Falcon Client ID", + required=True) + req.add_argument("-s", "--falcon_client_secret", + help="Falcon Client Secret", + required=True) - return parser.parse_args() + parsed = parser.parse_args() + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + + return parsed def do_policy_delete(pol_id: str = None): """Delete the policy using the provided ID.""" @@ -603,10 +617,11 @@ def process_arguments(command, # Retrieve any provided command line arguments args = consume_command_line() - # Authenticate using our provided falcon client_id and client_secret + # Authenticate using our provided falcon client_id and client_secret and debugging if activated falcon_policy = PreventionPolicy(client_id=args.falcon_client_id, - client_secret=args.falcon_client_secret - ) + client_secret=args.falcon_client_secret, + debug=args.debug + ) # Review the provided arguments and then perform the request process_arguments(*determine_arguments(args)) diff --git a/samples/prevention_policy/requiremts_prevention_policy_hawk.txt b/samples/prevention_policy/requiremts_prevention_policy_hawk.txt new file mode 100644 index 000000000..6e933ceee --- /dev/null +++ b/samples/prevention_policy/requiremts_prevention_policy_hawk.txt @@ -0,0 +1,4 @@ +argparse +enum34 +crowdstrike-falconpy +tabulate From 5ea87e685fc5c3c8c3e377f48ebac39927685d37 Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 00:11:32 -0700 Subject: [PATCH 20/30] Added debugging functionality to aws_parameter_store.py alongside a requirement.txt file --- samples/authentication/aws_parameter_store.py | 25 +++++++++++++++---- .../requirements_aws_parameter_store.txt | 2 ++ 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 samples/authentication/requirements_aws_parameter_store.txt diff --git a/samples/authentication/aws_parameter_store.py b/samples/authentication/aws_parameter_store.py index f03882c70..6ba4627cc 100644 --- a/samples/authentication/aws_parameter_store.py +++ b/samples/authentication/aws_parameter_store.py @@ -34,6 +34,7 @@ This application demonstrates storing CrowdStrike API credentials within the AWS Parameter Store service, and retrieving them to access the CrowdStrike API. """ +import logging from argparse import ArgumentParser, RawTextHelpFormatter, Namespace try: import boto3 @@ -64,8 +65,19 @@ def consume_arguments() -> Namespace: default="FALCON_CLIENT_SECRET", dest="client_secret_parameter" ) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) + + parsed = parser.parse_args() + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + + + return parsed - return parser.parse_args() def get_parameter_store_params(cmd_line: Namespace): @@ -101,9 +113,9 @@ def get_parameter_store_params(cmd_line: Namespace): return returned_client_id, returned_client_secret -def perform_simple_demonstration(client_id: str, client_secret: str): +def perform_simple_demonstration(client_id: str, client_secret: str, debug: bool): """Perform a simple API demonstration using the credentials retrieved.""" - falcon = Hosts(client_id=client_id, client_secret=client_secret) + falcon = Hosts(client_id=client_id, client_secret=client_secret, debug=debug) # Retrieve 500 hosts and sort ascending by hostname aid_lookup = falcon.query_devices_by_filter_scroll(sort="hostname.asc", limit=500) if not aid_lookup["status_code"] == 200: @@ -120,6 +132,9 @@ def perform_simple_demonstration(client_id: str, client_secret: str): if __name__ == "__main__": - # Consume our command line, retrieve our credentials from AWS parameter store + # Consume our command line arguments + args = consume_arguments() + # retrieve our credentials from AWS parameter store + client_id, client_secret = get_parameter_store_params(args) # and then execute a simple API demonstration to prove functionality. - perform_simple_demonstration(*get_parameter_store_params(consume_arguments())) + perform_simple_demonstration(client_id, client_secret) diff --git a/samples/authentication/requirements_aws_parameter_store.txt b/samples/authentication/requirements_aws_parameter_store.txt new file mode 100644 index 000000000..b1876066a --- /dev/null +++ b/samples/authentication/requirements_aws_parameter_store.txt @@ -0,0 +1,2 @@ +boto3 +crowdstrike-falconpy \ No newline at end of file From 2ea56c8f0db95f18ec4be2834b5c1929103f6b88 Mon Sep 17 00:00:00 2001 From: okewoma <78776399+okewoma@users.noreply.github.com> Date: Thu, 11 Jul 2024 20:42:39 -0700 Subject: [PATCH 21/30] Updated README.md for prevention_policy_hawk.py --- samples/prevention_policy/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/samples/prevention_policy/README.md b/samples/prevention_policy/README.md index eaa970d48..c7accf5e0 100644 --- a/samples/prevention_policy/README.md +++ b/samples/prevention_policy/README.md @@ -270,7 +270,7 @@ In order to run this demonstration, you will need access to CrowdStrike API keys This example accepts the following input parameters. | Parameter | Purpose | Category | | :--- | :--- | :--- | -| `-h`, `--help` | Show help message and exit | optional | +| `-h`, `--help` | Show help message and exit | optional | | `-r`, `--show_settings` | Display policy settings | display | | `-z`, `--verbose` | Show all settings, including disabled | display | | `-e`, `--enable` | Enable the policy | administration | @@ -517,6 +517,7 @@ optional management arguments: -e, --enable Enable the policy -d, --disable Disable the policy -x, --delete Delete the policy + -debug, --debug Enable API debugging optional update arguments: -i POLICY_ID, --policy_id POLICY_ID From d1dacd2c82cf8c251968027083308904ef6f50e2 Mon Sep 17 00:00:00 2001 From: okewoma <78776399+okewoma@users.noreply.github.com> Date: Mon, 15 Jul 2024 23:56:30 -0700 Subject: [PATCH 22/30] Update README.md --- samples/authentication/README.md | 61 +++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/samples/authentication/README.md b/samples/authentication/README.md index 8f4dbb6f6..b92be4674 100644 --- a/samples/authentication/README.md +++ b/samples/authentication/README.md @@ -7,7 +7,8 @@ The examples in this folder focus on authentication to CrowdStrike's APIs. - [Azure Key Vault Authentication](#azure-key-vault-authentication) - CrowdStrike API authentication leveraging Azure Key Vault for credential storage. - [AES Authentication](#aes-authentication) - Leverage AES/CBC to encrypt credentials for use with authentication to the CrowdStrike API. -- [AES File Crypt](#aes-file-crypt) - Encrypt arbitrary files with AES/CBC. +- [AES File Crypt](#aes-file-crypt) - Encrypt arbitrary files with AES/CBC +- [AWS Parameter Store](#aws-parameter-store) - CrowdStrike API authentication leveraging AWS Parameter Store for credential storage - [Token Authentication](#token-authentication) - Token Authentication is the original solution for authenticating to a Service Class, and is still fully supported. This example demonstrates how to use Token Authentication to interact with multiple Service Classes. ## Azure Key Vault Authentication @@ -458,6 +459,64 @@ file arguments: Source code for this example can be found [here](aes_file_crypt.py). --- +## AWS Parameter store +This application demonstrates storing CrowdStrike API credentials within the AWS Parameter Store service, and retrieving them to access the CrowdStrike API. + +### Running the program +In order to run this demonstration, you will need access to CrowdStrike API keys. You will also need to set your specific AWS location + +#### Command line arguments +This program accepts the following command line arguments. + +| Argument | Long Argument | Description | +| :-- | :-- | :-- | +| `-h` | `--help` | Display command line help and exit | +| `-k` _CLIENT_ID_PARAMETER_ | `--client_id_parameter` _CLIENT_ID_PARAMETER_ | Name of the Key Vault Secrets parameter storing your API client ID | +| `-s` _CLIENT_SECRET_PARAMETER_ | `--client_secret_parameter` _CLIENT_SECRET_PARAMETER_ | Name of the Key Vault Secrets parameter storing your API client secret | +| `-d` | `--debug`| Enables debugging functionality | + +#### Basic usage + +##### Use this command to test out the sample. + +```shell +python3 aws_parameter_store.py -k FALCON_CLIENT_ID -s FALCON_CLIENT_SECRET +``` +##### Use this command to activate debugging. + +```shell +python3 aws_parameter_store.py -k FALCON_CLIENT_ID -s FALCON_CLIENT_SECRET -d +``` +#### Command-line help +Command-line help is available via the `-h` argument. + +```shell +usage: aws_parameter_store.py [-h] [-k] CLIENT_ID [-s] CLIENT_SECRET [-d] DEGUG + + + ___ ____ __ ____ _______. + / \ \ \ / \ / / / | + / ^ \ \ \/ \/ / | (----` + / /_\ \ \ / \ \ + / _____ \ \ /\ / .----) | +/__/ \__\ \__/ \__/ |_______/ + + ____ __ _____ __ + / __ \____ __________ _____ ___ ___ / /____ _____ / ___// /_____ ________ + / /_/ / __ `/ ___/ __ `/ __ `__ \/ _ \/ __/ _ \/ ___/ \__ \/ __/ __ \/ ___/ _ \ + / ____/ /_/ / / / /_/ / / / / / / __/ /_/ __/ / ___/ / /_/ /_/ / / / __/ + /_/ \__,_/_/ \__,_/_/ /_/ /_/\___/\__/\___/_/ /____/\__/\____/_/ \___/ + + +optional arguments: + -h, --help show this help message and exit + -d, --debug enables degugging + +required arguments: + -k CLIENT_ID, --client_id_parameter CLIENT_ID + -s CLIENT_SECRET, --client_secret_parameter CLIENT_SECRET +``` + ## Token Authentication [Token authentication](https://www.falconpy.io/Usage/Authenticating-to-the-API.html#legacy-authentication) (also referred to as _legacy authentication_) is the process of authenticating to a FalconPy Service Class by providing a previously assigned bearer token directly to the [`auth_token`](https://www.falconpy.io/Usage/Basic-Service-Class-usage.html#legacy-authentication) keyword when instantiating the Service Class. This is the original method of authentication provided by Service Classes, and while it is frequently eschewed in preference to [Direct](https://www.falconpy.io/Usage/Authenticating-to-the-API.html#direct-authentication) and [Object](https://www.falconpy.io/Usage/Authenticating-to-the-API.html#object-authentication) [Authentication](https://www.falconpy.io/Usage/Authenticating-to-the-API.html), there are multiple scenarios where it is still the best option for the situation. From 6ce5926bdb209095f0031bf3970aa7b5d040e44a Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 00:17:52 -0700 Subject: [PATCH 23/30] requirements.txt for token_authentication_example --- .../requirements_token_authentication_example.txt | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 samples/authentication/requirements_token_authentication_example.txt diff --git a/samples/authentication/requirements_token_authentication_example.txt b/samples/authentication/requirements_token_authentication_example.txt new file mode 100644 index 000000000..295ff972a --- /dev/null +++ b/samples/authentication/requirements_token_authentication_example.txt @@ -0,0 +1,4 @@ +boto3 +click +colorama +crowdstrike-falconpy \ No newline at end of file From 4cb4aade4b34461761b43212c5905b1d7c3b4f71 Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 00:26:31 -0700 Subject: [PATCH 24/30] Added debugging functionality to token_authentication_example.py and also icluded region selection, related to #1181 --- .../token_authentication_example.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/samples/authentication/token_authentication_example.py b/samples/authentication/token_authentication_example.py index fcd1eb787..95f8a0f49 100644 --- a/samples/authentication/token_authentication_example.py +++ b/samples/authentication/token_authentication_example.py @@ -67,12 +67,16 @@ def consume_arguments() -> Namespace: dest="base_url", help="CrowdStrike cloud region. (auto or usgov1, Default: auto)", required=False, - default="auto" + default="usgov1" ) - return parser.parse_args + parsed = parser.parse_args() + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + + return parsed # ### BEGIN token simulation -def get_token(): +def get_token(debug=False): """ Generate a token to use for authentication. @@ -111,7 +115,8 @@ def get_token(): ) auth = OAuth2( client_id=falcon_client_id, - client_secret=falcon_client_secret + client_secret=falcon_client_secret, + debug=debug ) # Generate a token auth.token() @@ -192,6 +197,10 @@ def passed(svc_class: str): if __name__ == "__main__": + # Parse command-line arguments and retrieve debug mode setting + args = consume_arguments() + # Authenticate using Falcon API OAuth2 with debug mode enabled if specified + get_token(debug=args.debug) # Test each of these classes to confirm cross collection authentication for Service Classes classes_to_test = [CloudConnectAWS, Detects, Hosts, IOC, Incidents, Intel] # Grab a simulated token and execute the test series From d0848a4e2620f1e526f2dc4f0c42f547382661e3 Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 00:28:07 -0700 Subject: [PATCH 25/30] Added debugging functionality to policy_wonk.py and also inluded a requirements.txt --- samples/ioc/create_ioc.py | 15 +++ samples/sensor_update_policies/policy_wonk.py | 101 +++++++++++++----- .../requirements_policy_wonk.txt | 2 + 3 files changed, 93 insertions(+), 25 deletions(-) create mode 100644 samples/sensor_update_policies/requirements_policy_wonk.txt diff --git a/samples/ioc/create_ioc.py b/samples/ioc/create_ioc.py index 3d3b77878..5592b4265 100644 --- a/samples/ioc/create_ioc.py +++ b/samples/ioc/create_ioc.py @@ -24,6 +24,7 @@ "applied_globally": true } """ +import logging import json import os from argparse import ArgumentParser, RawTextHelpFormatter @@ -36,6 +37,20 @@ def consume_command_line(): parser.add_argument("-s", "--falcon_client_secret", help="Falcon API Client Secret", required=True) parser.add_argument("-m", "--method", help="SDK method to use ('service' or 'uber').", required=False, default="service") parser.add_argument("-i", "--indicator", help="Path to the file representing the indicator (JSON format).", default="example_indicator.json", required=False) + parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) + + + parsed = parser.parse_args() + allow = ["indicator", "report", "actor"] + parsed.types = [t for t in parsed.types.split(",") if t in allow] if parsed.types else allow + + if parsed.debug: + logging.basicConfig(level=logging.DEBUG) + return parser.parse_args() diff --git a/samples/sensor_update_policies/policy_wonk.py b/samples/sensor_update_policies/policy_wonk.py index 82b7be152..162be925b 100644 --- a/samples/sensor_update_policies/policy_wonk.py +++ b/samples/sensor_update_policies/policy_wonk.py @@ -9,7 +9,7 @@ __/ | |___/ for Sensor Update Policies - FalconPy v1.0 + FalconPy v1.4.4 Creation date: 05.06.2022 - jshcodes@CrowdStrike @@ -20,6 +20,7 @@ Multiple simultaneous actions may be performed against multiple Sensor Update Policy records using this utility. """ +import logging from argparse import ArgumentParser, RawTextHelpFormatter from tabulate import tabulate try: @@ -107,18 +108,39 @@ def consume_arguments(): """Consume arguments from the command line.""" desc = shiny_help_text(__doc__) parser = ArgumentParser(description=desc, formatter_class=RawTextHelpFormatter) + # Debug + parser.add_argument("-debug", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) # List disp = parser.add_argument_group("list arguments") - disp.add_argument("-l", "--list_all", help="Show all policies (Default action)", required=False, action="store_true") - disp.add_argument("-k", "--kernels", help="Show kernel build compatibility details", required=False, action="store_true") - disp.add_argument("-b", "--builds", help="Show available builds", required=False, action="store_true") - disp.add_argument("-o", "--host_groups", help="Show available host groups", required=False, action="store_true") + disp.add_argument("-l", "--list_all", + help="Show all policies (Default action)", + required=False, + action="store_true") + disp.add_argument("-k", "--kernels", + help="Show kernel build compatibility details", + required=False, + action="store_true") + disp.add_argument("-b", "--builds", + help="Show available builds", + required=False, + action="store_true") + disp.add_argument("-o", "--host_groups", + help="Show available host groups", + required=False, + action="store_true") disp.add_argument("-m", "--maintenance", help="Show maintenance or a specific uninstall token", required=False, action="store_true" ) - disp.add_argument("-v", "--show_members", help="Show policy members in results", required=False, action="store_true") + disp.add_argument("-v", "--show_members", + help="Show policy members in results", + required=False, + action="store_true") disp.add_argument("-z", "--show_groups", help="Show host groups assigned to policies in results", required=False, @@ -126,14 +148,24 @@ def consume_arguments(): ) # Search srch = parser.add_argument_group("search arguments") - srch.add_argument("-q", "--search_string", help="String to match against policy or host group name", required=False) + srch.add_argument("-q", "--search_string", + help="String to match against policy or host group name", + required=False) # Create crt = parser.add_argument_group("create arguments") - crt.add_argument("-c", "--create", help="Create a new policy", required=False, action="store_true") + crt.add_argument("-c", "--create", + help="Create a new policy", + required=False, action="store_true") # Update upd = parser.add_argument_group("update and delete arguments") - upd.add_argument("-d", "--disable", help="Disable the policy", required=False, action="store_true") - upd.add_argument("-e", "--enable", help="Enable the policy", required=False, action="store_true") + upd.add_argument("-d", "--disable", + help="Disable the policy", + required=False, + action="store_true") + upd.add_argument("-e", "--enable", + help="Enable the policy", + required=False, + action="store_true") upd.add_argument("-x", "--disable_uninstall_protection", help="Disable uninstall protection for the policy", required=False, @@ -150,28 +182,44 @@ def consume_arguments(): required=False, action="store_true" ) - upd.add_argument("-r", "--remove", help="Remove the policy", required=False, action="store_true") - upd.add_argument("-g", "--add_host_group", help="Add host group to the specified policy\n(comma delimit)", required=False) + upd.add_argument("-r", "--remove", + help="Remove the policy", + required=False, + action="store_true") + upd.add_argument("-g", "--add_host_group", + help="Add host group to the specified policy\n(comma delimit)", + required=False) upd.add_argument("-y", "--yank_host_group", help="Remove host group from the specified policy\n(comma delimit)", required=False ) # IDs and platform names for updates idg = parser.add_argument_group("required arguments for updating or removing policies") - idg.add_argument("-i", "--policy_id", help="ID(s) of the policy to update or remove (comma delimit)", required=False) - idg.add_argument("-n", "--platform_name", help="Platform name for policy precedence configurations", required=False) + idg.add_argument("-i", "--policy_id", + help="ID(s) of the policy to update or remove (comma delimit)", + required=False) + idg.add_argument("-n", "--platform_name", + help="Platform name for policy precedence configurations", + required=False) # MSSP msp = parser.add_argument_group("MSSP arguments") - msp.add_argument("-w", "--member_cid", help="Child CID (MSSP access)", required=False) + msp.add_argument("-w", "--member_cid", + help="Child CID (MSSP access)", + required=False) # Other oth = parser.add_argument_group("other arguments") - oth.add_argument("-t", "--base_url", help="Specify the API base URL", required=False) + oth.add_argument("-t", "--base_url", + help="Specify the API base URL", + required=False) # Always required req = parser.add_argument_group("always required arguments") - req.add_argument("-f", "--falcon_client_id", help="Falcon Client ID", required=True) - req.add_argument("-s", "--falcon_client_secret", help="Falcon Client Secret", required=True) + req.add_argument("-f", "--falcon_client_id", + help="Falcon Client ID", + required=True) + req.add_argument("-s", "--falcon_client_secret", + help="Falcon Client Secret", + required=True) - return parser.parse_args() def process_command_line(): # pylint: disable=R0912,R0915 @@ -252,10 +300,13 @@ def process_command_line(): # pylint: disable=R0912,R0915 base_url = "auto" if args.base_url: base_url = args.base_url + + if args.debug: + logging.basicConfig(level=logging.DEBUG) return command_to_perform, args.falcon_client_id, args.falcon_client_secret, args.search_string,\ args.policy_id, update_type, flag_type, hide_members, args.platform_name, group_id,\ - hide_groups, mssp_access, base_url + hide_groups, mssp_access, base_url, args.debug def hide_members_column(): @@ -407,7 +458,7 @@ def update_policies(id_to_update: str, update_style: str = "", flag_style: str = if update_result["status_code"] != 200: raise SystemExit(generate_api_error_list(update_result["body"]["errors"])) - +# pylint: disable=E0606 def list_kernel_compatibility(): """List all available kernels.""" kernel_list_lookup = falcon.query_combined_kernels() @@ -663,16 +714,16 @@ def list_host_groups(search_str: str = ""): Install it with: {Color.BOLD}python3 -m pip install crowdstrike-falconpy{Color.END} """ -if int(FALCONPY_VERSION.split(".")[0]) < 1: +if int(FALCONPY_VERSION.split(".", maxsplit=1)[0]) < 1: raise SystemExit(INVALID_VERSION) INDICATOR = ["|", "/", "-", "\\"] INDICATOR_POSITION = 0 command, client_id, client_secret, API_SEARCH, policy_id, which_update, \ - enable_disable, HIDE, platform_name, hg_id, GROUP_HIDE, member_cid, base_url = process_command_line() -falcon = connect_sensor_update_api(client_id, client_secret, member_cid, base_url) -falcon_groups = connect_host_group_api(client_id, client_secret, member_cid, base_url) + enable_disable, HIDE, platform_name, hg_id, GROUP_HIDE, member_cid, base_url, debug = process_command_line() +falcon = connect_sensor_update_api(client_id, client_secret, member_cid, base_url, debug) +falcon_groups = connect_host_group_api(client_id, client_secret, member_cid, base_url, debug) if "kernel" in command: list_kernel_compatibility() diff --git a/samples/sensor_update_policies/requirements_policy_wonk.txt b/samples/sensor_update_policies/requirements_policy_wonk.txt new file mode 100644 index 000000000..790cddc1f --- /dev/null +++ b/samples/sensor_update_policies/requirements_policy_wonk.txt @@ -0,0 +1,2 @@ +crowdstrike-falconpy +tabulate From ed70335832881805d18bdb0d0f07c7d382ae05fd Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 10:04:46 -0700 Subject: [PATCH 26/30] Update prevention_policy_hawk sample --- samples/prevention_policy/prevention_policy_hawk.py | 4 ++-- samples/recon/email_monitoring_recon.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/samples/prevention_policy/prevention_policy_hawk.py b/samples/prevention_policy/prevention_policy_hawk.py index 319b3f234..be0711907 100644 --- a/samples/prevention_policy/prevention_policy_hawk.py +++ b/samples/prevention_policy/prevention_policy_hawk.py @@ -353,7 +353,7 @@ def consume_command_line(): parser = ArgumentParser(description=shiny_description(__doc__), formatter_class=RawTextHelpFormatter ) - parser.add_argument("-d", "--debug", + parser.add_argument("-debug", "--debug", help="Enable API debugging", action="store_true", default=False @@ -621,7 +621,7 @@ def process_arguments(command, falcon_policy = PreventionPolicy(client_id=args.falcon_client_id, client_secret=args.falcon_client_secret, debug=args.debug - ) + ) # Review the provided arguments and then perform the request process_arguments(*determine_arguments(args)) diff --git a/samples/recon/email_monitoring_recon.py b/samples/recon/email_monitoring_recon.py index 9dec83274..887b8e04d 100644 --- a/samples/recon/email_monitoring_recon.py +++ b/samples/recon/email_monitoring_recon.py @@ -41,7 +41,7 @@ falcon = Recon(client_id=args.falcon_client_id, client_secret=args.falcon_client_secret, - base_url=args.base_url + base_url=args.base_url, debug=args.debug ) QUERY = "(" From e5849671c3d9bc0e1580ed5fe5d3a6c1b2cada93 Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 13:16:33 -0700 Subject: [PATCH 27/30] Fix typo in sensor download README --- samples/sensor_download/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/sensor_download/README.md b/samples/sensor_download/README.md index d2409fce2..5b3befb7b 100644 --- a/samples/sensor_download/README.md +++ b/samples/sensor_download/README.md @@ -92,13 +92,13 @@ Filters described above are applied to select the appropriate version to downloa python3 download_sensor.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -o centos -v 7 -d ``` ##### Activating Debugging -This exmaple shows how you can activate debugging functionality when you run download_senor.py. +This example shows how you can activate debugging functionality when you run download_senor.py. ```shell python3 download_sensor.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -debug ``` ##### Allowing Access to GovCloud Users -This exmaple shows how you GovCloud user can access sensor_download.py. +This example shows how you GovCloud user can access sensor_download.py. ```shell python3 download_sensor.py -k $FALCON_CLIENT_ID -s $FALCON_CLIENT_SECRET -b From d1016cd34b1368a3f288699d3417c546797466df Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 18:52:00 -0700 Subject: [PATCH 28/30] Cleaning up debugging implementation in create_ioc.py and email_monitoring_recon.py --- samples/ioc/create_ioc.py | 24 ++++++++++++++++-------- samples/recon/email_monitoring_recon.py | 9 ++++++++- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/samples/ioc/create_ioc.py b/samples/ioc/create_ioc.py index 5592b4265..df39099ae 100644 --- a/samples/ioc/create_ioc.py +++ b/samples/ioc/create_ioc.py @@ -33,10 +33,20 @@ def consume_command_line(): parser = ArgumentParser(description=__doc__, formatter_class=RawTextHelpFormatter) - parser.add_argument("-k", "--falcon_client_id", help="Falcon API Client ID", required=True) - parser.add_argument("-s", "--falcon_client_secret", help="Falcon API Client Secret", required=True) - parser.add_argument("-m", "--method", help="SDK method to use ('service' or 'uber').", required=False, default="service") - parser.add_argument("-i", "--indicator", help="Path to the file representing the indicator (JSON format).", default="example_indicator.json", required=False) + parser.add_argument("-k", "--falcon_client_id", + help="Falcon API Client ID", + required=True) + parser.add_argument("-s", "--falcon_client_secret", + help="Falcon API Client Secret", + required=True) + parser.add_argument("-m", "--method", + help="SDK method to use ('service' or 'uber').", + required=False, + default="service") + parser.add_argument("-i", "--indicator", + help="Path to the file representing the indicator (JSON format).", + default="example_indicator.json", + required=False) parser.add_argument("-d", "--debug", help="Enable API debugging", action="store_true", @@ -45,14 +55,12 @@ def consume_command_line(): parsed = parser.parse_args() - allow = ["indicator", "report", "actor"] - parsed.types = [t for t in parsed.types.split(",") if t in allow] if parsed.types else allow if parsed.debug: logging.basicConfig(level=logging.DEBUG) - return parser.parse_args() + return parsed def connect_api(class_type: str = "service", creds: dict = None): @@ -73,7 +81,7 @@ def connect_api(class_type: str = "service", creds: dict = None): if args.method not in ["service", "uber"]: args.method = "service" -falcon = connect_api(args.method, credentials) +falcon = connect_api(args.method, credentials, args.debug) if not os.path.exists(args.indicator): raise SystemExit("Unable to load indicator file.") diff --git a/samples/recon/email_monitoring_recon.py b/samples/recon/email_monitoring_recon.py index 887b8e04d..f94cc93d5 100644 --- a/samples/recon/email_monitoring_recon.py +++ b/samples/recon/email_monitoring_recon.py @@ -8,6 +8,7 @@ Creation: 06.21.2022, wozboz@CrowdStrike """ +import logging from csv import reader from argparse import ArgumentParser, RawTextHelpFormatter from falconpy import Recon @@ -33,8 +34,14 @@ help="File with email-addresses to use as input", required=True, ) - +parser.add_argument("-d", "--debug", + help="Enable API debugging", + action="store_true", + default=False + ) args = parser.parse_args() +if args.debug: + logging.basicConfig(level=logging.DEBUG) EMAIL_FILE = args.file From b4d0b9826c750b8c54056a652a0bb13a95608755 Mon Sep 17 00:00:00 2001 From: okewoma Date: Tue, 16 Jul 2024 10:29:41 -0700 Subject: [PATCH 29/30] Update wordlist.txt --- .github/wordlist.txt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/wordlist.txt b/.github/wordlist.txt index e411f0c5e..31ac3e0d7 100644 --- a/.github/wordlist.txt +++ b/.github/wordlist.txt @@ -1380,4 +1380,12 @@ Destom ValueError QueryCasesIdsByFilter SDKDEMO - +kube +KPA +argparse +colorama +Oke +Okumo +Moomaw +Esha +Kumar \ No newline at end of file From 503cb35eb482c3248f7af4ef678839f8a0f4df6e Mon Sep 17 00:00:00 2001 From: Joshua Hiller Date: Thu, 18 Jul 2024 00:11:37 -0400 Subject: [PATCH 30/30] Add debugging and base_url functionality to download sensor sample. Related to #1181. --- samples/sensor_download/download_sensor.py | 28 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/samples/sensor_download/download_sensor.py b/samples/sensor_download/download_sensor.py index 5260f9678..f0ca1256a 100644 --- a/samples/sensor_download/download_sensor.py +++ b/samples/sensor_download/download_sensor.py @@ -14,6 +14,7 @@ Required API Scope - Sensor Download: READ """ from argparse import ArgumentParser, RawTextHelpFormatter +import logging from tabulate import tabulate try: from falconpy import APIHarness @@ -70,6 +71,17 @@ def consume_arguments(): 'latext, latex_raw, latex_booktabs, latex_longtable, textile, tsv)', required=False, default="fancy_grid" ) + parser.add_argument('-b', '--base_url', + help="CrowdStrike region (only required for usgov1)", + required=False, + default="auto" + ) + parser.add_argument('--debug', + help="Enable API debugging", + required=False, + default=False, + action="store_true" + ) return parser.parse_args() def get_version_map(sensor_versions: list): # pylint: disable=R0914 @@ -166,17 +178,25 @@ def create_constants(): os_filter = f"os:'{str(os_name)}'" return cmd, args.key, args.secret, os_filter, args.filename, args.table_format, args.all, \ - args.osver, args.nminus + args.osver, args.nminus, args.debug, args.base_url CMD, CLIENTID, CLIENTSECRET, OS_FILTER, FILENAME, FORMAT, SHOW_ALL, \ - OSVER, NMINUS = create_constants() + OSVER, NMINUS, ENABLE_DEBUG, BASE_URL = create_constants() + +if ENABLE_DEBUG: + # Activate debugging if requested + logging.basicConfig(level=logging.DEBUG) # Login to the Falcon API and retrieve our list of sensors -falcon = APIHarness(client_id=CLIENTID, client_secret=CLIENTSECRET) +falcon = APIHarness(client_id=CLIENTID, + client_secret=CLIENTSECRET, + debug=ENABLE_DEBUG, + base_url=BASE_URL + ) sensors = falcon.command(action="GetCombinedSensorInstallersByQuery", filter=OS_FILTER, - sort="version.desc" + sort="version.desc" ) if sensors["status_code"] == 401: