Skip to content

Commit 194ffe2

Browse files
committed
Adding License, PGI Capability, Timestamp Readable
1 parent 33d2f75 commit 194ffe2

File tree

2 files changed

+171
-73
lines changed

2 files changed

+171
-73
lines changed

audit_activegate_plugin.py

Lines changed: 164 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,190 @@
1+
'''
2+
Copyright (C) George Teodorescu & Aaron Philipose - All Rights Reserved
3+
Unauthorized copying of this file, via any medium is strictly prohibited
4+
Proprietary and confidential
5+
Written by George Teodorescu<[email protected]> and Aaron Philipose<[email protected]>, July 2020
6+
7+
8+
Automated Configuration Audit
9+
10+
This tool tracks monitoring adjustments for monitored entities,
11+
and logs the changes back to the entities' "Event" feed as an annotation.
12+
13+
'''
114
from ruxit.api.base_plugin import RemoteBasePlugin
215
from math import floor
316
import requests
417
import logging
518
import time
19+
import os
620

721
logger = logging.getLogger(__name__)
822

923

1024
class AuditPluginRemote(RemoteBasePlugin):
25+
'''
26+
Main class for the plugin
27+
28+
@param url - Dynatrace Tenant URL
29+
@param apiToken - API Token for Dynatrace Tenant. Permissions - Event Feed (v1), Read Audit Logs, Read Monitored Entities (v2)
30+
@param pollingInterval - How often to retreive Audit Logs from server (in minutes)
31+
@param verify_ssl - Boolean to choose to validate the SSL certificate of the server
32+
33+
'''
1134
def initialize(self, **kwargs):
35+
'''
36+
Initialize the plugin with variables provided by user in the UI
37+
38+
@param config - dictionary of all parameters needed for the class (listed in class)
39+
'''
1240
logger.info("Config: %s", self.config)
1341
config = kwargs['config']
42+
1443
self.url = config['url'].strip()
15-
self.apiToken = config['apiToken'].strip()
44+
if self.url[-1] == '/':
45+
self.url = self.url[:-1]
46+
47+
self.headers = {
48+
'Authorization': 'Api-Token ' + config['apiToken'].strip(),
49+
}
50+
1651
self.pollingInterval = int(config['pollingInterval']) * 60 * 1000
1752
self.start_time = floor(time.time()*1000) - self.pollingInterval
53+
54+
55+
os.environ['TZ'] = config['timezone']
56+
time.tzset()
57+
logging.info(f" Timezone: {config['timezone']} and ENV: {os.environ['TZ']} and tzname {time.tzname}")
58+
1859
self.verify_ssl = config['verify_ssl']
1960
if not self.verify_ssl:
2061
requests.packages.urllib3.disable_warnings()
2162

