diff --git a/AuditActiveGatePlugin.py b/AuditActiveGatePlugin.py new file mode 100644 index 0000000..4e09070 --- /dev/null +++ b/AuditActiveGatePlugin.py @@ -0,0 +1,160 @@ +# Copyright 2022 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +''' +Automated Configuration Audit + +This tool tracks monitoring adjustments for monitored entities, +and logs the changes back to the entities' "Event" feed as an annotation. + +''' +# First-Party Imports +import re +import logging +from datetime import datetime +from math import floor +from typing import List +# Third-Party Imports +import pytz +import requests +from ruxit.api.base_plugin import RemoteBasePlugin +from RequestHandler import RequestHandler +from AuditEntryV1Handler import AuditEntryV1Handler +from AuditEntryV2Handler import AuditEntryV2Handler + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class AuditPluginRemote(RemoteBasePlugin): + """Main class for the plugin + + Plugin-Provided Args: + url (str): Dynatrace Tenant URL + apiToken (str): API Token for Dynatrace Tenant. + Permissions - Read Events(v2),Read Audit Logs,Read Monitored Entities(v2) + pollingInterval (int): How often to retreive Audit Logs from server (in minutes) + verify_ssl (bool): Boolean to choose to validate the SSL certificate of the server + Args: + RemoteBasePlugin (RemoteBasePlugin): Base Class for Dynatrace Plugin + + Returns: + None + """ + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.start_time=floor(datetime.now().timestamp()*1000) - self.pollingInterval + self.end_time=None + + def initialize(self, **kwargs): + """Initialize the plugin with variables provided by user in the UI + """ + logger.info("Config: %s", self.config) + config = kwargs['config'] + + self.url = config['url'].strip() + if self.url[-1] == '/': + self.url = self.url[:-1] + + self.headers = { + 'Authorization': 'Api-Token ' + config['apiToken'].strip(), + } + + self.pollingInterval = int(config['pollingInterval']) * 60 * 1000 + + self.timezone = pytz.timezone(config['timezone']) + self.start_time = floor(datetime.now().timestamp()*1000) - self.pollingInterval + self.end_time = None + self.verify_ssl = config['verify_ssl'] + if not self.verify_ssl: + requests.packages.urllib3.disable_warnings() # pylint: disable=no-member + + def get_audit_logs(self) -> dict: + """Pull Audit Logs From API + + Returns: + dict: Audit log entrys recorded from the audit API + """ + request_handler = RequestHandler(self.url, self. headers, self.verify_ssl) + audit_log_endpoint = "/api/v2/auditlogs?" \ + + "filter=category(\"CONFIG\")&sort=timestamp" \ + + f"&from={self.start_time}&to={self.end_time}" + changes = request_handler.get_dt_api_json(audit_log_endpoint) + return changes['auditLogs'] + + def get_api_version(self, audit_log_entry: dict) -> int: + """Identify processing method required by parsing entry for API version used + + Args: + audit_log_entry (dict): Single audit entry + + Returns: + int: API Version of call (0 if none) + """ + entity_id_entry = str(audit_log_entry['entityId']) + if re.match("^ME_\\w+\\: \\w+", entity_id_entry): + return 1 + if re.match ("[a-z\\:\\[\\]\\.]", entity_id_entry): + print (entity_id_entry, "matched API V2") + return 2 + return 0 + + def is_system_user( + self, + user: str + ) -> bool: + """Checks if user from the audit is a system user + + Args: + user (str): User string + + Returns: + bool: If user is a detected system user + """ + return bool(re.match("^\\w+ \\w+ \\w+$", user)) + + def process_audit_payload(self, audit_logs: List[dict]) -> None: + """Process audit list and trigger annotation posting for matching Monitored Entities + + Args: + audit_logs (List[dict]): list of audit records returned from the API + """ + audit_v1_entry = AuditEntryV1Handler() + audit_v2_entry = AuditEntryV2Handler() + request_handler = RequestHandler(self.url, self. headers, self.verify_ssl) + for audit_log_entry in audit_logs: + if self.is_system_user(str(audit_log_entry['user'])): + continue + api_version = self.get_api_version(audit_log_entry) + if api_version == 1: + request_params=audit_v1_entry.extract_info(audit_log_entry, request_handler) + elif api_version == 2: + request_params=audit_v2_entry.extract_info(audit_log_entry, request_handler) + else: + log_id = str(audit_log_entry['logId']) # pylint: disable=unused-variable + logger.info('[Main] %(log_id)s ENTRY NOT MATCHED') + + request_handler.post_annotations( + request_params['entityId'], + request_params['properties'] + ) + + def query(self, **kwargs): + ''' + Routine call from the ActiveGate + ''' + self.end_time = floor(datetime.now().timestamp()*1000) + if self.end_time - self.start_time >= self.pollingInterval: + audit_logs = self.get_audit_logs() + self.process_audit_payload(audit_logs) + self.start_time = self.end_time + 1 diff --git a/AuditEntryBaseHandler.py b/AuditEntryBaseHandler.py new file mode 100644 index 0000000..8f7d632 --- /dev/null +++ b/AuditEntryBaseHandler.py @@ -0,0 +1,125 @@ +# Copyright 2022 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Library for Base Audit Entry Handler +""" +import logging +from typing import List +from RequestHandler import RequestHandler # pylint: disable=unused-import + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class AuditEntryBaseHandler(): + ''' + Base Class for Audit Entry to be Processed and Pushed. + Should only be used by child classes. + ''' + def extract_info( + self, + audit_log_entry: dict, + request_handler : 'RequestHandler' # pylint: disable=unused-argument + ) -> dict: + """Extract info for annotations and processing from audit entry + + Args: + audit_log_entry (dict): singular audit log entry from audit list + request_handler (RequestHandler): Request Handler to use in case expansion is needed + + Returns: + dict: dict with properties dict nested + """ + event_type = str(audit_log_entry['eventType']) + user = str(audit_log_entry['user']) + category = str(audit_log_entry['category']) + timestamp = int(audit_log_entry['timestamp']) + patch = str(audit_log_entry['patch']) + log_id = str(audit_log_entry['logId']) + # Adding placeholder to reseverve order in dict + annotation_data = { + 'properties' : { + "eventType" : event_type, + "user" : user, + "category" : category, + "timestamp": timestamp, + "patch" : patch, + "logId": log_id, + } + } + return annotation_data + + def get_processes_from_group( + self, + process_group_id: str, + request_handler : 'RequestHandler' + ) -> List[str]: + """Get all the Process Group Instances from a Process Group change + + Args: + process_group_id (str): Process Group that needs to be investigated + request_handler (RequestHandler): Request Handler to query API + + Returns: + List[str]: List of progress group instances from progress group entity + """ + logger.info("[AuditEntryBase] Entity ID: %s", process_group_id) + monitored_entities_endpoint = \ + f"/api/v2/entities/{process_group_id}?fields=toRelationships.isInstanceOf" + pg_details = request_handler.get_dt_api_json(monitored_entities_endpoint) + pgi_list = [] + for relationship in pg_details['toRelationships']['isInstanceOf']: + if relationship['type'] == "PROCESS_GROUP_INSTANCE": + pgi_list.append(relationship['id']) + return pgi_list + + def process_group_instance_to_entity_str( + self, + pgi_list: List[str] + ) -> str: + """Takes a process group instance list returns in one string + + Args: + pgi_list (List[str]): List of progress group instances + + Returns: + str: All process groups, comma seperated + """ + all_instances_str = "" + for process_group_instance in pgi_list: + all_instances_str = f"{all_instances_str}\"{process_group_instance}\"," + if len(all_instances_str) > 0: + all_instances_str = all_instances_str[:-1] + pgi_list_str = f"{all_instances_str}" + logger.info("PGI STRING: %s", pgi_list_str) + return pgi_list_str + + def get_all_entities( + self, + entity_id: str, + request_handler: 'RequestHandler' + ) -> str: + """Checks Entity if it needs to be exploded into a list of entities + + Args: + entity_id (str): singular entity_id + request_handler (RequestHandler): Request Handler to query API + + Returns: + str: singular entity_id or list of entity_ids strung + """ + if entity_id.startswith("PROCESS_GROUP-"): + pgi_list = self.get_processes_from_group(entity_id, request_handler) + entity_id = self.process_group_instance_to_entity_str(pgi_list) + return entity_id diff --git a/AuditEntryV1Handler.py b/AuditEntryV1Handler.py new file mode 100644 index 0000000..3fc902e --- /dev/null +++ b/AuditEntryV1Handler.py @@ -0,0 +1,46 @@ +# Copyright 2022 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Processing audit entry information that is formatted in V1 style +""" +from RequestHandler import RequestHandler # pylint: disable=unused-import +from AuditEntryBaseHandler import AuditEntryBaseHandler + +class AuditEntryV1Handler(AuditEntryBaseHandler): + """Class to process V1 formatted audit log entries + + Args: + AuditEntryBaseHandler (Class): Parent Class for shared operations + """ + def extract_info(self, audit_log_entry, request_handler : 'RequestHandler'): + """Extract info for annotations and processing from audit entry + + Args: + audit_log_entry (dict): singular audit log entry from audit list + request_handler (RequestHandler): Request Handler to use in case expansion is needed + + Returns: + dict: dict with entity_id and properties dict nested + """ + annotation_data = super().extract_info(audit_log_entry, request_handler) + entity_id = str(audit_log_entry['entityId']).rsplit(maxsplit=1)[1] + entity_type = str(audit_log_entry['entityId']).split(maxsplit=1)[0] + annotation_data ['entityId'] = f"\"{entity_id}\"" + + if entity_type.startswith("ME_PROCESS_GROUP:"): + pgi_list = self.get_processes_from_group(entity_id, request_handler) + pgi_str = self.process_group_instance_to_entity_str(pgi_list) + annotation_data ['entityId'] = pgi_str + if entity_type.startswith("ME_"): + return annotation_data diff --git a/AuditEntryV2Handler.py b/AuditEntryV2Handler.py new file mode 100644 index 0000000..fdb89b6 --- /dev/null +++ b/AuditEntryV2Handler.py @@ -0,0 +1,50 @@ +# Copyright 2022 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Processing audit entry information that is formatted in V1 style +""" +import re +from RequestHandler import RequestHandler # pylint: disable=unused-import +from AuditEntryBaseHandler import AuditEntryBaseHandler + +class AuditEntryV2Handler(AuditEntryBaseHandler): + """Class to process V1 formatted audit log entries + + Args: + AuditEntryBaseHandler (Class): Parent Class for shared operations + """ + def extract_info(self, audit_log_entry, request_handler: 'RequestHandler'): + """Extract info for annotations and processing from audit entry + + Args: + audit_log_entry (dict): singular audit log entry from audit list + request_handler (RequestHandler): Request Handler to use in case expansion is needed + + Returns: + dict: dict with entity_id and properties dict nested + """ + annotation_data = super().extract_info(audit_log_entry, request_handler) + entity_regex = re.search( + "^([a-z]+\\:[a-z\\.\\-]+) \\(([A-Z0-9\\-\\_]+)\\)\\:", + str(audit_log_entry['entityId']) + ) + entity_id = entity_regex.group(2) + entity_type = entity_regex.group(1) + annotation_data ['entityId'] = f"\"{entity_id}\"" + annotation_data ['properties']['entityType'] = entity_type + if entity_id.startswith("PROCESS_GROUP-"): + pgi_list = self.get_processes_from_group(entity_id, request_handler) + pgi_str = self.process_group_instance_to_entity_str(pgi_list) + annotation_data ['entityId'] = pgi_str + return annotation_data diff --git a/README.md b/README.md index aa446c2..db0ffb5 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,14 @@ # dt-automated-config-audit 📝 With multiple Dyantrace admins managing a tenant, It can often be difficult to track accountability of Dynatrace configuration changes. The Dynatrace Automated Configuration Audit uses the flexible Dynatrace API to report configuration changes made to Dynatrace Entities and reports it into the Event feed of the entity altered. This allows for a clear visalization and accountability of changes to you Applications, Services, Processes and Hosts. This is an ActiveGate extension that only needs to sit on 1 Dynatrace ActiveGate to operate. +## What's New 💡 +Version 3.0.0 brings support for audit log that use the newer v2 format to now be posted to the log. In addition, the structure has now paved the way for a future update - Automated GitHub backups (stay tuned). Please note, the update changes the API permissions required. For version 4 released, even more scope will be needed to support newer features. + ## Prerequisites ✔️ 1) Enable *"Log all audit-related system events"* in your Dynatrace environment > - Go to Settings -> Preferences -> Data privacy and security -> Log audit events -2) Create API Token with V1 metrics, Audit Logs & Read Entities +2) Create API Token with auditLogs.read, entities.read and events.ingest permissions + NOTE: In future versions more permissions will be required, including ReadConfig and DataExport ## Installation 🚀 1) Download the latest release. @@ -19,7 +23,7 @@ With multiple Dyantrace admins managing a tenant, It can often be difficult to t ![Screenshot](https://github.com/geoteo/dt-automated-config-audit/blob/master/Automated%20Configuration%20Audit.png) ## Development ⌨️ -- An optimal development environment should use Python 3.6.6 and pipenv +- An optimal development environment should use Python 3.8 and pipenv - Dependencies needed are requests, pytz and a current plugin-sdk available from your Dynatrace environment ## Contact 🤝 diff --git a/RequestHandler.py b/RequestHandler.py new file mode 100644 index 0000000..64e61dc --- /dev/null +++ b/RequestHandler.py @@ -0,0 +1,106 @@ +# Copyright 2022 Dynatrace LLC + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Request Handler for making API calls to Dynatrace +""" +from time import sleep +import logging +import requests + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + +class RequestHandler(): + """Request Handler for making API calls to Dynatrace + """ + def __init__(self, base_url, headers, verify_ssl=True): + self.url = base_url + self.headers = headers + self.verify_ssl = verify_ssl + + def get_dt_api_json( + self, + endpoint: str, + json_payload: dict = None, + params: dict = None + ) -> dict: + """Get JSON response from DT API + + Args: + endpoint (str): Endpoint to query from Dynatrace + json_payload (dict, optional): JSON data to send. Defaults to None. + params (dict, optional): Param data to send. Defaults to None. + + Returns: + dict: JSON response from Dynatrace API endpoint + """ + response = self.make_dt_api_request("GET", endpoint, json_payload, params) + return response.json() + + def make_dt_api_request( + self, + http_method, + endpoint, + json_payload=None, + params=None + ) -> requests.Response: + ''' + Make API calls with proper error handling + + @param endpoint - endpoint for Dynatrace API call + @param json_payload - dict payload to pass as JSON body + + @return response - response dictionary for valid API call + #TODO - ADAPT DOCSTRING TO NEW FORMAT + ''' + while True: + response = requests.request( + http_method, + f"{self.url}{endpoint}", + json=json_payload, + headers=self.headers, + verify=self.verify_ssl, + params=params + ) + if response.status_code == 429: + logger.info("[RequestHandler] AUDIT - RATE LIMITED! SLEEPING...") + sleep(response.headers['X-RateLimit-Reset']/1000000) + else: + break + return response + + def post_annotations(self, entity_id: str, properties: dict) -> None: + """Post annoations to Dynatrace entity event log + + Args: + entity_id (str): Entity ID to post update + properties (dict): All info needed to post in the annotation + """ + endpoint = "/api/v2/events/ingest" + json_payload = { + "eventType": "CUSTOM_ANNOTATION", + "title" : "Automated Configuration Audit", + "timeout": 0, + "entitySelector": f"entityId ({entity_id})", + "properties": properties + } + response = self.make_dt_api_request("POST", endpoint, json_payload=json_payload) + logger.info( + "[RequestHandler] Annotation for LOG_ID: %s,ENTITY_ID:%s : %s", + json_payload['properties']['logId'], + entity_id, + response.status_code + ) + logger.debug("[RequestHandler] Requests: %s", response.request) + logger.debug("[RequestHandler] Request Text: %s", response.text) diff --git a/audit_activegate_plugin.py b/audit_activegate_plugin.py deleted file mode 100644 index 30171ca..0000000 --- a/audit_activegate_plugin.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright 2020 Dynatrace LLC - -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at - -# http://www.apache.org/licenses/LICENSE-2.0 - -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -''' -Automated Configuration Audit - -This tool tracks monitoring adjustments for monitored entities, -and logs the changes back to the entities' "Event" feed as an annotation. - -''' -from ruxit.api.base_plugin import RemoteBasePlugin -from datetime import datetime, timedelta -from math import floor -from time import sleep -import requests -import logging -import pytz - -logger = logging.getLogger(__name__) - - -class AuditPluginRemote(RemoteBasePlugin): - ''' - Main class for the plugin - - @param url - Dynatrace Tenant URL - @param apiToken - API Token for Dynatrace Tenant. Permissions - Event Feed (v1), Read Audit Logs, Read Monitored Entities (v2) - @param pollingInterval - How often to retreive Audit Logs from server (in minutes) - @param verify_ssl - Boolean to choose to validate the SSL certificate of the server - - ''' - def initialize(self, **kwargs): - ''' - Initialize the plugin with variables provided by user in the UI - - @param config - dictionary of all parameters needed for the class (listed in class) - ''' - logger.info("Config: %s", self.config) - config = kwargs['config'] - - self.url = config['url'].strip() - if self.url[-1] == '/': - self.url = self.url[:-1] - - self.headers = { - 'Authorization': 'Api-Token ' + config['apiToken'].strip(), - } - - self.pollingInterval = int(config['pollingInterval']) * 60 * 1000 - - self.timezone = pytz.timezone(config['timezone']) - self.start_time = floor(datetime.now().timestamp()*1000) - self.pollingInterval - self.verify_ssl = config['verify_ssl'] - if not self.verify_ssl: - requests.packages.urllib3.disable_warnings() - - def make_api_request(self, http_method, endpoint, json=None): - ''' - Make API calls with proper error handling - - @param endpoint - endpoint for Dynatrace API call - @param json - dict payload to pass as JSON body - - @return response - response dictionary for valid API call - ''' - while True: - response = requests.request(http_method, f"{self.url}{endpoint}", json=json, headers=self.headers, verify=self.verify_ssl) - if response.status_code == 429: - logging.info("AUDIT - RATE LIMITED! SLEEPING...") - sleep(response.headers['X-RateLimit-Reset']/1000000) - else: - break - return response.json() - - def get_audit_logs(self): - ''' - Retrieve API logs from the tenant - - @return audit_logs - List of changes recorded from the audit API - ''' - audit_log_endpoint = f"/api/v2/auditlogs?filter=eventType(CREATE,UPDATE)&from={self.start_time}&to={self.end_time}&sort=timestamp" - changes = self.make_api_request("GET", audit_log_endpoint) - return changes['auditLogs'] - - def post_annotations(self, eventType, user, category, timestamp, entityId, patch): - ''' - Post annotation to event feed for the provided EntityID - - @param eventType - Type of event that triggered Audit Log (CREATE/UPDATE) - @param user - User that made the action - @param category - Audit Category - @param timestamp - Unix Epoch time when the change was made - @param entityId - Entity that was affected by the creation/update - @param patch - Exact option or feature that was changed and it's old value - - ''' - is_managed = True if "/e/" in self.url else False - event_endpoint = "/api/v1/events" - payload = { - "eventType": "CUSTOM_ANNOTATION", - "start": 0, - "end": 0, - "timeoutMinutes": 0, - "attachRules": { - "entityIds": [entityId] - }, - "customProperties": { - "eventType": eventType, - "User": user, - "Category": category, - "Timestamp": datetime.now(tz=self.timezone).strftime("%a, %d %b %Y %H:%M:%S %z"), - "entityId": entityId, - "Change": patch - }, - "source": "Automated Configuration Audit", - "annotationType": "Dynatrace Configuration Change", - "annotationDescription": " ", - "description": "Dynatrace Configuration Change", - } - if is_managed: - managed_domain = self.url.split(sep="/e/")[0] - payload['customProperties'][ - 'User Link'] = f"{managed_domain}/cmc#cm/users/userdetails;uuid={user}" - response = self.make_api_request("POST", event_endpoint, json=payload) - logging.info( - f"AUDIT - MATCHED: {user} {eventType} {category} {timestamp} {entityId}") - logging.info(f"AUDIT - POST RESPONSE: {response}") - - def get_processes_from_group(self, process_group_id): - ''' - Get all the Process Group Instances from a Process Group change - - @param process_group_id - Process Group that needs to be investigated - - @return pgi_list - List of Process Group Instances that belong to Process Group - ''' - logging.info(f"Entity ID: {process_group_id}") - monitored_entities_endpoint = f"/api/v2/entities/{process_group_id}?fields=toRelationships.isInstanceOf" - pg_details = self.make_api_request("GET", monitored_entities_endpoint) - pgi_list = [] - logging.info(f"PG JSON - {pg_details}") - for relationship in pg_details['toRelationships']['isInstanceOf']: - if relationship['type'] == "PROCESS_GROUP_INSTANCE": - pgi_list.append(relationship['id']) - return pgi_list - - def process_audit_payload(self, audit_logs): - ''' - Process audit list and trigger annotation posting for matching Monitored Entities - - @param audit_logs - list of audit records returned from the API - ''' - for x in range(len(audit_logs)): - eventType = str(audit_logs[x]['eventType']) - user = str(audit_logs[x]['user']) - category = str(audit_logs[x]['category']) - timestamp = int(audit_logs[x]['timestamp']) - entityId = str(audit_logs[x]['entityId']).rsplit(maxsplit=1)[1] - entityType = str(audit_logs[x]['entityId']).split(maxsplit=1)[0] - patch = str(audit_logs[x]['patch']) - # If entityId beings with ME_ then proceed to extract the real entityId by replacing the match with nothing - if entityType.startswith("ME_PROCESS_GROUP:") and user != "agent quotas worker": - pgi_list = self.get_processes_from_group(entityId) - for pgi in pgi_list: - self.post_annotations( - eventType, user, category, timestamp, pgi, patch) - elif entityType.startswith("ME_") and user != "agent quotas worker": - self.post_annotations( - eventType, user, category, timestamp, entityId, patch) - else: - logging.info( - f"AUDIT - NOT MATCHED: {user} {eventType} {category} {timestamp} {entityId}") - logging.info( - f"AUDIT - CHANGES FOUND BETWEEN {self.start_time} & {self.end_time} = {len(audit_logs)}") - - def query(self, **kwargs): - ''' - Routine call from the ActiveGate - ''' - self.end_time = floor(datetime.now().timestamp()*1000) - if self.end_time - self.start_time >= self.pollingInterval: - audit_logs = self.get_audit_logs() - self.process_audit_payload(audit_logs) - self.start_time = self.end_time + 1 diff --git a/plugin.json b/plugin.json index f23dac8..8b60a0f 100644 --- a/plugin.json +++ b/plugin.json @@ -1,13 +1,13 @@ { "name": "custom.remote.python.automated_configuration_audit", - "version": "2.0.1", + "version": "3.0.0", "type": "python", "requiredAgentVersion": "1.101.0", "entity": "CUSTOM_DEVICE", "metricGroup": "tech.Automated_Configuration_Audit", "technologies": ["Automated Configuration Audit"], "source": { - "package": "audit_activegate_plugin", + "package": "AuditActiveGatePlugin", "className": "AuditPluginRemote", "install_requires": ["requests>=2.6.0", "pytz>=2020.1"], "activation": "Remote" @@ -32,8 +32,8 @@ "configUI": { "displayName": "Automated Configuration Audit", "properties": [ - {"key": "url", "displayName": "URL of the Dynatrace Tenant", "displayOrder": 1, "displayHint": "For example: https://usg925.dynatrace-managed.com/e/c28e9814-d46d-4b11-8ba0-6f76708e384e"}, - {"key": "apiToken", "displayName": "API Token with V1 metrics, Audit Logs & Read Entities", "displayOrder": 2, "displayHint": "For example: ABllUTJYQwKKQRWSpPIva"}, + {"key": "url", "displayName": "URL of the Dynatrace Tenant", "displayOrder": 1, "displayHint": "For example: https://abc123.dynatrace-managed.com/e/tenant-id-here"}, + {"key": "apiToken", "displayName": "API Token with auditLogs.read, entities.read and events.ingest", "displayOrder": 2, "displayHint": "Current Token Format: dt.PUBLICSECTION.PRIVATESECTION"}, {"key": "pollingInterval", "displayName": "Polling frequency (in minutes)", "displayOrder": 3, "displayHint": "For example: 5"}, {"key": "verify_ssl", "displayName": "Verify URL SSL Certicate", "displayOrder": 4}, {"key": "timezone", "displayName": "Timezone", "displayOrder": 5, "displayHint": "For example: America/Chicago"}