# Copyright (C) 2005-2024 Splunk Inc. All Rights Reserved. import json import socket import os import re import base64 from splunk import BadRequest, ResourceNotFound from splunk.util import normalizeBoolean from itsi_py3 import _ from splunk.clilib.bundle_paths import make_splunkhome_path import splunk.clilib.cli_common as comm from ITOA.setup_logging import logger as itsi_logger from . import itsi_module_common as utils from .itsi_module_interface_object_manifest import object_manifest class ItsiModuleModules(object): """ Class to create, get, handle_action for ITSI module """ """ Class variables """ _metadata_base_endpoint_single_module = '/services/apps/local/%s?output_mode=json' _metadata_base_endpoint_all_modules = '/services/apps/local?search=DA-ITSI&output_mode=json&count=0' _ALL_MODULES = '-' _validation_error_messages = { 'package_prefix_mismatch': _('Module package name must start with DA-ITSI-.'), 'label_required': _('Module requires a label.'), 'description_required': _('Module requires a description.'), 'version_required': _('Module requires a version.'), 'author_required': _('Module requires an author.'), 'filename_required': _('No filename specified. Must specify a filename to download.'), 'invalid_filename': _('The filename you are requesting is an invalid SPL package.'), 'no_files_exported': _('Cannot download requested file. No downloads have been exported yet.'), 'package_not_found': _('Requested package does not exist. Either module was not created, or not exported yet.') } def __init__(self, session_key, app='SA-ITOA', owner='nobody', logger=None): """ Initialize objects @type session_key: basestring @param session_key: session key @type app: basestring @param app: current app @type owner: basestring @param owner: current user @type logger: object @param logger: logger to use @rtype: object @return: instance of the class """ self.session_key = session_key self.owner = owner self.app = app self.logger = logger if logger else itsi_logger def get(self, itsi_module, **kwargs): """ Gets all metadata about a ITSI module @type itsi_module: string @param itsi_module: ITSI module that was requested @rtype: dict @return: dictionary of ITSI module metadata """ # Set up target endpoint to make request to, and then attempt to get data if itsi_module == self._ALL_MODULES: target_endpoint = self._metadata_base_endpoint_all_modules else: target_endpoint = self._metadata_base_endpoint_single_module % itsi_module self.logger.debug('Attempting get from modules from endpoint: %s', target_endpoint) # Construct response that provides all module metadata response = utils.construct_metadata_response(target_endpoint, itsi_module, self.session_key, **kwargs) self.logger.debug('Get response for module %s: %s', itsi_module, json.dumps(response)) return response def handle_action(self, itsi_module, module_action_name, **kwargs): """ Handle custom action for ITSI module @type itsi_module: basestring @param itsi_module: the name of ITSI module @type module_action_name: basestring @param module_action_name: custom action for ITSI module @type kwargs: dict @param kwargs: extra params @rtype: dict if module_action_name is generate_package @return: - if module_action_name is generate_package, appid, url path, file path of ITSI module that is packaged. - if module_action_name is validate, return a list of validation errors/infos related to all of the object types and metadata - if module_action_name is download_package, return the data for the packaged .spl file that should be downloaded. If the file doesn't exist, return a 404 """ self._validate_module_name(itsi_module) self._validate_module_action(module_action_name) self.logger.debug('Executing action %s', module_action_name) if module_action_name == 'generate_package': return self._handle_generate_package_action(itsi_module, **kwargs) elif module_action_name == 'validate': return self._handle_validate_module(itsi_module, **kwargs) elif module_action_name == 'download_package': return self._handle_download_module(itsi_module, **kwargs) def list_contents(self, itsi_module, object_instances, **kwargs): """ List object contents in ITSI module @type itsi_module: basestring @param itsi_module: the name of ITSI module @type object_instances: list @param object_instances: objects of supported object types @type kwargs: dict @param kwargs: extra params @rtype: dict @return: { : [] } """ response = {} if type(object_instances) is list: for object_instance in object_instances: object_content = object_instance.get(itsi_module, None, **kwargs) for entry in object_content: object_type = entry['object_type'] if object_type not in response: response[object_type] = [] response[object_type].append(entry) return response def _validate_module_name(self, itsi_module): """ Validate passed ITSI module name """ if not itsi_module: msg = _('ITSI module name is not valid.') self.logger.error(msg) raise BadRequest(extendedMessages=msg) def _validate_module_action(self, module_action_name): """ Validate passed ITSI module action """ msg = '' if not module_action_name: msg = _('ITSI module action is not valid.') elif module_action_name not in ['generate_package', 'validate', 'download_package']: msg = _('ITSI module action is not supported: {}.').format(module_action_name) if msg: self.logger.error(msg) raise BadRequest(extendedMessages=msg) def _handle_validate_module(self, itsi_module, **kwargs): """ Validate the module contents @type itsi_module: basestring @param itsi_module: the name of itsi module @type kwargs: dict @param kwargs: extra params @rtype: dictionary @return: dictionary of object type to actual validation result """ all_results = { 'module': {} } module_metadata = self.get(itsi_module) module_errors = [] if not module_metadata.get('package_name').startswith(itsi_module): module_errors.append([itsi_module, itsi_module, self._validation_error_messages['package_prefix_mismatch']]) if not module_metadata.get('label'): module_errors.append([itsi_module, itsi_module, self._validation_error_messages['label_required']]) if not module_metadata.get('description'): module_errors.append([itsi_module, itsi_module, self._validation_error_messages['description_required']]) if not module_metadata.get('version'): module_errors.append([itsi_module, itsi_module, self._validation_error_messages['version_required']]) if not module_metadata.get('author'): module_errors.append([itsi_module, itsi_module, self._validation_error_messages['author_required']]) if len(module_errors): all_results['module']['errors'] = module_errors # Validate all the supported objects for object_type in list(object_manifest.keys()): if not type(object_manifest[object_type]) is list: object_instance = object_manifest[object_type](self.session_key) all_results[object_type] = object_instance.validate(itsi_module, None) self.logger.debug('Found following validation results for module %s: %s', itsi_module, all_results) return all_results def _handle_download_module(self, itsi_module, **kwargs): """ Handles the module download given a file name passed into the URL @type itsi_module: string @param itsi_module: ITSI module that is being requested @rtype: dictionary @return: Dictionary containing a single field "file_contents" with the base64 encoded file contents inside """ response_object = {} file_data = '' VALID_FILENAME_PATTERN = r'^DA-ITSI-([0-9a-zA-Z_]+)_([0-9]_?)+\.spl$' # Make sure that the filename is provided filename = kwargs.get('filename') if not filename: raise BadRequest(extendedMessages=self._validation_error_messages.get('filename_required')) # Make sure that the user is requesting a valid SPL file valid_filename = re.match(VALID_FILENAME_PATTERN, filename) if not valid_filename: raise BadRequest(extendedMessages=self._validation_error_messages.get('invalid_filename')) download_folder_path = make_splunkhome_path(['etc', 'apps', 'SA-ITOA', 'appserver', 'static', 'download']) # If path doesn't exist, or is a file, no downloads have been exported yet if not os.path.isdir(download_folder_path): raise ResourceNotFound(self._validation_error_messages.get('no_files_exported')) file_path = os.path.join(download_folder_path, filename) # If filename doesn't exist, either module not created or may not have been exported yet if not os.path.isfile(file_path): raise ResourceNotFound(extendedMessages=self._validation_error_messages.get('package_not_found')) # Open file, base 64 encode the file contents, and dump into the response object try: with open(file_path, 'r') as file: file_data = file.read() response_object['file_contents'] = base64.standard_b64encode(file_data) except Exception: raise utils.ItsiModuleError(status=500, message=_('Failed to get contents of SPL file.')) return response_object def _get_splunk_web_uri(): """ Returns Splunkweb uri """ return comm.getWebUri().replace('127.0.0.1', socket.gethostname().lower())