22-
def query(self, **kwargs):
23-
self.end_time = floor(time.time()*1000)
24-
if self.end_time - self.start_time >= self.pollingInterval:
25-
self.run_audit()
26-
logging.info(
27-
f"AUDIT - RUN INTERVAL: START -> {self.start_time} END -> {self.end_time}")
28-
self.start_time = self.end_time + 1
63+
def make_api_request(self, http_method, endpoint, json=None):
64+
'''
65+
Make API calls with proper error handling
2966
30-
def run_audit(self):
31-
if self.url[-1] == '/':
32-
self.url = self.url[:-1]
67+
@param endpoint - endpoint for Dynatrace API call
68+
@param json - dict payload to pass as JSON body
69+
70+
@return response - response dictionary for valid API call
71+
'''
72+
while True:
73+
response = requests.request(http_method, f"{self.url}{endpoint}", json=json, headers=self.headers, verify=self.verify_ssl)
74+
if response.status_code == 429:
75+
logging.info("AUDIT - RATE LIMITED! SLEEPING...")
76+
time.sleep(response.headers['X-RateLimit-Reset']/1000000)
77+
else:
78+
break
79+
return response.json()
80+
81+
def get_audit_logs(self):
82+
'''
83+
Retrieve API logs from the tenant
84+
85+
@return audit_logs - List of changes recorded from the audit API
86+
'''
87+
audit_log_endpoint = f"/api/v2/auditlogs?filter=eventType(CREATE,UPDATE)&from={self.start_time}&to={self.end_time}&sort=timestamp"
88+
changes = self.make_api_request("GET", audit_log_endpoint)
89+
return changes['auditLogs']
90+
91+
def post_annotations(self, eventType, user, category, timestamp, entityId, patch):
92+
'''
93+
Post annotation to event feed for the provided EntityID
94+
95+
@param eventType - Type of event that triggered Audit Log (CREATE/UPDATE)
96+
@param user - User that made the action
97+
@param category - Audit Category
98+
@param timestamp - Unix Epoch time when the change was made
99+
@param entityId - Entity that was affected by the creation/update
100+
@param patch - Exact option or feature that was changed and it's old value
33101
102+
'''
34103
is_managed = True if "/e/" in self.url else False
35-
eventAPI = self.url + "/api/v1/events"
36-
auditLogAPI = self.url + \
37-
f"/api/v2/auditlogs?filter=eventType(CREATE,UPDATE)&from={self.start_time}&to={self.end_time}&sort=timestamp"
38-
payload = {}
39-
headers = {
40-
'Authorization': 'Api-Token ' + self.apiToken,
41-
'content-type': "application/json"
104+
event_endpoint = "/api/v1/events"
105+
payload = {
106+
"eventType": "CUSTOM_ANNOTATION",
107+
"start": 0,
108+
"end": 0,
109+
"timeoutMinutes": 0,
110+
"attachRules": {
111+
"entityIds": [entityId]
112+
},
113+
"customProperties": {
114+
"eventType": eventType,
115+
"User": user,
116+
"Category": category,
117+
"Timestamp": time.strftime("%a, %d %b %Y %H:%M:%S %z", time.localtime(timestamp/1000)),
118+
"entityId": entityId,
119+
"Change": patch
120+
},
121+
"source": "Automated Configuration Audit",
122+
"annotationType": "Dynatrace Configuration Change",
123+
"annotationDescription": " ",
124+
"description": "Dynatrace Configuration Change",
42125
}
126+
if is_managed:
127+
managed_domain = self.url.split(sep="/e/")[0]
128+
payload['customProperties'][
129+
'User Link'] = f"{managed_domain}/cmc#cm/users/userdetails;uuid={user}"
130+
response = self.make_api_request("POST", event_endpoint, json=payload)
131+
logging.info(
132+
f"AUDIT - MATCHED: {user} {eventType} {category} {timestamp} {entityId}")
133+
logging.info(f"AUDIT - POST RESPONSE: {response}")
43134

44-
response = requests.request(
45-
"GET", auditLogAPI, headers=headers, data=payload, verify=self.verify_ssl)
46-
47-
changes = response.json()
48-
auditLogs = changes['auditLogs']
49-
x = 0
50-
# GET audit log for config changes
51-
if len(auditLogs) > 0:
52-
for x in range(len(auditLogs)):
53-
eventType = str(auditLogs[x]['eventType'])
54-
user = str(auditLogs[x]['user'])
55-
category = str(auditLogs[x]['category'])
56-
timestamp = str(auditLogs[x]['timestamp'])
57-
entityId = str(auditLogs[x]['entityId'])
58-
patch = str(auditLogs[x]['patch'])
59-
# If entityId beings with ME_ then proceed to extract the real entityId by replacing the match with nothing
60-
if entityId.startswith("ME_") and user != "agent quotas worker":
61-
entityId = entityId.rsplit(maxsplit=1)[1]
62-
payload = {
63-
"eventType": "CUSTOM_ANNOTATION",
64-
"start": 0,
65-
"end": 0,
66-
"timeoutMinutes": 0,
67-
"attachRules": {
68-
"entityIds": [entityId]
69-
},
70-
"customProperties": {
71-
"eventType": eventType,
72-
"User": user,
73-
"Category": category,
74-
"Timestamp": timestamp,
75-
"entityId": entityId,
76-
"Change": patch
77-
},
78-
"source": "Automated Configuration Audit",
79-
"annotationType": "Dynatrace Configuration Change",
80-
"annotationDescription": " ",
81-
"description": "Dynatrace Configuration Change",
82-
}
83-
if is_managed:
84-
managed_domain = self.url.split(sep="/e/")[0]
85-
payload['customProperties'][
86-
'User Link'] = f"{managed_domain}/cmc#cm/users/userdetails;uuid={user}"
87-
response = requests.request(
88-
"POST", eventAPI, json=payload, headers=headers, verify=self.verify_ssl)
89-
logging.info(f"AUDIT - MATCHED: {user} {eventType} {category} {timestamp} {entityId}")
90-
logging.info(f"AUDIT - POST RESPONSE: {response.text}")
135+
def get_processes_from_group(self, process_group_id):
136+
'''
137+
Get all the Process Group Instances from a Process Group change
138+
139+
@param process_group_id - Process Group that needs to be investigated
140+
141+
@return pgi_list - List of Process Group Instances that belong to Process Group
142+
'''
143+
logging.info(f"Entity ID: {process_group_id}")
144+
monitored_entities_endpoint = f"/api/v2/entities/{process_group_id}?fields=toRelationships.isInstanceOf"
145+
pg_details = self.make_api_request("GET", monitored_entities_endpoint)
146+
pgi_list = []
147+
logging.info(f"PG JSON - {pg_details}")
148+
for relationship in pg_details['toRelationships']['isInstanceOf']:
149+
if relationship['type'] == "PROCESS_GROUP_INSTANCE":
150+
pgi_list.append(relationship['id'])
151+
return pgi_list
152+
153+
def process_audit_payload(self, audit_logs):
154+
'''
155+
Process audit list and trigger annotation posting for matching Monitored Entities
156+
157+
@param audit_logs - list of audit records returned from the API
158+
'''
159+
for x in range(len(audit_logs)):
160+
eventType = str(audit_logs[x]['eventType'])
161+
user = str(audit_logs[x]['user'])
162+
category = str(audit_logs[x]['category'])
163+
timestamp = int(audit_logs[x]['timestamp'])
164+
entityId = str(audit_logs[x]['entityId']).rsplit(maxsplit=1)[1]
165+
entityType = str(audit_logs[x]['entityId']).split(maxsplit=1)[0]
166+
patch = str(audit_logs[x]['patch'])
167+
# If entityId beings with ME_ then proceed to extract the real entityId by replacing the match with nothing
168+
if entityType.startswith("ME_PROCESS_GROUP:") and user != "agent quotas worker":
169+
pgi_list = self.get_processes_from_group(entityId)
170+
for pgi in pgi_list:
171+
self.post_annotations(
172+
eventType, user, category, timestamp, pgi, patch)
173+
elif entityType.startswith("ME_") and user != "agent quotas worker":
174+
self.post_annotations(
175+
eventType, user, category, timestamp, entityId, patch)
91176
else:
92-
logging.info(f"AUDIT - NOT MATCHED: {user} {eventType} {category} {timestamp} {entityId}")
93-
else:
94-
logging.info(f"AUDIT - NO RECENT CHANGES FOUND! BETWEEN {self.start_time} & {self.end_time}")
177+
logging.info(
178+
f"AUDIT - NOT MATCHED: {user} {eventType} {category} {timestamp} {entityId}")
179+
logging.info(
180+
f"AUDIT - CHANGES FOUND BETWEEN {self.start_time} & {self.end_time} = {len(audit_logs)}")
181+
182+
def query(self, **kwargs):
183+
'''
184+
Routine call from the ActiveGate
185+
'''
186+
self.end_time = floor(time.time()*1000)
187+
if self.end_time - self.start_time >= self.pollingInterval:
188+
audit_logs = self.get_audit_logs()
189+
self.process_audit_payload(audit_logs)
190+
self.start_time = self.end_time + 1

plugin.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "custom.remote.python.automated_configuration_audit",
3-
"version": "1.0.0",
3+
"version": "2.0.0",
44
"type": "python",
55
"requiredAgentVersion": "1.101.0",
66
"entity": "CUSTOM_DEVICE",
@@ -25,16 +25,18 @@
2525
"properties": [
2626
{"key": "url", "type": "String"},
2727
{"key": "apiToken", "type": "Password"},
28-
{"key": "pollingInterval", "type": "Integer"},
29-
{"key": "verify_ssl", "type": "Boolean"}
28+
{"key": "pollingInterval", "type": "Integer", "defaultValue": 1},
29+
{"key": "verify_ssl", "type": "Boolean", "defaultValue": true},
30+
{"key": "timezone", "type": "String", "defaultValue": "UTC"}
3031
],
3132
"configUI": {
3233
"displayName": "Automated Configuration Audit",
3334
"properties": [
3435
{"key": "url", "displayName": "URL of the Dynatrace Tenant", "displayOrder": 1, "displayHint": "For example: https://usg925.dynatrace-managed.com/e/c28e9814-d46d-4b11-8ba0-6f76708e384e"},
35-
{"key": "apiToken", "displayName": "API Token with access to V1 metrics and V2 Audit Logs", "displayOrder": 2, "displayHint": "For example: ABllUTJYQwKKQRWSpPIva"},
36+
{"key": "apiToken", "displayName": "API Token with V1 metrics, Audit Logs & Read Entities", "displayOrder": 2, "displayHint": "For example: ABllUTJYQwKKQRWSpPIva"},
3637
{"key": "pollingInterval", "displayName": "Polling frequency (in minutes)", "displayOrder": 3, "displayHint": "For example: 5"},
37-
{"key": "verify_ssl", "displayName": "Verify URL SSL Certicate", "displayOrder": 4}
38+
{"key": "verify_ssl", "displayName": "Verify URL SSL Certicate", "displayOrder": 4},
39+
{"key": "timezone", "displayName": "Timezone", "displayOrder": 5, "displayHint": "For example: America/Chicago"}
3840
]
3941
}
4042
}

0 commit comments

Comments
 (0)