-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Added agent_info module and extracted common code with download_agent module to super class --------- Co-authored-by: Marco Wester <marco.wester@sva.de>
- Loading branch information
1 parent
4a9f706
commit c8f8d43
Showing
7 changed files
with
350 additions
and
163 deletions.
There are no files selected for viewing
115 changes: 115 additions & 0 deletions
115
plugins/module_utils/sentinelone/sentinelone_agent_base.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2024, Marco Wester <marco.wester@sva.de> | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
from ansible.module_utils.six.moves.urllib.parse import urlencode | ||
from ansible_collections.sva.sentinelone.plugins.module_utils.sentinelone.sentinelone_base import SentineloneBase | ||
from ansible.module_utils.basic import AnsibleModule | ||
|
||
|
||
class SentineloneAgentBase(SentineloneBase): | ||
def __init__(self, module: AnsibleModule): | ||
module.params['site_name'] = module.params['site'] | ||
|
||
module.params['site_name'] = module.params['site'] | ||
# self.token, self.console_url, self.site_name, self.state, self.api_endpoint_*, self.group_names will be set in | ||
# super Class | ||
super().__init__(module) | ||
|
||
# Set module specific parameters | ||
self.agent_version = module.params["agent_version"] | ||
self.custom_version = module.params["custom_version"] | ||
self.os_type = module.params["os_type"] | ||
self.packet_format = module.params["packet_format"] | ||
self.architecture = module.params["architecture"] | ||
self.download_dir = module.params.get("download_dir", None) | ||
|
||
# Do sanity checks | ||
self.check_sanity(self.os_type, self.packet_format, self.architecture, module) | ||
|
||
@staticmethod | ||
def check_sanity(os_type: str, packet_format: str, architecture: str, module: AnsibleModule): | ||
""" | ||
Check if the passed module arguments are contradicting each other | ||
:param architecture: OS architecture | ||
:type architecture: str | ||
:param os_type: The specified OS type | ||
:type os_type: str | ||
:param packet_format: The speciefied packet format | ||
:type packet_format: str | ||
:param module: Ansible module for error handling | ||
:type module: AnsibleModule | ||
""" | ||
|
||
if architecture == "aarch64" and os_type != "Linux": | ||
module.fail_json(msg="Error: architecture 'aarch64' needs os_type to be 'Linux'") | ||
|
||
if os_type == 'Windows': | ||
if packet_format not in ['exe', 'msi']: | ||
module.fail_json(msg="Error: 'packet_format' needs to be 'exe' or 'msi' if os_type is 'Windows'") | ||
elif packet_format not in ['deb', 'rpm']: | ||
module.fail_json(msg="Error: 'packet_format' needs to be 'deb' or 'rpm' if os_type is 'Linux'") | ||
|
||
def get_package_obj(self, agent_version: str, custom_version: str, os_type: str, packet_format: str, | ||
architecture: str, module: AnsibleModule): | ||
""" | ||
Queries the API to get the info about the agent package which maches the parameters | ||
:param agent_version: which version to search for | ||
:type agent_version: str | ||
:param custom_version: custom agent version if specified | ||
:type custom_version: str | ||
:param os_type: For which OS the package should fit | ||
:type os_type: str | ||
:param packet_format: the packet format | ||
:type packet_format: str | ||
:param architecture: The OS architecture | ||
:type architecture: str | ||
:param module: Ansible module for error handling | ||
:type module: AnsibleModule | ||
:return: Returns the found agent object | ||
:rtype: dict | ||
""" | ||
|
||
# Build query parameters dependend on the Modules input | ||
# Default parameters which are set always | ||
query_params = { | ||
'platformTypes': os_type.lower(), | ||
'sortOrder': 'desc', | ||
'sortBy': 'version', | ||
'fileExtension': f".{packet_format}" | ||
} | ||
|
||
if self.site_id is not None: | ||
query_params['siteIds'] = str(self.site_id) | ||
|
||
if agent_version == 'custom': | ||
query_params['version'] = custom_version | ||
elif agent_version == 'latest': | ||
query_params['status'] = 'ga' | ||
|
||
if os_type == 'Linux': | ||
# Use query parameter to do a free text search matching the 'fileName' field beacause S1 API does not | ||
# provide the information elementary. 'osArches' parameter applies only for windows | ||
if architecture == 'aarch64': | ||
query_params['query'] = 'SentinelAgent-aarch64' | ||
else: | ||
query_params['query'] = 'SentinelAgent_linux' | ||
else: | ||
query_params['packageType'] = 'AgentAndRanger' | ||
# osArches is only supported if you query windows packaes | ||
query_params['osArches'] = architecture.replace('_', ' ') | ||
|
||
# translate dictionary to URI argurments and build full query | ||
query_params_encoded = urlencode(query_params) | ||
api_query_agent_package = f"{self.api_endpoint_update_agent_packages}?{query_params_encoded}" | ||
|
||
response = self.api_call(module, api_query_agent_package) | ||
if response["pagination"]["totalItems"] > 0: | ||
return response["data"][0] | ||
|
||
module.fail_json(msg="Error: No agent package found in management console. Please check the given parameters.") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,199 @@ | ||
#!/usr/bin/python | ||
# -*- coding: utf-8 -*- | ||
|
||
# Copyright: (c) 2024, Marco Wester <marco.wester@sva.de> | ||
# Erik Schindler <erik.schindler@sva.de> | ||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) | ||
from __future__ import (absolute_import, division, print_function) | ||
__metaclass__ = type | ||
|
||
DOCUMENTATION = r''' | ||
--- | ||
module: sentinelone_agent_info | ||
short_description: "Get info about the SentinelOne agent package" | ||
version_added: "2.0.0" | ||
description: | ||
- "This module is able to get info about the sentinelone agent package you requested" | ||
options: | ||
console_url: | ||
description: | ||
- "Insert your management console URL" | ||
type: str | ||
required: true | ||
site: | ||
description: | ||
- "Optional name of the site from where the agent package is located" | ||
- "If omitted the scope will be on account level" | ||
type: str | ||
required: false | ||
token: | ||
description: | ||
- "SentinelOne API auth token to authenticate at the management API" | ||
type: str | ||
required: true | ||
agent_version: | ||
description: | ||
- "Version of the agent to get info about" | ||
- "B(latest) (default) - Latest GA (stable) release for the specified parameters" | ||
- "B(latest_ea) - same as latest, but also includes EA packages" | ||
- "B(custom) - custom_version is required when agent_versioin is custom" | ||
type: str | ||
default: latest | ||
required: false | ||
choices: | ||
- latest | ||
- latest_ea | ||
- custom | ||
custom_version: | ||
description: | ||
- "Explicit version of the agent to get info about" | ||
- "Has to be set when agent_version=custom" | ||
- "Will be ignored if B(agent_version) is not B(custom)" | ||
type: str | ||
required: false | ||
os_type: | ||
description: | ||
- "The type of the OS" | ||
type: str | ||
required: true | ||
choices: | ||
- Linux | ||
- Windows | ||
packet_format: | ||
description: | ||
- "The format of the agent package" | ||
type: str | ||
required: true | ||
choices: | ||
- rpm | ||
- deb | ||
- msi | ||
- exe | ||
architecture: | ||
description: | ||
- "Architecture of the packet" | ||
- "Windows: Only B(32_bit) and B(64_bit) are allowed" | ||
- "Linux: If not set infos about the 64 bit agent will be retrieved. If set to B(aarch64) infos about the ARM agent will be retrieved" | ||
type: str | ||
required: false | ||
default: 64_bit | ||
choices: | ||
- 32_bit | ||
- 64_bit | ||
- aarch64 | ||
author: | ||
- "Marco Wester (@mwester117) <marco.wester@sva.de>" | ||
requirements: | ||
- "deepdiff >= 5.6" | ||
notes: | ||
- "Python module deepdiff required. Tested with version >=5.6. Lower version may work too" | ||
- "Currently only supported in single-account management consoles" | ||
''' | ||
|
||
EXAMPLES = r''' | ||
--- | ||
- name: Get info about specified package | ||
sva.sentinelone.sentinelone_agent_info: | ||
console_url: "https://XXXXX.sentinelone.net" | ||
token: "XXXXXXXXXXXXXXXXXXXXXXXXXXX" | ||
os_type: "Windows" | ||
packet_format: "msi" | ||
architecture: "64_bit" | ||
agent_version: "latest" | ||
''' | ||
|
||
RETURN = r''' | ||
--- | ||
original_message: | ||
description: Detailed infos about the requested agent package | ||
type: str | ||
returned: on success | ||
sample: >- | ||
{'accounts': [], 'createdAt': '2024-09-17T14:28:31.657142Z', 'fileExtension': '.rpm', 'fileName': 'SentinelAgent_linux_x86_64_v24_2_2_20.rpm', | ||
'fileSize': 46269381, 'id': '2041405603323138037', | ||
'link': 'https://XXXXX.sentinelone.net/web/api/v2.1/update/agent/download/2049999999991104/2041999999999999037', 'majorVersion': '24.2', | ||
'minorVersion': 'GA', 'osArch': '32/64 bit', 'osType': 'linux', 'packageType': 'Agent', 'platformType': 'linux', 'rangerVersion': null, | ||
'scopeLevel': 'global', 'sha1': '3d32d43860bc0a77926a4d8186c8427be59c1a06', 'sites': [], 'status': 'ga', 'supportedOsVersions': null, | ||
'updatedAt': '2024-09-17T14:28:31.655927Z', 'version': '24.2.2.20'} | ||
message: | ||
description: Get basic infos about the agent package | ||
type: str | ||
returned: on success | ||
sample: "Agent found: SentinelAgent_linux_x86_64_v24_2_2_20.rpm" | ||
''' | ||
|
||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib | ||
from ansible_collections.sva.sentinelone.plugins.module_utils.sentinelone.sentinelone_agent_base import SentineloneAgentBase | ||
from ansible_collections.sva.sentinelone.plugins.module_utils.sentinelone.sentinelone_base import lib_imp_errors | ||
|
||
|
||
class SentineloneAgentInfo(SentineloneAgentBase): | ||
def __init__(self, module: AnsibleModule): | ||
""" | ||
Initialization of the AgentInfo object | ||
:param module: Requires the AnsibleModule Object for parsing the parameters | ||
:type module: AnsibleModule | ||
""" | ||
|
||
# super Class | ||
super().__init__(module) | ||
|
||
|
||
def run_module(): | ||
# define available arguments/parameters a user can pass to the module | ||
module_args = dict( | ||
console_url=dict(type='str', required=True), | ||
site=dict(type='str', required=False), | ||
token=dict(type='str', required=True, no_log=True), | ||
agent_version=dict(type='str', required=False, default='latest', choices=['latest', 'latest_ea', 'custom']), | ||
custom_version=dict(type='str', required=False), | ||
os_type=dict(type='str', required=True, choices=['Linux', 'Windows']), | ||
packet_format=dict(type='str', required=True, choices=['rpm', 'deb', 'msi', 'exe']), | ||
architecture=dict(type='str', required=False, choices=['32_bit', '64_bit', 'aarch64'], default="64_bit") | ||
) | ||
|
||
module = AnsibleModule( | ||
argument_spec=module_args, | ||
required_if=[ | ||
('agent_version', 'custom', ('custom_version',)) | ||
], | ||
supports_check_mode=True | ||
) | ||
|
||
if not lib_imp_errors['has_lib']: | ||
module.fail_json(msg=missing_required_lib("DeepDiff"), exception=lib_imp_errors['lib_imp_err']) | ||
|
||
# Create AgentInfo Object | ||
agent_info_obj = SentineloneAgentInfo(module) | ||
|
||
agent_version = agent_info_obj.agent_version | ||
custom_version = agent_info_obj.custom_version | ||
os_type = agent_info_obj.os_type | ||
packet_format = agent_info_obj.packet_format | ||
architecture = agent_info_obj.architecture | ||
|
||
# Get package object from API with given parameters | ||
package_obj = agent_info_obj.get_package_obj(agent_version, custom_version, os_type, packet_format, architecture, module) | ||
|
||
changed = False | ||
original_message = package_obj | ||
basic_message = f"Agent found: {package_obj['fileName']}" | ||
|
||
result = dict( | ||
changed=changed, | ||
original_message=original_message, | ||
message=basic_message | ||
) | ||
|
||
# in the event of a successful module execution, you will want to | ||
# simple AnsibleModule.exit_json(), passing the key/value results | ||
module.exit_json(**result) | ||
|
||
|
||
def main(): | ||
run_module() | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Oops, something went wrong.