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
+ '''
1
14
from ruxit .api .base_plugin import RemoteBasePlugin
2
15
from math import floor
3
16
import requests
4
17
import logging
5
18
import time
19
+ import os
6
20
7
21
logger = logging .getLogger (__name__ )
8
22
9
23
10
24
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
+ '''
11
34
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
+ '''
12
40
logger .info ("Config: %s" , self .config )
13
41
config = kwargs ['config' ]
42
+
14
43
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
+
16
51
self .pollingInterval = int (config ['pollingInterval' ]) * 60 * 1000
17
52
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
+
18
59
self .verify_ssl = config ['verify_ssl' ]
19
60
if not self .verify_ssl :
20
61
requests .packages .urllib3 .disable_warnings ()
21
62
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
29
66
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
33
101
102
+ '''
34
103
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" ,
42
125
}
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 } " )
43
134
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 )
91
176
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
0 commit comments