Skip to content

Commit

Permalink
Merge pull request #6 from Dynatrace/V2-Adoption
Browse files Browse the repository at this point in the history
V2 adoption
  • Loading branch information
Aaron Philipose authored Jun 1, 2022
2 parents 0e0ed77 + a52aafc commit d922691
Show file tree
Hide file tree
Showing 8 changed files with 497 additions and 201 deletions.
160 changes: 160 additions & 0 deletions AuditActiveGatePlugin.py
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
125 changes: 125 additions & 0 deletions AuditEntryBaseHandler.py
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
46 changes: 46 additions & 0 deletions AuditEntryV1Handler.py
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
50 changes: 50 additions & 0 deletions AuditEntryV2Handler.py
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
Loading

0 comments on commit d922691

Please sign in to comment.