Skip to content

Commit

Permalink
Feature agent info module (#43)
Browse files Browse the repository at this point in the history
* 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
mwester117 and Marco Wester authored Sep 25, 2024
1 parent 4a9f706 commit c8f8d43
Show file tree
Hide file tree
Showing 7 changed files with 350 additions and 163 deletions.
115 changes: 115 additions & 0 deletions plugins/module_utils/sentinelone/sentinelone_agent_base.py
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.")
4 changes: 2 additions & 2 deletions plugins/module_utils/sentinelone/sentinelone_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-

# Copyright: (c) 2023, Marco Wester <marco.wester@sva.de>
# 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
Expand Down Expand Up @@ -57,7 +57,7 @@ def __init__(self, module: AnsibleModule):
self.token = module.params["token"]
self.console_url = module.params["console_url"]
self.site_name = module.params["site_name"]
self.state = module.params["state"]
self.state = module.params.get("state", None)
self.group_names = module.params.get("groups", [])

# Get AccountID by name
Expand Down
199 changes: 199 additions & 0 deletions plugins/modules/sentinelone_agent_info.py
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()
Loading

0 comments on commit c8f8d43

Please sign in to comment.