From 1721d3d32092f52155938c90395a5a6145dbdee6 Mon Sep 17 00:00:00 2001 From: Gianluca Salvo Date: Wed, 20 Mar 2024 11:58:04 +0100 Subject: [PATCH 1/5] Support for hostname templating --- plugins/inventory/falcon_hosts.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/plugins/inventory/falcon_hosts.py b/plugins/inventory/falcon_hosts.py index 0dbb298e..22f67595 100644 --- a/plugins/inventory/falcon_hosts.py +++ b/plugins/inventory/falcon_hosts.py @@ -57,6 +57,14 @@ - See the L(Falcon documentation,https://falcon.crowdstrike.com/documentation/page/c0b16f1b/host-and-host-group-management-apis#qadd6f8f) for more information about what filters are available for this inventory. type: str + hostnames: + description: + - A list of templates in order of precedence to compose inventory_hostname. + - Ignores template if resulted in an empty string or None value. + - You can use property specified in I(properties) as variables in the template. + type: list + elements: string + default: ['hostname'] requirements: - python >= 3.6 - crowdstrike-falconpy >= 1.3.0 @@ -306,6 +314,13 @@ def _get_hostname(self, hostvars): if ipaddress: hostname = ipaddress + hostnames = self.get_option('hostnames') + for preference in hostnames: + try: + hostname = self._compose(preference,hostvars) + except: + self.display.vv("Error in Hostname templating for host \'{hostname}\' and template \'{template}\'".format(hostname = hostname,template=preference) ) + return hostname def _add_host_to_inventory(self, host_details): From 5aefbf9e15cca1fe62331efbee2e5ae513311a37 Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Fri, 22 Mar 2024 12:35:51 -0400 Subject: [PATCH 2/5] refactor how we handle hostnames and hostname precedence --- plugins/inventory/falcon_hosts.py | 70 +++++++++++++++++-------------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/plugins/inventory/falcon_hosts.py b/plugins/inventory/falcon_hosts.py index 22f67595..0dd0f92d 100644 --- a/plugins/inventory/falcon_hosts.py +++ b/plugins/inventory/falcon_hosts.py @@ -61,10 +61,11 @@ description: - A list of templates in order of precedence to compose inventory_hostname. - Ignores template if resulted in an empty string or None value. - - You can use property specified in I(properties) as variables in the template. + - You can use any host variable as a template. + - The default is to use the hostname, external_ip, and local_ip in that order. type: list elements: string - default: ['hostname'] + default: ['hostname', 'external_ip', 'local_ip'] requirements: - python >= 3.6 - crowdstrike-falconpy >= 1.3.0 @@ -126,6 +127,17 @@ # place hosts in a group named aws_us_west_2 if the zone_group is in us-west-2 aws_us_west_2: "'us-west-2' in zone_group and 'Amazon' in system_manufacturer" +# use Jinja2 expressions to compose hostnames +# hostnames: +# - hostname|lower + +# define the order of precedence for composing hostnames +# hostnames: +# - device_id +# - hostname +# - external_ip +# - local_ip + # create and modify host variables from Jinja2 expressions # compose: # # this sets the ansible_host variable to the external_ip address @@ -148,6 +160,8 @@ import re import traceback +from ansible.errors import AnsibleError +from ansible.module_utils.common.text.converters import to_native, to_text from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable FALCONPY_IMPORT_ERROR = None @@ -292,47 +306,42 @@ def _hostvars(self, host): return hostvars - def _get_ip_address(self, hostvars): - """Return the IP address for a host.""" - ip_address = None - if "external_ip" in hostvars: - ip_address = hostvars["external_ip"] - elif "local_ip" in hostvars: - ip_address = hostvars["local_ip"] - - return ip_address - - def _get_hostname(self, hostvars): + def _get_hostname(self, hostvars, hostnames=None, strict=False): """Return the hostname for a host.""" hostname = None + errors = [] - if hostvars.get("hostname"): - hostname = hostvars.get("hostname") - else: - # Use the IP address as the hostname if no hostname is available - ipaddress = self._get_ip_address(hostvars) - if ipaddress: - hostname = ipaddress - - hostnames = self.get_option('hostnames') for preference in hostnames: try: - hostname = self._compose(preference,hostvars) - except: - self.display.vv("Error in Hostname templating for host \'{hostname}\' and template \'{template}\'".format(hostname = hostname,template=preference) ) + hostname = self._compose(preference, hostvars) + except Exception as e: # pylint: disable=broad-except + if strict: + raise AnsibleError( + "Could not compose %s as hostnames - %s" % (preference, to_native(e)) + ) from e + + errors.append( + (preference, str(e)) + ) + if hostname: + return to_text(hostname) - return hostname + raise AnsibleError( + 'Could not template any hostname for host, errors for each preference: %s' % ( + ', '.join(['%s: %s' % (pref, err) for pref, err in errors]) + ) + ) def _add_host_to_inventory(self, host_details): """Add host to inventory.""" + strict = self.get_option("strict") + hostnames = self.get_option("hostnames") + for host in host_details: hostvars = self._hostvars(host) # Get the hostname - hostname = self._get_hostname(hostvars) - if not hostname: - # Skip the host if no hostname is available - continue + hostname = self._get_hostname(hostvars, hostnames, strict) # Add the host to the inventory self.inventory.add_host(hostname) @@ -342,7 +351,6 @@ def _add_host_to_inventory(self, host_details): self.inventory.set_variable(hostname, key, value) # Add host groups - strict = self.get_option("strict") self._set_composite_vars(self.get_option("compose"), hostvars, hostname, strict) # Create user-defined groups based on variables/jinja2 conditionals From 98a820cf9620cfc4a44ec118ba28b97c51afd881 Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Mon, 25 Mar 2024 16:20:39 -0400 Subject: [PATCH 3/5] update the examples to be more explicit This helps our docs be clearer as to the potential pitfalls of using hostname for inventory_hostname due to duplication. --- plugins/inventory/falcon_hosts.py | 160 ++++++++++++++++-------------- 1 file changed, 83 insertions(+), 77 deletions(-) diff --git a/plugins/inventory/falcon_hosts.py b/plugins/inventory/falcon_hosts.py index 0dd0f92d..3dd754ab 100644 --- a/plugins/inventory/falcon_hosts.py +++ b/plugins/inventory/falcon_hosts.py @@ -59,7 +59,7 @@ type: str hostnames: description: - - A list of templates in order of precedence to compose inventory_hostname. + - A list of templates in order of precedence to compose C(inventory_hostname). - Ignores template if resulted in an empty string or None value. - You can use any host variable as a template. - The default is to use the hostname, external_ip, and local_ip in that order. @@ -70,6 +70,9 @@ - python >= 3.6 - crowdstrike-falconpy >= 1.3.0 notes: + - By default, Ansible will deduplicate the C(inventory_hostname), so if multiple hosts have the same hostname, only + the last one will be used. In this case, consider using the C(device_id) as the first preference in the C(hostnames). + You can use C(compose) to specify how Ansible will connectz to the host with the C(ansible_host) variable. - If no credentials are provided, FalconPy will attempt to use the API credentials via environment variables. - The current behavior is to use the hostname if it exists, otherwise we will attemp to use either the external IP address or the local IP address. If neither of those exist, the host will be skipped as Ansible would not @@ -79,81 +82,84 @@ """ EXAMPLES = r""" -# sample file: my_inventory.falcon_hosts.yml - -# required for all falcon_hosts inventory configs -plugin: crowdstrike.falcon.falcon_hosts - -# authentication credentials (required if not using environment variables) -#client_id: 1234567890abcdef12345678 -#client_secret: 1234567890abcdef1234567890abcdef12345 -#cloud: us-1 - -# fql filter expression to limit results (by default all hosts are returned) -# examples below: - -# return all Windows hosts -#filter: "platform_name:'Windows'" - -# return stale devices that haven't checked in for 15 days -#filter: "last_seen:<='now-15d'" - -# return all new Linux hosts within the past week -#filter: "first_seen:<='now-1w' + platform_name:'Linux'" - -# return all hosts seen in the last 12 hours that are in RFM mode -#filter: "reduced_functionality_mode:'yes' + last_seen:>='now-12h'" - -# return all Linux hosts running in eBPF User Mode -#filter: "linux_sensor_mode:'User Mode'" - -# place hosts into dynamically created groups based on variable values -keyed_groups: - # places host in a group named tag_ for each tag on a host - - prefix: tag - key: tags - # places host in a group named platform_ based on the platform name (Linux, Windows, etc.) - - prefix: platform - key: platform_name - # places host in a group named rfm_ to see if the host is in reduced functionality mode - - prefix: rfm - key: reduced_functionality_mode - -# place hosts in named groups based on conditional statements -groups: - # places hosts in a group named windows_hosts if the platform_name is Windows - windows_hosts: "platform_name == 'Windows'" - - # place hosts in a group named aws_us_west_2 if the zone_group is in us-west-2 - aws_us_west_2: "'us-west-2' in zone_group and 'Amazon' in system_manufacturer" - -# use Jinja2 expressions to compose hostnames -# hostnames: -# - hostname|lower - -# define the order of precedence for composing hostnames -# hostnames: -# - device_id -# - hostname -# - external_ip -# - local_ip - -# create and modify host variables from Jinja2 expressions -# compose: -# # this sets the ansible_host variable to the external_ip address -# ansible_host: external_ip -# # this defines combinations of host servers, IP addresses, and related SSH private keys. -# ansible_host: external_ip -# ansible_user: "'root'" -# ansible_ssh_private_key_file: "'/path/to/private_key_file'" - -# caching is supported for this inventory plugin. -# caching can be configured in the ansible.cfg file or in the inventory file. -cache: true -cache_plugin: jsonfile -cache_connection: /tmp/falcon_inventory -cache_timeout: 1800 -cache_prefix: falcon_hosts +# Return all hosts + plugin: crowdstrike.falcon.falcon_hosts + client_id: 1234567890abcdef12345678 + client_secret: 1234567890abcdef1234567890abcdef12345 + cloud: us-1 + +# Return all Windows hosts (authentication via environment variables) + plugin: crowdstrike.falcon.falcon_hosts + filter: "platform_name:'Windows'" + +# Return all Linux hosts in reduced functionality mode + plugin: crowdstrike.falcon.falcon_hosts + filter: "platform_name:'Linux' + reduced_functionality_mode:'yes'" + +# Return stale devices that haven't checked in for 15 days + plugin: crowdstrike.falcon.falcon_hosts + filter: "last_seen:<='now-15d'" + +# Return all Linux hosts running in eBPF User Mode + plugin: crowdstrike.falcon.falcon_hosts + filter: "linux_sensor_mode:'User Mode'" + +# Place hosts into dynamically created groups based on variable values + plugin: crowdstrike.falcon.falcon_hosts + keyed_groups: + # places host in a group named tag_ for each tag on a host + - prefix: tag + key: tags + # places host in a group named platform_ based on the + # platform name (Linux, Windows, etc.) + - prefix: platform + key: platform_name + # places host in a group named tag_ for each tag on a host + - prefix: rfm + key: reduced_functionality_mode + +# Place hosts into dynamically created groups based on conditional statements + plugin: crowdstrike.falcon.falcon_hosts + groups: + # places hosts in a group named windows_hosts if the platform_name is Windows + windows_hosts: "platform_name == 'Windows'" + # place hosts in a group named aws_us_west_2 if the zone_group is in us-west-2 + aws_us_west_2: "'us-west-2' in zone_group and 'Amazon' in system_manufacturer" + +# Compose inventory_hostname from Jinja2 expressions + plugin: crowdstrike.falcon.falcon_hosts + hostnames: + - hostname|lower + +# Compose inventory_hostname from Jinja2 expressions with order of precedence + plugin: crowdstrike.falcon.falcon_hosts + hostnames: + - external_ip + - local_ip + - serial_number + +# Use device_id as the inventory_hostname to prevent deduplication and set ansible_host +# to a reachable attribute + plugin: crowdstrike.falcon.falcon_hosts + hostnames: + - device_id + compose: + ansible_host: hostname | default(external_ip) | default(local_ip) | default(None) + +# Compose connection variables for each host + plugin: crowdstrike.falcon.falcon_hosts + compose: + ansible_host: external_ip + ansible_user: "'root'" + ansible_ssh_private_key_file: "'/path/to/private_key_file'" + +# Use caching for the inventory + plugin: crowdstrike.falcon.falcon_hosts + cache: true + cache_plugin: jsonfile + cache_connection: /tmp/falcon_inventory + cache_timeout: 1800 + cache_prefix: falcon_hosts """ import os @@ -350,7 +356,7 @@ def _add_host_to_inventory(self, host_details): for key, value in hostvars.items(): self.inventory.set_variable(hostname, key, value) - # Add host groups + # Create composite vars self._set_composite_vars(self.get_option("compose"), hostvars, hostname, strict) # Create user-defined groups based on variables/jinja2 conditionals From e92ec7339efc1ce451b00ccaf8c7e38533e40aeb Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Mon, 25 Mar 2024 16:48:10 -0400 Subject: [PATCH 4/5] chore: fix lint issues with examples section To not upset the Ansible lint gods, these updates provide the same example set in the 'sample file' format. --- plugins/inventory/falcon_hosts.py | 143 ++++++++++++++---------------- 1 file changed, 68 insertions(+), 75 deletions(-) diff --git a/plugins/inventory/falcon_hosts.py b/plugins/inventory/falcon_hosts.py index 3dd754ab..f9a7d10e 100644 --- a/plugins/inventory/falcon_hosts.py +++ b/plugins/inventory/falcon_hosts.py @@ -82,84 +82,77 @@ """ EXAMPLES = r""" -# Return all hosts - plugin: crowdstrike.falcon.falcon_hosts - client_id: 1234567890abcdef12345678 - client_secret: 1234567890abcdef1234567890abcdef12345 - cloud: us-1 - -# Return all Windows hosts (authentication via environment variables) - plugin: crowdstrike.falcon.falcon_hosts - filter: "platform_name:'Windows'" - -# Return all Linux hosts in reduced functionality mode - plugin: crowdstrike.falcon.falcon_hosts - filter: "platform_name:'Linux' + reduced_functionality_mode:'yes'" - -# Return stale devices that haven't checked in for 15 days - plugin: crowdstrike.falcon.falcon_hosts - filter: "last_seen:<='now-15d'" - -# Return all Linux hosts running in eBPF User Mode - plugin: crowdstrike.falcon.falcon_hosts - filter: "linux_sensor_mode:'User Mode'" - -# Place hosts into dynamically created groups based on variable values - plugin: crowdstrike.falcon.falcon_hosts - keyed_groups: - # places host in a group named tag_ for each tag on a host - - prefix: tag - key: tags - # places host in a group named platform_ based on the - # platform name (Linux, Windows, etc.) - - prefix: platform - key: platform_name - # places host in a group named tag_ for each tag on a host - - prefix: rfm - key: reduced_functionality_mode - -# Place hosts into dynamically created groups based on conditional statements - plugin: crowdstrike.falcon.falcon_hosts - groups: - # places hosts in a group named windows_hosts if the platform_name is Windows - windows_hosts: "platform_name == 'Windows'" - # place hosts in a group named aws_us_west_2 if the zone_group is in us-west-2 - aws_us_west_2: "'us-west-2' in zone_group and 'Amazon' in system_manufacturer" - -# Compose inventory_hostname from Jinja2 expressions - plugin: crowdstrike.falcon.falcon_hosts - hostnames: - - hostname|lower - -# Compose inventory_hostname from Jinja2 expressions with order of precedence - plugin: crowdstrike.falcon.falcon_hosts - hostnames: - - external_ip - - local_ip - - serial_number - -# Use device_id as the inventory_hostname to prevent deduplication and set ansible_host +# sample file: my_inventory.falcon_hosts.yml + +# required for all falcon_hosts inventory plugin configs +plugin: crowdstrike.falcon.falcon_hosts + +# authentication credentials (required if not using environment variables) +# client_id: 1234567890abcdef12345678 +# client_secret: 1234567890abcdef1234567890abcdef12345 +# cloud: us-1 + +# return all Windows hosts (authentication via environment variables) +# filter: "platform_name:'Windows'" + +# return all Linux hosts in reduced functionality mode +# filter: "platform_name:'Linux' + reduced_functionality_mode:'yes'" + +# return stale devices that haven't checked in for 15 days +# filter: "last_seen:<='now-15d'" + +# return all Linux hosts running in eBPF User Mode +# filter: "linux_sensor_mode:'User Mode'" + +# place hosts into dynamically created groups based on variable values +keyed_groups: + # places host in a group named tag_ for each tag on a host + - prefix: tag + key: tags + # places host in a group named platform_ based on the + # platform name (Linux, Windows, etc.) + - prefix: platform + key: platform_name + # places host in a group named tag_ for each tag on a host + - prefix: rfm + key: reduced_functionality_mode + +# place hosts into dynamically created groups based on conditional statements +groups: + # places hosts in a group named windows_hosts if the platform_name is Windows + windows_hosts: "platform_name == 'Windows'" + # place hosts in a group named aws_us_west_2 if the zone_group is in us-west-2 + aws_us_west_2: "'us-west-2' in zone_group and 'Amazon' in system_manufacturer" + +# compose inventory_hostname from Jinja2 expressions +# hostnames: +# - hostname|lower + +# compose inventory_hostname from Jinja2 expressions with order of precedence +# hostnames: +# - external_ip +# - local_ip +# - serial_number + +# use device_id as the inventory_hostname to prevent deduplication and set ansible_host # to a reachable attribute - plugin: crowdstrike.falcon.falcon_hosts - hostnames: - - device_id - compose: - ansible_host: hostname | default(external_ip) | default(local_ip) | default(None) - -# Compose connection variables for each host - plugin: crowdstrike.falcon.falcon_hosts - compose: - ansible_host: external_ip - ansible_user: "'root'" - ansible_ssh_private_key_file: "'/path/to/private_key_file'" +# hostnames: +# - device_id +# compose: +# ansible_host: hostname | default(external_ip) | default(local_ip) | default(None) + +# compose connection variables for each host +# compose: +# ansible_host: external_ip +# ansible_user: "'root'" +# ansible_ssh_private_key_file: "'/path/to/private_key_file'" # Use caching for the inventory - plugin: crowdstrike.falcon.falcon_hosts - cache: true - cache_plugin: jsonfile - cache_connection: /tmp/falcon_inventory - cache_timeout: 1800 - cache_prefix: falcon_hosts +# cache: true +# cache_plugin: jsonfile +# cache_connection: /tmp/falcon_inventory +# cache_timeout: 1800 +# cache_prefix: falcon_hosts """ import os From 3067d70354e5b05f3fd28e3c5e10bcd6422ee1e8 Mon Sep 17 00:00:00 2001 From: Carlos Matos Date: Mon, 25 Mar 2024 16:54:55 -0400 Subject: [PATCH 5/5] add changelog fragment --- changelogs/fragments/pr-474.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 changelogs/fragments/pr-474.yml diff --git a/changelogs/fragments/pr-474.yml b/changelogs/fragments/pr-474.yml new file mode 100644 index 00000000..1b763bbe --- /dev/null +++ b/changelogs/fragments/pr-474.yml @@ -0,0 +1,2 @@ +bugfixes: + - falcon_hosts - added support for hostname preferences and fixed documentation (https://github.com/CrowdStrike/ansible_collection_falcon/pull/474)