-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNSUserKeyDifferentialImport.py
More file actions
361 lines (307 loc) · 17.5 KB
/
Copy pathNSUserKeyDifferentialImport.py
File metadata and controls
361 lines (307 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env python
# BCG NSUserKeyDifferentialImport
# Development: MacDevOps@bcg.com
# Operations: DLPTeam@bcg.com
#
# Change Log
# tong.nick@bcg.com 06-FEB-2020 Created
# tong.nick@bcg.com 11-FEB-2020 Exception Handling for Crash Protection
# Summary: Imports user key values from NetSkope to JAMF.
#
# Description: NSUserKeyDifferentialImport fetches a list of all computer IDs
# and names (jamfIdAndName) from JAMF and, for each jamfIdAndName,
# instantiates the Computer() class, resulting in a single computer object
# corresponding to each jamfIdAndName.
#
# Computer()'s initialization method fetches and assigns values to a number of
# properties that may be of interest to the you, including:
#
# jid (jamf computer_id)
# udid (jamf unique device identifier)
# name (jamf computer_name)
# email (jamf email_address)
# userkey (value from jamf extension attribute that stores the NetSkope user key)
#
# Each computer object that has an empty userkey value is then stored in the
# [computers] list, while computer objects with a non-empty userkey value are
# discarded because those do not need to re-import a userkey value.
#
# Once the [computers] list is populated with computer objects representing
# each JAMF computer record having an empty userkey, NetSkope is queried for
# a userkey using the computer object's e-mail property value. If NetSkope
# returns a userkey, the corresponding computer object is is mutated by assigning
# the returned value to the computer object's userkey property.
#
# Once each computer object is updated with a userkey (or error(s) in the absence
# of a userkey), the list is iterated one last time to post the userkeys to JAMF.
#
# Usage: Update values in the below Settings section. When testing, set the
# recordLimit to a non-zero integer to avoid iterating over thousands of records.
# Additionally, set verboseMode to True when testing in order to log to stdout.
# When running in production, set recordLimit to 0 and verboseMode to False.
import sys
import argparse
import requests
################################################################################
parser = argparse.ArgumentParser(formatter_class=argparse.RawDescriptionHelpFormatter, description=('''NSUserKeyDifferentialImport differentially imports UserKeys from NetSkope into JAMF\n& NSUserKeyDifferentialImportHelper generates nsbranding.json configuration files\nfrom the imported UserKeys.\n\nTo configure this tool, run:\n\nNSUserKeyDifferentialImport.py --configure\n\nWhen testing the tool, enable verbose mode to log to stdout and set a finite limit\nto avoid processing all computer records:\n\nNSUserKeyDifferentialImport.py --limit 100 -v\n\nWhen running the tool as a scheduled job, do not pass any options - this ensures\nthat no sensitive information will be printed to stdout while processing all records:\n\nNSUserKeyDifferentialImport.py\n\nFor more information, visit: github.bcg.com/macdevops/NSUserKeyDifferentialImport'''), epilog='Copyright (c) 2020 The Boston Consulting Group, Inc.')
parser.add_argument("-c", "--configure", help="configure this tool", action="store_true")
parser.add_argument("-v", "--verbose", help="verbosely log to stdout", action="store_true")
parser.add_argument("--limit", help="max count of userkeys to fetch (int)")
if not len(sys.argv) > 1:
parser.print_help()
args = parser.parse_args()
################################################################################
def optConfigure():
print("optConfigure()")
sys.exit("###optConfigure")
################################################################################
if args.verbose:
verboseMode = True
else:
verboseMode = False
if args.limit != None:
recordLimit = args.limit
else:
recordLimit = 0
if args.configure:
optConfigure()
sys.exit("###")
################################################################################
# Settings: Edit Variables in This Section #####################################
################################################################################
# recordLimit = 100 # 0 for no limit. Will find up to this number of empty-userkey JAMF records before stopping
# verboseMode = True # Boolean keywords are case sensitive: True or False must be capitalized
nsEnv = 'REDACTED' #
nsDomain = 'goskope.com'
nsToken = 'REDACTED'
jamfEnv = 'REDACTED' #
jamfDomain = 'bcg.com'
jamfToken = 'REDACTED=='
jamfXattr = 'netskopeUserKey' # Name of the JAMF extension attribute field that does/will contain NetSkope UserKey values
################################################################################
# End Settings: Generally, anything below this line does not need to be edited #
################################################################################
nsBaseURL = "https://%s.%s/api" % (nsEnv, nsDomain)
nsApiVersion = "v1"
nsUserConfigAPI = nsBaseURL + "/" + nsApiVersion + "/userconfig"
jamfBaseURL = "https://%s.%s/JSSResource" % (jamfEnv, jamfDomain)
jamfComputersAPI = jamfBaseURL + "/computers"
jamfComputerPropertiesByIdAPI = jamfBaseURL + "/computers/id"
jamfHeaders = {
'Accept': "application/json", # JAMF allows JSON or XML download, so using JSON for convenience
'Content-Type': "application/xml", # JAMF only allows XML upload (no JSON) as of January 2020
'Authorization': "Basic %s" % (jamfToken),
'Accept-Encoding': "gzip, deflate",
'Connection': "keep-alive",
'cache-control': "no-cache"
}
################################################################################
def getFromNetSkope(url, nparams):
try:
r = requests.request("GET", url, params=nparams, timeout=5)
r.raise_for_status()
return r
except requests.exceptions.RequestException as err:
print("CHECK NETWORK CONNECTION \n")
if verboseMode == True:
print("RequestException on getFromNetSkope(url: %s, params: %s): %s \n" % (url, nparams, err))
except requests.exceptions.HTTPError as err:
if verboseMode == True:
print("HTTPError on getFromNetSkope(url: %s, params: %s): %s \n" % (url, nparams, err))
except requests.exceptions.ConnectionError as err:
if verboseMode == True:
print("ConnectionError on getFromNetSkope(url: %s, params: %s): %s \n" % (url, nparams, err))
except requests.exceptions.Timeout as err:
if verboseMode == True:
print("Timeout on getFromNetSkope(url: %s, params: %s): %s \n" % (url, nparams, err))
except requests.exceptions.SSLError as err:
if verboseMode == True:
print("SSLError on getFromNetSkope(url: %s, headers: %s): %s \n" % (url, nparams, err))
except Exception as err:
if verboseMode == True:
print("Unrecognized Exception on getFromNetSkope(url: %s, headers: %s): %s \n" % (url, nparams, err))
################################################################################
def getFromJamf(url, jheaders):
try:
r = requests.request("GET", url, headers=jheaders, timeout=5)
r.raise_for_status()
return r
except requests.exceptions.RequestException as err:
print("CHECK NETWORK CONNECTION \n")
if verboseMode == True:
print("RequestException on getFromJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.HTTPError as err:
if verboseMode == True:
print("HTTPError on getFromJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.ConnectionError as err:
if verboseMode == True:
print("ConnectionError on getFromJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.Timeout as err:
if verboseMode == True:
print("Timeout on getFromJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.SSLError as err:
if verboseMode == True:
print("SSLError on getFromJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except Exception as err:
if verboseMode == True:
print("Unrecognized Exception on getFromJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
################################################################################
def putToJamf(url, jheaders, jdata):
# requests.request("PUT", jamfComputerPropertiesByIdAPI + "/%s/subset/extensionattributes" % (self.jid), data=xml, headers=jamfHeaders)
try:
r = requests.request("PUT", url, headers=jheaders, data=jdata, timeout=10)
r.raise_for_status()
return r
except requests.exceptions.RequestException as err:
print("CHECK NETWORK CONNECTION \n")
if verboseMode == True:
print("RequestException on putToJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.HTTPError as err:
if verboseMode == True:
print("HTTPError on putToJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.ConnectionError as err:
if verboseMode == True:
print("ConnectionError on putToJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.Timeout as err:
if verboseMode == True:
print("Timeout on putToJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except requests.exceptions.SSLError as err:
if verboseMode == True:
print("SSLError on putToJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
except Exception as err:
if verboseMode == True:
print("Unrecognized Exception on putToJamf(url: %s, headers: %s): %s \n" % (url, jheaders, err))
################################################################################
computers = []
################################################################################
class Computer:
def __init__(self, jamfIdAndName):
self.jamfComputerPropertiesByIdAPI = jamfComputerPropertiesByIdAPI
self.jamfIdAndName = jamfIdAndName
self.jid = jamfIdAndName['id']
self.name = jamfIdAndName['name']
self.jamfComputerPropertiesByIdAPIResponse = getFromJamf(jamfComputerPropertiesByIdAPI + "/%s" % (jamfIdAndName['id']), jamfHeaders)
if self.jamfComputerPropertiesByIdAPIResponse is not None:
self.jamfComputerPropertiesByIdAPIResponseJSON = self.jamfComputerPropertiesByIdAPIResponse.json()
self.udid = self.jamfComputerPropertiesByIdAPIResponseJSON['computer']['general']['udid']
self.email = self.jamfComputerPropertiesByIdAPIResponseJSON['computer']['location']['email_address']
self.errors = ''
xattrIndex=0
for xattr in self.jamfComputerPropertiesByIdAPIResponseJSON['computer']['extension_attributes']:
self.xattrName = self.jamfComputerPropertiesByIdAPIResponseJSON['computer']['extension_attributes'][xattrIndex]['name']
if self.xattrName == jamfXattr:
self.userkey = self.jamfComputerPropertiesByIdAPIResponseJSON['computer']['extension_attributes'][xattrIndex]['value']
break
xattrIndex = xattrIndex + 1
else:
if verboseMode == True:
print ("Warning: Unable to retrieve and assign properties to Computer(%s, %s), it will be discarded \n" % (jamfIdAndName['id'], jamfIdAndName['name']))
def printObjAddress(self):
print(hex(id(self)))
def fillUserKeyFromNetSkope(self):
self.nsQueryParams = {'token': nsToken, 'email': self.email, 'configtype': "agent"}
self.nsAgentUserConfigResponse = getFromNetSkope(nsUserConfigAPI, self.nsQueryParams)
if self.nsAgentUserConfigResponse is not None:
self.nsAgentUserConfigResponseJSON = self.nsAgentUserConfigResponse.json()
self.nsStatus = self.nsAgentUserConfigResponseJSON['status']
if self.nsStatus == 'success':
self.userkey = self.nsAgentUserConfigResponseJSON['data']['brandingdata']['UserKey']
self.nsErrors = ['']
else:
self.nsErrors = self.nsAgentUserConfigResponseJSON['errors']
else:
self.nsStatus = 'undefined'
self.nsErrors = ['']
return self.nsStatus, self.nsErrors
def updateJamf(self):
# At the time of this writing (Jan 2020), JAMF only supports XML upload (JSON only supported for download, not upload)
xml = '<?xml version="1.0" encoding="UTF-8" standalone="no"?><computer><extension_attributes><attribute><name>%s</name><value>%s</value></attribute></extension_attributes></computer>' % (jamfXattr, self.userkey)
self.jamfPutXattrByIdAPIResponse = putToJamf(jamfComputerPropertiesByIdAPI + "/%s/subset/extensionattributes" % (self.jid), jamfHeaders, xml)
if verboseMode == True:
print("################################################################################\n")
print("OBJECT %s\n" % (hex(id(self))))
print("Object Properties (ID, UDID, Name, Email, UserKey, Errors):\n")
print("%s, %s, %s, %s, %s, %s\n" % (self.jid, self.udid, self.name, self.email, self.userkey, self.errors))
print("API Endpoint URL:")
print(jamfComputerPropertiesByIdAPI + "/%s/subset/extensionattributes\n" % (self.jid))
print("XML Body of PUT Request:")
print("%s\n" % (xml))
print("Response Body:")
print("%s\n" % (self.jamfPutXattrByIdAPIResponse))
print("Response Status:")
if hasattr(self.jamfPutXattrByIdAPIResponse, 'status_code'):
print("%s\n" % (self.jamfPutXattrByIdAPIResponse.status_code))
else:
print("999 (jamfPutXattrByIdAPIResponse has no status_code attribute)")
################################################################################
# jamfComputersAPIResponse = requests.request("GET", jamfComputersAPI, headers=jamfHeaders)
jamfComputersAPIResponse = getFromJamf(jamfComputersAPI, jamfHeaders)
if jamfComputersAPIResponse is not None:
jamfComputersAPIResponseJSON = jamfComputersAPIResponse.json()
else:
sys.exit("Fatal: Exiting on empty response from getFromJamf(url: %s, headers: %s)" % (jamfComputersAPI, jamfHeaders))
i=0
for jamfIdAndName in jamfComputersAPIResponseJSON['computers']:
computer = Computer(jamfIdAndName)
if hasattr(computer, 'userkey') == True:
if computer.userkey == '':
computers.append(computer)
if verboseMode == True:
print ("Info: Inserted Computer(%s, %s, %s, %s, %s, %s) at computers[%s] because it does not have pre-existing nsUserKey \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors, i))
i=i+1
else:
if verboseMode == True:
print ("Info: Discarded Computer(%s, %s, %s, %s, %s, %s) because it has a pre-existing nsUserKey \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
if recordLimit > 0:
if i == recordLimit:
break
else:
if verboseMode == True:
print ("Warning: Discarded Computer(%s, %s) because Computer().__init__ was unable to fetch and assign properties \n" % (computer.jid, computer.name))
################################################################################
for computer in computers:
nsStatus, nsErrors = computer.fillUserKeyFromNetSkope()
if nsStatus == 'success':
if len(computer.userkey) > 0:
if len(computer.userkey) == 20:
if verboseMode == True:
print ("Info: NetSkope Returned UserKey of Expected Length (20) for Computer(%s, %s, %s, %s, %s, %s), which is now queued for submission to JAMF \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
else:
if verboseMode == True:
print ("Warning: NetSkope Returned UserKey of Unexpected Length (!=20) for Computer(%s, %s, %s, %s, %s, %s), which is nonetheless now queued for submission to JAMF \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
else:
computer.errors = ['nsQueriedButReturnedWithEmptyUserKey']
if verboseMode == True:
print ("Error: NetSkope Returned UserKey of Zero Length (0) for Computer(%s, %s, %s, %s, %s, %s) \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
elif nsStatus == 'error':
# nsErrors = nsAgentUserConfigResponseJSON['errors']
if 'Error Processing Request' in nsErrors:
computer.errors = ['nsQueriedButReturnedEmailNotFound']
if verboseMode == True:
print ("Error: NetSkope Returned Error for Computer(%s, %s, %s, %s, %s, %s) \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
else:
computer.errors = ['nsQueriedButReturnedWithUnrecognizedError']
if verboseMode == True:
print("Error: NetSkope Returned Unrecognized Error for Computer(%s, %s, %s, %s, %s, %s) \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
elif nsStatus == 'undefined':
print("HINT: CHECK FOR INTERMITTENT NETWORK CONNECTIVITY \n")
if verboseMode == True:
print("Error: NetSkope Did Not Return for Computer(%s, %s, %s, %s, %s, %s) \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
else:
computer.errors = ['nsQueriedButReturnedWithUnrecognizedStatus']
if verboseMode == True:
print("Error: NetSkope Returned Unrecognized Status of %s for Computer(%s, %s, %s, %s, %s, %s) \n" % (nsStatus, computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
################################################################################
for computer in computers:
if hasattr(computer, 'userkey'):
if len(computer.userkey) == 20:
computer.updateJamf()
else:
if verboseMode == True:
print("################################################################################")
print("Error: Did not update JAMF UserKey for Computer(%s, %s, %s, %s, %s, %s) because the length of the userkey value is not equal to 20 \n" % (computer.jid, computer.udid, computer.name, computer.email, computer.userkey, computer.errors))
else:
if verboseMode == True:
print("################################################################################")
print("Error: Did not update JAMF UserKey for Computer(%s, %s) because the computer.userkey attribute does not exist \n" % (computer.jid, computer.name))
################################################################################