-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from Dynatrace/V2-Adoption
V2 adoption
- Loading branch information
Showing
8 changed files
with
497 additions
and
201 deletions.
There are no files selected for viewing
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,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 |
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,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 |
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,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 |
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,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 |
Oops, something went wrong.