#!/usr/bin/env python # coding=utf-8 __author__ = "TrackMe Limited" __copyright__ = "Copyright 2022-2026, TrackMe Limited, U.K." __credits__ = "TrackMe Limited, U.K." __license__ = "TrackMe Limited, all rights reserved" __version__ = "0.1.0" __maintainer__ = "TrackMe Limited, U.K." __email__ = "support@trackme-solutions.com" __status__ = "PRODUCTION" # Standard library imports import os import sys import time import json # Logging imports import logging from logging.handlers import RotatingFileHandler # Networking imports import urllib3 urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) # set splunkhome splunkhome = os.environ["SPLUNK_HOME"] # set logging filehandler = RotatingFileHandler( "%s/var/log/splunk/trackme_load_tenants_summary.log" % splunkhome, mode="a", maxBytes=10000000, backupCount=1, ) formatter = logging.Formatter( "%(asctime)s %(levelname)s %(filename)s %(funcName)s %(lineno)d %(message)s" ) logging.Formatter.converter = time.gmtime filehandler.setFormatter(formatter) log = logging.getLogger() # root logger - Good to get it only once. for hdlr in log.handlers[:]: # remove the existing file handlers if isinstance(hdlr, logging.FileHandler): log.removeHandler(hdlr) log.addHandler(filehandler) # set the new handler # set the log level to INFO, DEBUG as the default is ERROR log.setLevel(logging.INFO) # append current directory sys.path.append(os.path.dirname(os.path.abspath(__file__))) # import libs import import_declare_test # import Splunk libs from splunklib.searchcommands import ( dispatch, GeneratingCommand, Configuration, Option, validators, ) # Import trackme libs from trackme_libs import trackme_reqinfo @Configuration(distributed=False) class TrackMeTenantsStatus(GeneratingCommand): tenant_id = Option( doc=""" **Syntax:** **tenant_id=**** **Description:** Optional, the tenant identifier.""", require=False, default=None, ) output = Option( doc=""" **Syntax:** **output=**** **Description:** Optional, return the either the status per tenant/report (default), or the list of tenant the user is allowed to access to. Valid options are: status | tenants""", require=False, default="status", validate=validators.Match("output", r"^(status|tenants)$"), ) def has_user_access(self, effective_roles, record): tenant_roles_admin = ( set(record["tenant_roles_admin"]) if isinstance(record["tenant_roles_admin"], list) else set(record["tenant_roles_admin"].split(",")) ) tenant_roles_power = ( set(record["tenant_roles_power"]) if isinstance(record["tenant_roles_power"], list) else set(record["tenant_roles_power"].split(",")) ) tenant_roles_user = ( set(record["tenant_roles_user"]) if isinstance(record["tenant_roles_user"], list) else set(record["tenant_roles_user"].split(",")) ) allowed_roles = ( tenant_roles_admin | tenant_roles_user | tenant_roles_power | {"admin", "trackme_admin", "sc_admin"} ) return bool(set(effective_roles) & allowed_roles) def get_effective_roles(self, user_roles, roles_dict): effective_roles = set(user_roles) # start with user's direct roles to_check = list(user_roles) # roles to be checked for inherited roles while to_check: current_role = to_check.pop() inherited_roles = roles_dict.get(current_role, []) for inherited_role in inherited_roles: if inherited_role not in effective_roles: effective_roles.add(inherited_role) to_check.append(inherited_role) return effective_roles def generate(self, **kwargs): if self: # Get request info and set logging level reqinfo = trackme_reqinfo( self._metadata.searchinfo.session_key, self._metadata.searchinfo.splunkd_uri, ) log.setLevel(reqinfo["logging_level"]) # get current user username = self._metadata.searchinfo.username # get user info users = self.service.users # Get roles for the current user username_roles = [] for user in users: if user.name == username: username_roles = user.roles logging.info(f'username="{username}", roles="{username_roles}"') # get roles roles = self.service.roles roles_dict = {} for role in roles: imported_roles_value = role.content.get("imported_roles", []) if imported_roles_value: # Check if it has a non-empty value roles_dict[role.name] = imported_roles_value logging.debug(f"roles_dict={json.dumps(roles_dict, indent=2)}") # get effective roles, which takes into account both direct membership and inheritance effective_roles = self.get_effective_roles(username_roles, roles_dict) # Data collection collection_name = "kv_trackme_virtual_tenants" collection = self.service.kvstore[collection_name] # Define the KV query search string if self.tenant_id and self.tenant_id != "*": query_string_filter = { "tenant_id": self.tenant_id, "tenant_status": "enabled", } elif self.tenant_id and self.tenant_id == "*": query_string_filter = { "tenant_status": "enabled", } else: query_string_filter = { "tenant_status": "enabled", } query_string = {"$and": [query_string_filter]} # log debug logging.debug(f"query string={json.dumps(query_string)}") # Get the records filtered_records = [] try: records = collection.data.query(query=json.dumps(query_string)) # Loop through the records for record in records: # handle all other cases and use RBAC accordingly to the tenant # log logging.info( f'checking permissions of user="{username}" with roles="{username_roles}" for tenant_id="{record["tenant_id"]}"' ) if self.has_user_access(effective_roles, record) or username in ( "splunk-system-user" ): filtered_records.append(record) # For each record in records, get the tenant_id, component and load the status as a dict for filtered_record in filtered_records: tenant_id = filtered_record.get("tenant_id") component = filtered_record.get("component") # counter count = 0 # Simply return the tenant record filtered from RBAC if self.output == "tenants": # Add schema_version_required to the record schema_version_required = int( reqinfo.get("schema_version_required") ) filtered_record["schema_version_required"] = ( schema_version_required ) # Set the status of tenant_updated_status, if schema_version in the record is equal to schema_version_required, # the status is "updated", otherwise it is "pending" # If schema_version_required is 0 (version retrieval failed), treat all tenants as "updated" # to align with graceful degradation when DB Connect causes permission issues if schema_version_required == 0: filtered_record["tenant_updated_status"] = "updated" else: # Handle case where schema_version is missing from the record schema_version_raw = filtered_record.get("schema_version") if schema_version_raw is None: # If schema_version is missing, use "undetermined" to indicate we cannot determine the status # This is different from "pending" which implies an upgrade is in progress filtered_record["tenant_updated_status"] = "undetermined" elif int(schema_version_raw) == schema_version_required: filtered_record["tenant_updated_status"] = "updated" else: filtered_record["tenant_updated_status"] = "pending" # yield_record yield_record = {} for key, value in filtered_record.items(): if key == "_key": continue yield_record[key] = value yield_record["_time"] = time.time() yield_record["_raw"] = json.dumps(yield_record) # yield yield yield_record # Handle and return the status record elif self.output == "status": try: tenant_objects_exec_summary = json.loads( filtered_record.get("tenant_objects_exec_summary") ) # increment count += 1 # For each report, render the status summary for report in tenant_objects_exec_summary: subrecord_dict = tenant_objects_exec_summary.get(report) try: last_duration = round( float(subrecord_dict.get("last_duration")), 3 ) except Exception as e: last_duration = 0 # get last_exec last_exec = subrecord_dict.get("last_exec", None) if last_exec: # turn into a human readable format with strftime %c try: last_exec = float(last_exec) last_exec = time.strftime( "%c", time.localtime(last_exec) ) except Exception as e: pass subrecord = { "_time": time.time(), "tenant_id": tenant_id, "component": subrecord_dict.get("component"), "report": report, "earliest": subrecord_dict.get("earliest"), "latest": subrecord_dict.get("latest"), "last_status": subrecord_dict.get("last_status"), "last_exec": last_exec, "last_duration": last_duration, "last_result": subrecord_dict.get("last_result"), } subrecord["_raw"] = json.dumps(subrecord) # yield yield subrecord except Exception as e: logging.warning( f'failed to retrieve tenant_objects_exec_summary with exception="{str(e)}"' ) nonerecord = { "_time": time.time(), "tenant_id": tenant_id, "component": "none", "report": "none", "earliest": "none", "latest": "none", "last_status": "none", "last_exec": "none", "last_duration": "none", "last_result": "none", } nonerecord["_raw"] = json.dumps(nonerecord) # yield yield nonerecord # if there are no reports for this tenant, return 1 if count == 0: nonerecord = { "_time": time.time(), "tenant_id": tenant_id, "component": "none", "report": "none", "earliest": "none", "latest": "none", "last_status": "none", "last_exec": "none", "last_duration": "none", "last_result": "none", } nonerecord["_raw"] = json.dumps(nonerecord) # yield yield nonerecord except Exception as e: # yield yield { "_time": str(time.time()), "_raw": f'failed to retrieve tenant_objects_exec_summary with exception="{str(e)}"', } dispatch(TrackMeTenantsStatus, sys.argv, sys.stdin, sys.stdout, __name__)