diff --git a/scripts/StagingUserAssignation.py b/scripts/StagingUserAssignation.py index 9460e90..742ca8e 100644 --- a/scripts/StagingUserAssignation.py +++ b/scripts/StagingUserAssignation.py @@ -1,45 +1,57 @@ #!/usr/bin/python3 import os -import base64 -import requests -import json import argparse -from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7 -from cryptography.hazmat.primitives import hashes, serialization +import logging +from functions import getSettings +from includes.airwatchAPI import * +from includes.GLPIAPI import * parser = argparse.ArgumentParser() -parser.add_argument("-debug", action=argparse.BooleanOptionalAction) -parser.add_argument("-force", action=argparse.BooleanOptionalAction) -parser.add_argument("-serialnumber") -parser.add_argument("-staginguser") +parser.add_argument("-sn", "--serialnumber", dest="serialnumber", type=str) +parser.add_argument("-u", "--staginguser", dest="staginguser", type=str) +parser.add_argument("-c", "--configPath", dest="configpath", type=str) +parser.add_argument("-f", "--force", dest="force", action="store_true") +parser.add_argument("-s", "--silent", dest="silent", action="store_true") +parser.add_argument("-v", "--verbose", dest="debug", action="store_true") args = parser.parse_args() -settingsDefault = { - "airwatchServer":"https://airwatchServer", - "airwatchAPIKey":"APIKEY", - "airwatchAuthMethod":"CMSURL", - "airwatchCertPath":"/path/to/cert", - "airwatchCertPass":"certPassword", - "airwatchAPIUser":"UserAPI", - "airwatchAPIPassword":"PasswordUserAPI", - "glpiServer":"http://127.0.0.1/glpi", - "glpiAppToken":"GLPIAppToken", - "glpiUserToken":"GLPIUserToken", - "stagingUser":"staging-pr", - "userAgent":"Airwatch Synchronizer" -} - -settings = None - -if(not os.path.isfile("./settings.json")): - f = open("./settings.json", "w") - f.write(json.dumps(settingsDefault, indent=4)) - f.close() - exit(1) +# Récupération des informations du fichier de configuration +if(args.configpath != None and args.configpath != ''): + settings = getSettings(args.configpath) else: - with open("./settings.json", "r") as f: - settings = json.load(f) + settings = getSettings("./conf/settings.conf") + +#=========== Configuration des logs ===========# + +logger = logging.getLogger(__name__) + +if(args.debug or settings["LOGS"]["Debug"]): + debug = True + logginglevel = logging.DEBUG +else: + logginglevel = logging.INFO + +logger.setLevel(logginglevel) + +formatter = logging.Formatter(fmt='%(asctime)s | %(levelname)s: %(message)s', datefmt='%Y/%m/%d %H:%M:%S') + +# handler pour log dans un fichier +if(settings["LOGS"]["Enabled"]): + if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""): + fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}stagingUserAssignation.log") + else: + fileHandler = logging.FileHandler('./logs/stagingUserAssignation.log') + fileHandler.setLevel(logginglevel) + fileHandler.setFormatter(formatter) + logger.addHandler(fileHandler) + +# handler pour log dans la console +if(not args.silent): + consoleHandler = logging.StreamHandler() + consoleHandler.setLevel(logginglevel) + consoleHandler.setFormatter(formatter) + logger.addHandler(consoleHandler) #======== Paramètres du script ========# @@ -48,203 +60,89 @@ lockFile = './airwatchStagingUserAssignation.lock' debug=args.debug -# Informations du serveur Airwatch -airwatchServer = settings["airwatchServer"] -airwatchAPIKey = settings["airwatchAPIKey"] -airwatchAuthMethod = settings["airwatchAuthMethod"] -airwatchAPIUser = None -airwatchAPIPassword = None -airwatchCertPath = None -airwatchCertPass = None -if(airwatchAuthMethod == 'password'): - airwatchAPIUser = settings["airwatchAPIUser"] - airwatchAPIPassword = settings["airwatchAPIPassword"] -elif(airwatchAuthMethod == 'CMSURL'): - airwatchCertPath = settings["airwatchCertPath"] - airwatchCertPass = settings["airwatchCertPass"] -stagingUser = settings["stagingUser"] +stagingUser = settings["AIRWATCH"]["StagingUser"] if(args.staginguser != None): stagingUser = args.staginguser -# Informations du serveur GLPI -GLPIServer = settings["glpiServer"] -GLPIAppToken = settings["glpiAppToken"] -GLPIUserToken = settings["glpiUserToken"] - # ====================================== # -def getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri, User=None, password=None, CertPath=None, CertPassword=None): - if(airwatchAuthMethod == "password"): - airwatchAPIUserToken = base64.b64encode(f"{airwatchAPIUser}:{airwatchAPIPassword}".encode('ascii')).decode("ascii") - - return { - "Authorization": f"Basic {airwatchAPIUserToken}", - "aw-tenant-code": airwatchAPIKey, - "Accept": "application/json" - } - else: - signing_data = uri.split('?')[0] - with open(CertPath, 'rb') as certfile: - cert = certfile.read() - key, certificate, additional_certs = pkcs12.load_key_and_certificates(cert, CertPassword.encode()) - options = [pkcs7.PKCS7Options.DetachedSignature] - signed_data = pkcs7.PKCS7SignatureBuilder().set_data(signing_data.encode("UTF-8")).add_signer(certificate, key, hashes.SHA256()).sign(serialization.Encoding.DER, options) - signed_data_b64 = base64.b64encode(signed_data).decode() - - return { - "Authorization": f"CMSURL'1 {signed_data_b64}", - "aw-tenant-code": airwatchAPIKey, - "Accept": "application/json" - } - # Vérification de la présence du verrou avant de continuer if(os.path.isfile(lockFile) and not args.force): - if(debug): - print('Lock file is present, exiting...') + logger.debug('Lock file exists, exiting...') exit(0) else: open(lockFile, "w").close() -# Adresse de recherche des appareils filtré sur l'utilisateur de staging -# avec limite de 500 appareils par page (limite max de l'API) -airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?user={stagingUser}&pagesize=500&page=" - -airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) - -# Page de départ pour la recherche -pageNumber = 0 - -# Initialisation de la variable devices qui va stocker l'ensemble des appareils trouvés -devices = [] - -uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}" -if(debug): - print(f"Uri for device search on airwatch : {uri}") -result = requests.get(uri, headers=airwatchHeaders) - -if(debug): - print(f"Result of request : {result}") - -# On vérifie qu'on a bien un retour OK pour la requête API -if(result.status_code != 200): - # Suppression du verrou - os.remove(lockFile) - exit(0) - -result = result.json() - -# On fait une requête pour chaque page en fonction tant qu'on a pas atteint le nombre -# d'appareils trouvé dans la première requête -while(len(devices) != result["Total"]): - uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}" - result = requests.get(uri, headers=airwatchHeaders).json() - devices += result["Devices"] - pageNumber += 1 - - -# Adresse d'initalisation de l'api GLPI -GLPIAPIInitUri = '/apirest.php/initSession/' - -GLPIHeaders = { - 'Content-Type': 'application/json', - "Authorization": f"user_token {GLPIUserToken}", - "App-Token": GLPIAppToken -} - -# Récupération d'un token de session -uri = f"{GLPIServer}{GLPIAPIInitUri}" -result = requests.get(uri, headers=GLPIHeaders) - -if(debug): - print(f"GLPI api access : {result}") - -if(result.status_code != 200): - # Suppression du verrou +# Initialisation de l'api Airwatch +try: + airwatch = AirwatchAPI(settings) + logger.info("Airwatch server connection succeeded") +except requests.exceptions.ConnectionError as error: + logger.critical(f"Connection to Airwatch server failed : {error}") os.remove(lockFile) exit(1) -GLPISessionToken = result.json()["session_token"] +# Recherche des appareils filtré sur l'utilisateur de staging +devices = airwatch.GetDevices(stagingUser) -if(debug): - print(f"GLPI session Token: {GLPISessionToken}") +if(devices == None): + logger.info(f"No device found with staging user ({stagingUser}), exiting...") + os.remove(lockFile) + exit(0) +else: + logger.info(f"{len(devices)} devices found with staging user ({stagingUser})") -# Changement des headers pour remplacer l'user token par le token de session -GLPIHeaders = { - 'Content-Type': 'application/json', - "Session-Token": GLPISessionToken, - "App-Token": GLPIAppToken -} +# Initialisation de l'api GLPI +try: + glpiapi = GLPIAPI(settings) + logger.info("GLPI server connection succeeded") +except requests.exceptions.ConnectionError as error: + logger.critical(f"Connection to GLPI server failed : {error}") + os.remove(lockFile) + exit(1) -# Adresse de recherche des appareils présents dans ordinateurs sur GLPI -GLPIAPISearchComputer = '/apirest.php/search/computer?' for device in devices: - if(device["EnrollmentStatus"] != 'Enrolled'): + if(device.EnrollmentStatus != 'Enrolled'): + logger.error(f"Device with id {device.Id} not enrolled, should it be deleted ?") continue - if(args.serialnumber != None and device["SerialNumber"] != args.serialnumber): + if(args.serialnumber != None and device.SerialNumber != args.serialnumber): continue - - if(device["Imei"] != ''): - if(debug): - print(f"Imei = {device['Imei']}") - # Recherche des appareils en fonction du numéro de série ou de l'imei - # l'imei pouvant être dans le champ numéro de série ou les champs imei custom - search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device["SerialNumber"]}$'\ - f'&criteria[1][link]=OR&criteria[1][field]=5&criteria[1][searchtype]=contains&criteria[1][value]=^{device["Imei"]}$'\ - f'&criteria[2][link]=OR&criteria[2][field]=76667&criteria[2][searchtype]=contains&criteria[2][value]=^{device["Imei"]}$'\ - f'&criteria[3][link]=OR&criteria[3][field]=76670&criteria[3][searchtype]=contains&criteria[3][value]=^{device["Imei"]}$' - else: - # Recherche des appareils en fonction du numéro de série seulement - search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device["SerialNumber"]}$' - + if(debug): - print(f"Serial Number = {device['SerialNumber']}") + logger.debug(f"Serial Number = {device.SerialNumber}") + + deviceID, data, deviceCount = glpiapi.GetDevice(device) - searchUri = f"{GLPIServer}{GLPIAPISearchComputer}{search_parameter}" - if(debug): - print(f"searchURI = {searchUri}") - search = requests.get(searchUri, headers=GLPIHeaders) - - - # On ne gère pas pour l'instant d'autres code que le code 200 - # voir en fonction des codes erreurs retournés par le serveur - if(search.status_code != 200): - break - - search = search.json() - - if(search["totalcount"] == 1): + if(deviceCount == 1): # Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil - for device_id, data in search["data"].items(): - device_user = search["data"][device_id]["70"] - if(debug): - print(f"user on device in GLPI : {device_user}") + + device_user = data["70"] + + logger.info(f"Found device {device.Id} in GLPI with id = {deviceID}") + + logger.debug(f"user on device in GLPI : {device_user}") + # Vérification que l'appareil est associé à un utilisateur dans GLPI if(device_user != None): - # Récupération de l'utilisateur sur Magenta - cmdURI = f'/API/system/users/search?username={device_user}' - airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=cmdURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) - uri = f"{airwatchServer}{cmdURI}" - if(debug): - print(f"Airwatch user search uri : {uri}") - user = requests.get(uri, headers=airwatchHeaders) + # Récupération de l'utilisateur sur Airwatch + airwatchUser = airwatch.GetUser(device_user) - # On ne gère pas pour l'instant d'autres code que le code 200 - # voir en fonction des codes erreurs retournés par le serveur - if(user.status_code != 200): - break - - user = user.json() - # Changement de l'utilisateur assigné sur l'appareil dans Magenta - cmdURI = f'/API/mdm/devices/{device["Id"]["Value"]}/enrollmentuser/{user["Users"][0]["Id"]["Value"]}' - patchUri = f'{airwatchServer}{cmdURI}' - airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=cmdURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) - if(debug): - print(f"patchUri = {patchUri}") - requests.patch(patchUri, headers=airwatchHeaders) + if(airwatchUser == None): + logger.error(f"User {device_user} not found in Airwatch") + continue + logger.info(f"Assigning device with id {device.Id} to user {device_user} (id={airwatchUser.Id}) in Airwatch") + result = airwatch.SetDeviceUser(device, airwatchUser) + else: + logger.warning(f"Device with id {device.Id} is not assigned to any user in GLPI, skipping the device") + elif(deviceCount > 1): + logger.info(f"More than one entry found in GLPI for device with id {device.Id}") + else: + logger.error(f"Device {device.Id} with serialnumber {device.SerialNumber} not found in GLPI (in trash bin ?)") + # Suppression du verrou os.remove(lockFile) diff --git a/scripts/conf/settings.dist.conf b/scripts/conf/settings.dist.conf new file mode 100644 index 0000000..bf1d444 --- /dev/null +++ b/scripts/conf/settings.dist.conf @@ -0,0 +1,28 @@ + +[AIRWATCH] +Server = "https://airwatchServer" +APIKey = "APIKEY" + +# Méthode d'authentification (CMSURL or PASSWORD) +# CMSURL permet l'authentification avec un certificat utilisateur +# PASSWORD permet l'authentification avec un nom d'utilisateur et un mot de passe (APIUser, APIPassword) +AuthenticationMethod = "CMSURL" +CertificatePath = "/path/to/cert" +CertificatePassword = "12345" +APIUser = "UserAPI" +APIPassword = "PasswordUserAPI" + +# Utilisateur de staging que l'on va remplacer par l'utilisateur trouvé dans GLPI +StagingUser = "staging-pr" + +[GLPI] +Server = "http://127.0.0.1/glpi" +AppToken = "GLPIAppToken" +UserToken = "GLPIUserToken" +# User agent qui sera visible sur GLPI lors de la synchronisation +UserAgent = "Airwatch Synchronizer" + +[LOGS] +Enabled = true +Path = "./logs/" +Debug = false diff --git a/scripts/functions.py b/scripts/functions.py new file mode 100644 index 0000000..87ea9d7 --- /dev/null +++ b/scripts/functions.py @@ -0,0 +1,43 @@ +import os +import toml + +def getSettings(settingsPath): + settingsDefault =""" +[AIRWATCH] +Server = "https://airwatchServer" +APIKey = "APIKEY" + +# Méthode d'authentification (CMSURL or PASSWORD) +# CMSURL permet l'authentification avec un certificat utilisateur +# PASSWORD permet l'authentification avec un nom d'utilisateur et un mot de passe (APIUser, APIPassword) +AuthenticationMethod = "CMSURL" +CertificatePath = "/path/to/cert" +CertificatePassword = "12345" +APIUser = "UserAPI" +APIPassword = "PasswordUserAPI" + +# Utilisateur de staging que l'on va remplacer par l'utilisateur trouvé dans GLPI +StagingUser = "staging-pr" + +[GLPI] +Server = "http://127.0.0.1/glpi" +AppToken = "GLPIAppToken" +UserToken = "GLPIUserToken" +# User agent qui sera visible sur GLPI lors de la synchronisation +UserAgent = "Airwatch Synchronizer" + +[LOGS] +Enabled = true +Path = "./logs/" +Debug = false +""" + + settings = None + + if(not os.path.isfile(settingsPath)): + f = open(settingsPath, "w") + f.write(settingsDefault) + f.close() + with open(settingsPath, "r") as f: + settings = toml.load(f) + return settings diff --git a/scripts/includes/GLPIAPI.py b/scripts/includes/GLPIAPI.py new file mode 100644 index 0000000..f2b57e4 --- /dev/null +++ b/scripts/includes/GLPIAPI.py @@ -0,0 +1,210 @@ +import requests +import json +from datetime import datetime + +class GLPIAPI: + def __init__(self, settings): + self.Server = settings["GLPI"]["Server"] + self.AppToken = settings["GLPI"]["AppToken"] + self.UserToken = settings["GLPI"]["UserToken"] + self.UserAgent = settings["GLPI"]["UserAgent"] + self.SessionToken = None + self.StatusCode = None + self.Headers = None + self.InitConnection() + + def InitConnection(self): + initURI = '/apirest.php/initSession/' + GLPIHeaders = { + 'Content-Type': 'application/json', + "Authorization": f"user_token {self.UserToken}", + "App-Token": self.AppToken + } + + # Récupération d'un token de session + uri = f"{self.Server}{initURI}" + result = requests.get(uri, headers=GLPIHeaders) + self.StatusCode = result.status_code + if(result.status_code == 200): + self.SessionToken = result.json()["session_token"] + self.Headers = { + 'Content-Type': 'application/json', + "Session-Token": self.SessionToken, + "App-Token": self.AppToken + } + + def GetDevice(self, device): + if(device.Imei != ''): + # Recherche des appareils en fonction du numéro de série ou de l'imei + # l'imei pouvant être dans le champ numéro de série ou les champs imei custom + search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device.SerialNumber}$'\ + f'&criteria[1][link]=OR&criteria[1][field]=5&criteria[1][searchtype]=contains&criteria[1][value]=^{device.Imei}$'\ + f'&criteria[2][link]=OR&criteria[2][field]=76667&criteria[2][searchtype]=contains&criteria[2][value]=^{device.Imei}$'\ + f'&criteria[3][link]=OR&criteria[3][field]=76670&criteria[3][searchtype]=contains&criteria[3][value]=^{device.Imei}$' + else: + # Recherche des appareils en fonction du numéro de série seulement + search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device.SerialNumber}$' + + searchUri = f"{self.Server}/apirest.php/search/computer?{search_parameter}" + search = requests.get(searchUri, headers=self.Headers) + if(search.status_code == 200): + search = search.json() + if(search["totalcount"] == 1): + deviceID = list(search["data"].keys())[0] + data = search["data"][deviceID] + + return deviceID, data, search["totalcount"] + elif(search["totalcount"] > 1): + return deviceID, search["data"], search["totalcount"] + else: + return None, None, 0 + return None, None, None + + def UpdateInventory(self, inventory): + headers = { + "Content-Type":"Application/x-compress", + "user-agent":self.UserAgent + } + return requests.post(self.Server, headers=headers, json=inventory) + + def UpdateSerialNumber(self, deviceid, serialnumber): + + body = { + "input" : { + "id" : deviceid, + "serial" : serialnumber + } + } + uri = f"{self.Server}/apirest.php/Computer/" + return requests.put(uri, headers=self.Headers, json=body) + + def CreateInventoryForAirwatchDevice(self, device, deviceName, apps=None): + platforms = { + 2:"Apple iOS", + 5:"Android", + 12:"Windows Desktop" + } + + if(device.PlatformId in platforms.keys()): + platformName = platforms[device.PlatformId] + else: + platformName = "Unknown" + + processorArchs = { + 0:{ + "osArch":"arm64", + "softwareArch":"arm64" + }, + 9:{ + "osArch":"64-bit", + "softwareArch":"x86_64" + } + } + if(device.Arch in processorArchs.keys()): + osArch = processorArchs[device.Arch]["osArch"] + softwareArch = processorArchs[device.Arch]["softwareArch"] + else: + osArch = "Unknown" + softwareArch = "Unknown" + + logDate = datetime.strptime(device.LastSeen, "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d %H:%M:%S") + + inventory = GLPIInventory(logdate=logDate, versionclient=self.UserAgent, tag=device.Group, deviceid=f"{deviceName} - {device.SerialNumber}", itemtype="Computer") + inventory.SetOperatingSystem(platformName, device.OS, osArch) + inventory.SetHardware(deviceName, device.Uuid, device.TotalMemory) + inventory.AddUser(device.User) + + if(apps != None): + for app in apps: + if(app.Status != "Installed"): + continue + + install_date = datetime.strptime(app.InstallDate, "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d") + + if(install_date == "1-01-01"): + inventory.AddSoftware(app.Name, app.Version, app.Size, softwareArch, app.Guid) + else: + inventory.AddSoftware(app.Name, app.Version, app.Size, softwareArch, app.Guid, install_date) + + return inventory + +class GLPIInventory: + def __init__(self, logdate=None, versionclient=None, tag=None, deviceid=None, itemtype=None): + self.logdate = logdate + self.versionclient = versionclient + self.users = [] + self.operatingsystem = {} + self.softwares = [] + self.hardware = {} + self.tag = tag + self.deviceId = deviceid + self.itemType = itemtype + + + def AddUser(self, user): + self.users += [{ + "login": user + }] + + def DelUser(self, user): + for i in range(0, len(self.users)): + if(self.users[i]["login"] == user): + del self.users[i] + + def AddSoftware(self, name, version, filesize, arch, guid, install_date=None): + if(install_date == None): + self.softwares += [{ + "name": name, + "guid": guid, + "version": version, + "filesize": filesize, + "arch": arch + }] + else: + self.softwares += [{ + "name": name, + "guid": guid, + "version": version, + "install_date": install_date, + "filesize": filesize, + "arch": arch + }] + + def DelSoftware(self, guid): + for i in range(0, len(self.softwares)): + if(self.softwares[i]["guid"] == guid): + del self.softwares[i] + + def SetHardware(self, name, uuid, memory): + self.hardware = { + "name": name, + "uuid": uuid, + "memory": memory + } + + def SetOperatingSystem(self, name, version, arch): + self.operatingsystem = { + "name": name, + "version": version, + "full_name": f"{name} {version}", + "arch": arch + } + + def Json(self): + inventory = { + "action": "inventory", + "content":{ + "accesslog":{ + "logdate": self.logdate + }, + "versionclient": self.versionclient, + "users": self.users, + "operatingsystem": self.operatingsystem, + "softwares": self.softwares, + "hardware": self.hardware + }, + "tag": self.tag, + "deviceid": self.deviceId, + "itemtype": self.itemType + } + return json.dumps(inventory) diff --git a/scripts/includes/__init__.py b/scripts/includes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/airwatchAPI.py b/scripts/includes/airwatchAPI.py similarity index 86% rename from scripts/airwatchAPI.py rename to scripts/includes/airwatchAPI.py index 03e7a8d..b314e7c 100644 --- a/scripts/airwatchAPI.py +++ b/scripts/includes/airwatchAPI.py @@ -86,7 +86,7 @@ class AirwatchAPI: return None def ResetDEPProfiles(self, groupUuid): - cmdURI = f"/API/mdm/dep/groups/156147ba-3086-4ddc-8326-a6b9b8bf335a/devices" + cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices" uri = f"{self.Server}{cmdURI}" airwatchHeaders = self.GetHeaders(cmdURI) result = requests.get(uri, headers=airwatchHeaders) @@ -101,6 +101,32 @@ class AirwatchAPI: return True return False + def GetEnrollmentTokens(self, groupUuid, deviceType=2): + cmdURI = f"/API/mdm/groups/{groupUuid}/enrollment-tokens?device_type={deviceType}&page_size=500" + uri = f"{self.Server}{cmdURI}" + airwatchHeaders = self.GetHeaders(cmdURI) + result = requests.get(uri, headers=airwatchHeaders) + return result.json()["tokens"] + + def UpdateUserOnEnrollmentTokens(self, groupUuid,user, serialnumber): + body = { + "registration_type": "REGISTER_DEVICE", + "device_registration_record": { + "user_uuid": user.Uuid, + "friendly_name": serialnumber, + "ownership_type": "CORPORATE_DEDICATED", + "serial_number": serialnumber, + "to_email_address": user.Email, + "message_type": 0 + } + } + cmdURI = f"/API/mdm/groups/{groupUuid}/enrollment-tokens" + airwatchHeaders = self.GetHeaders(cmdURI) + airwatchHeaders["Accept"] = "application/json;version=2" + uri = f"{self.Server}{cmdURI}" + print(uri) + return requests.post(uri, headers=airwatchHeaders, json=body) + def SyncDEPDevices(self, groupUuid): cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices?action=sync" uri = f"{self.Server}{cmdURI}" diff --git a/scripts/settings.json b/scripts/settings.json deleted file mode 100644 index dbc0ea8..0000000 --- a/scripts/settings.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "airwatchServer": "https://airwatchServer", - "airwatchAPIKey": "APIKEY", - "airwatchAuthMethod": "CMSURL", - "airwatchCertPath": "/path/to/cert", - "airwatchCertPass": "certPassword", - "airwatchAPIUser": "UserAPI", - "airwatchAPIPassword": "PasswordUserAPI", - "glpiServer": "http://127.0.0.1/glpi", - "glpiAppToken": "GLPIAppToken", - "glpiUserToken": "GLPIUserToken", - "stagingUser": "staging-pr", - "userAgent": "Airwatch Synchronizer" -} \ No newline at end of file diff --git a/scripts/syncGLPI.py b/scripts/syncGLPI.py index fe71337..585eae4 100644 --- a/scripts/syncGLPI.py +++ b/scripts/syncGLPI.py @@ -1,78 +1,75 @@ #!/usr/bin/python3 - import os -import base64 -import requests -import json import argparse -from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7 -from cryptography.hazmat.primitives import hashes, serialization +import logging from datetime import datetime +from functions import getSettings +from includes.airwatchAPI import * +from includes.GLPIAPI import * parser = argparse.ArgumentParser() -parser.add_argument("-debug", action=argparse.BooleanOptionalAction) -parser.add_argument("-searchFilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"]) -parser.add_argument("-searchValue", type=str) -parser.add_argument("-force", action=argparse.BooleanOptionalAction) +parser.add_argument("-sF", "--searchFilter", dest="searchfilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"]) +parser.add_argument("-sV","--searchValue", dest="searchvalue", type=str) +parser.add_argument("-c", "--configPath", dest="configpath", type=str) +parser.add_argument("-f", "--force", dest="force", action="store_true") +parser.add_argument("-s", "--silent", dest="silent", action="store_true") +parser.add_argument("-v", "--verbose", dest="debug", action="store_true") args = parser.parse_args() -settingsDefault = { - "airwatchServer":"https://airwatchServer", - "airwatchAPIKey":"APIKEY", - "airwatchAuthMethod":"CMSURL", - "airwatchCertPath":"/path/to/cert", - "airwatchCertPass":"certPassword", - "airwatchAPIUser":"UserAPI", - "airwatchAPIPassword":"PasswordUserAPI", - "glpiServer":"http://127.0.0.1/glpi", - "glpiAppToken":"GLPIAppToken", - "glpiUserToken":"GLPIUserToken", - "stagingUser":"staging-pr", - "userAgent":"Airwatch Synchronizer" -} -settings = None - -if(not os.path.isfile("./settings.json")): - f = open("./settings.json", "w") - f.write(json.dumps(settingsDefault, indent=4)) - f.close() - exit(1) +# Récupération des informations du fichier de configuration +if(args.configpath != None and args.configpath != ''): + settings = getSettings(args.configpath) else: - with open("./settings.json", "r") as f: - settings = json.load(f) + settings = getSettings("./conf/settings.conf") + +#=========== Configuration des logs ===========# + +logger = logging.getLogger(__name__) + +if(args.debug or settings["LOGS"]["Debug"]): + logginglevel = logging.DEBUG +else: + logginglevel = logging.INFO + +logger.setLevel(logginglevel) + +formatter = logging.Formatter(fmt='%(asctime)s | %(levelname)s: %(message)s', datefmt='%Y/%m/%d %H:%M:%S') + +# handler pour log dans un fichier +if(settings["LOGS"]["Enabled"]): + if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""): + fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI.log") + else: + fileHandler = logging.FileHandler('./logs/syncGLPI.log') + fileHandler.setLevel(logginglevel) + fileHandler.setFormatter(formatter) + logger.addHandler(fileHandler) + +# handler pour log dans la console +if(not args.silent): + consoleHandler = logging.StreamHandler() + consoleHandler.setLevel(logginglevel) + consoleHandler.setFormatter(formatter) + logger.addHandler(consoleHandler) #======== Paramètres du script ========# # Emplacement du verrou lockFile = './airwatchSyncGLPI.lock' -debug=args.debug +logger.debug(f"============ Settings ============") +logger.debug(f"Airwatch server: {settings['AIRWATCH']['Server']}") +logger.debug(f"Authentication method : {settings['AIRWATCH']['AuthenticationMethod']}") +logger.debug(f"Staging user: {settings['AIRWATCH']['StagingUser']}") +logger.debug(f"GLPI server: {settings['GLPI']['Server']}") +logger.debug(f"UserAgent: {settings['GLPI']['UserAgent']}") -# Informations du serveur Airwatch -airwatchServer = settings["airwatchServer"] -airwatchAPIKey = settings["airwatchAPIKey"] -airwatchAuthMethod = settings["airwatchAuthMethod"] -airwatchAPIUser = None -airwatchAPIPassword = None -airwatchCertPath = None -airwatchCertPass = None -if(airwatchAuthMethod == 'password'): - airwatchAPIUser = settings["airwatchAPIUser"] - airwatchAPIPassword = settings["airwatchAPIPassword"] -else: - airwatchCertPath = settings["airwatchCertPath"] - airwatchCertPass = settings["airwatchCertPass"] - -# Informations du serveur GLPI -GLPIServer = settings["glpiServer"] -GLPIAppToken = settings["glpiAppToken"] -GLPIUserToken = settings["glpiUserToken"] # Filtres -searchFilter = args.searchFilter -searchValue = args.searchValue +searchFilter = args.searchfilter +searchValue = args.searchvalue # Platform exclusion (12 = computer) platformFilterEnabled = True @@ -80,100 +77,54 @@ platformFilterOut = [12] # ====================================== # -def getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri, User=None, password=None, CertPath=None, CertPassword=None): - if(airwatchAuthMethod == "password"): - airwatchAPIUserToken = base64.b64encode(f"{airwatchAPIUser}:{airwatchAPIPassword}".encode('ascii')).decode("ascii") - - return { - "Authorization": f"Basic {airwatchAPIUserToken}", - "aw-tenant-code": airwatchAPIKey, - "Accept": "application/json" - } - else: - signing_data = uri.split('?')[0] - with open(CertPath, 'rb') as certfile: - cert = certfile.read() - key, certificate, additional_certs = pkcs12.load_key_and_certificates(cert, CertPassword.encode()) - options = [pkcs7.PKCS7Options.DetachedSignature] - signed_data = pkcs7.PKCS7SignatureBuilder().set_data(signing_data.encode("UTF-8")).add_signer(certificate, key, hashes.SHA256()).sign(serialization.Encoding.DER, options) - signed_data_b64 = base64.b64encode(signed_data).decode() - - return { - "Authorization": f"CMSURL'1 {signed_data_b64}", - "aw-tenant-code": airwatchAPIKey, - "Accept": "application/json" - } - - # Vérification de la présence du verrou avant de continuer if(os.path.isfile(lockFile) and not args.force): - if(debug): - print('Lock file is present, exiting...') + logger.debug('Lock file exists, exiting...') exit(0) else: open(lockFile, "w").close() + +logger.info("========= Synchronization started =========") - -# Adresse de recherche des appareils -# avec limite de 500 appareils par page (limite max de l'API) -airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?pagesize=500&page=" -airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) - -# Page de départ pour la recherche -pageNumber = 0 - -# Initialisation de la variable devices qui va stocker l'ensemble des appareils trouvés -devices = [] - -uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}" - -if(debug): - print(f"Uri for device search on airwatch : {uri}") -result = requests.get(uri, headers=airwatchHeaders) - -if(debug): - print(f"Result of request : {result}") - - -# On vérifie qu'on a bien un retour OK pour la requête API -if(result.status_code != 200): - print(result.json()) - # Suppression du verrou +try: + airwatch = AirwatchAPI(settings) + airwatch.GetDevices() + logger.info("Airwatch server connection succeeded") +except Exception as error: + logger.critical(f"Connection to Airwatch server failed : {error}") os.remove(lockFile) - exit(0) + exit(1) -result = result.json() +# Initialisation de l'api GLPI +try: + glpiapi = GLPIAPI(settings) + logger.info("GLPI server connection succeeded") +except requests.exceptions.ConnectionError as error: + logger.critical(f"Connection to GLPI server failed : {error}") + os.remove(lockFile) + exit(1) -# On fait une requête pour chaque page tant qu'on a pas atteint le nombre -# d'appareils trouvé dans la première requête -while(len(devices) != result["Total"]): - uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}" - result = requests.get(uri, headers=airwatchHeaders).json() - devices += result["Devices"] - pageNumber += 1 +# Recherche des appareils +devices = airwatch.GetDevices() - -if(debug): - print(f"Nombre d'appareils {len(devices)}") +logger.info(f"Number of devices found in Airwatch : {len(devices)}") # ====================== Début suppression des doublons ================================= # # On récupére les numéros de série -serials = [device["SerialNumber"] for device in devices] +serials = [device.SerialNumber for device in devices] # On garde ceux qui sont présent plus d'une fois et qui n'ont pas HUBNOSERIAL (BYOD) comme numéro de série serials = {serial for serial in serials if serials.count(serial) > 1 and serial != 'HUBNOSERIAL'} -# Récupération des id et de la dernière date d'enrolement +# Récupération des devices et de la dernière date d'enrolement devicesDouble = {} for serial in serials: # on fait une liste des appareils avec le même numéro de série # que l'on stocke dans un dictionnaire avec le numéro de série en clé - devicesDouble[serial] = [[device["Id"]["Value"],datetime.strptime(device["LastEnrolledOn"], "%Y-%m-%dT%H:%M:%S.%f")] for device in devices if serial == device["SerialNumber"]] + devicesDouble[serial] = [[device,datetime.strptime(device.LastEnrolledOn, "%Y-%m-%dT%H:%M:%S.%f")] for device in devices if serial == device.SerialNumber] - -if(debug): - print(f"Doublons détectés: {len(devicesDouble)}") +logger.info(f"Duplicates detected : {len(devicesDouble)}") # On supprime les doublons qui ne se sont pas enrôlés en dernier devicesToDelete = [] @@ -189,220 +140,61 @@ for k,v in devicesDouble.items(): else: devicesToDelete += [d[0]] -# On retire ces appareils de la liste -devices = [d for d in devices if d["Id"]["Value"] not in devicesToDelete] - -# envoi de la requête de suppression des appareils sur magenta -airwatchAPIDeleteURI = '/API/mdm/devices/' -airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDeleteURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) +# envoi de la requête de suppression des appareils sur airwatch for device in devicesToDelete: - uri = f"{airwatchServer}{airwatchAPIDeleteURI}{device}" - if(debug): - print(f"Suppression de {uri}") - requests.delete(uri, headers=airwatchHeaders) + logger.info(f"Deleting {device.Id} - {device.FriendlyName} in Airwatch") + airwatch.DeleteDevice(device) + +devices = airwatch.GetDevices() # ====================== Fin suppression des doublons ================================= # if(searchFilter != None): - if(debug): - print(f"SearchFilter set to {searchFilter}") - print(f"SearchValue set to {searchValue}") + logger.debug(f"SearchFilter set to {searchFilter}") + logger.debug(f"SearchValue set to {searchValue}") if(searchFilter == 'Id'): - devices = [device for device in devices if device["Id"]["Value"] == searchValue] + devices = [device for device in devices if getattr(device, "Id") == searchValue] else: - devices = [device for device in devices if device[searchFilter] == searchValue] - -# Adresse d'initalisation de l'api GLPI -GLPIAPIInitUri = '/apirest.php/initSession/' - -GLPIHeaders = { - 'Content-Type': 'application/json', - "Authorization": f"user_token {GLPIUserToken}", - "App-Token": GLPIAppToken -} - -# Récupération d'un token de session -uri = f"{GLPIServer}{GLPIAPIInitUri}" -result = requests.get(uri, headers=GLPIHeaders) - -if(debug): - print(f"GLPI api access : {result}") - -if(result.status_code != 200): - # Suppression du verrou - os.remove(lockFile) - exit(1) - -GLPISessionToken = result.json()["session_token"] - -if(debug): - print(f"GLPI session Token: {GLPISessionToken}") - -# Changement des headers pour remplacer l'user token par le token de session -GLPIHeaders = { - 'Content-Type': 'application/json', - "Session-Token": GLPISessionToken, - "App-Token": GLPIAppToken -} - -# Adresse de recherche des appareils présents dans ordinateurs sur GLPI -GLPIAPISearchComputer = '/apirest.php/search/computer?' - -platforms = { - 2:"Apple iOS", - 5:"Android", - 12:"Windows Desktop" -} - -processorArchs = { - 0:{ - "osArch":"arm64", - "softwareArch":"arm64" - }, - 9:{ - "osArch":"64-bit", - "softwareArch":"x86_64" - } -} - + devices = [device for device in devices if getattr(device, "searchFilter") == searchValue] + for device in devices: - if(device["EnrollmentStatus"] != 'Enrolled'): + if(device.EnrollmentStatus != 'Enrolled'): + logger.warning(f"Device with id {device.Id} not enrolled, skipping this device...") continue - if(device["Imei"] != ''): - if(debug): - print(f"Imei = {device['Imei']}") - # Recherche des appareils en fonction du numéro de série ou de l'imei - # l'imei pouvant être dans le champ numéro de série ou les champs imei custom - search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device["SerialNumber"]}$'\ - f'&criteria[1][link]=OR&criteria[1][field]=5&criteria[1][searchtype]=contains&criteria[1][value]=^{device["Imei"]}$'\ - f'&criteria[2][link]=OR&criteria[2][field]=76667&criteria[2][searchtype]=contains&criteria[2][value]=^{device["Imei"]}$'\ - f'&criteria[3][link]=OR&criteria[3][field]=76670&criteria[3][searchtype]=contains&criteria[3][value]=^{device["Imei"]}$' - else: - # Recherche des appareils en fonction du numéro de série seulement - search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{device["SerialNumber"]}$' - - if(debug): - print(f"Serial Number = {device['SerialNumber']}") - - searchUri = f"{GLPIServer}{GLPIAPISearchComputer}{search_parameter}" - if(debug): - print(f"searchURI = {searchUri}") - search = requests.get(searchUri, headers=GLPIHeaders) + logger.info(f"Searching device {device.FriendlyName} (id={device.Id}) on GLPI") - - # On ne gère pas pour l'instant d'autres code que le code 200 - # voir en fonction des codes erreurs retournés par le serveur - if(search.status_code != 200): - break + deviceID, data, count = glpiapi.GetDevice(device) + apps = airwatch.GetDeviceApps(device) + if(count > 1): + logger.error(f"{count} devices matching airwatch device in GLPI (GLPI ids = {', '.join(deviceID)}), skipping this device...") + continue + if(count == 0): + logger.error(f"Device not found in GLPI, is it in the trash bin ? Skipping device...") + continue - search = search.json() - - if(search["totalcount"] == 1): - # Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil - for device_id, data in search["data"].items(): - platformId = device["PlatformId"]["Id"]["Value"] - if(platformId in platforms.keys()): - platformName = platforms[platformId] - else: - platformName = "Unknown" - - processorArch = device["ProcessorArchitecture"] - if(processorArch in processorArchs.keys()): - osArch = processorArchs[processorArch]["osArch"] - softwareArch = processorArchs[processorArch]["softwareArch"] - else: - osArch = "Unknown" - softwareArch = "Unknown" - - inventory = { - "action":"inventory", - "content":{ - "accesslog":{ - "logdate": datetime.strptime(device["LastSeen"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d %H:%M:%S") - }, - "versionclient":settings["userAgent"], - "users":[ - { - "login": device["UserName"] - } - ], - "operatingsystem":{ - "name": platformName, - "version": device["OperatingSystem"], - "full_name": f"{platformName} {device['OperatingSystem']}", - "arch": osArch - }, - "softwares":[ - - ], - "hardware":{ - "name":data["1"], - "uuid":device["Uuid"], - "memory":device["TotalPhysicalMemory"] - } - }, - "tag":device["LocationGroupName"], - "deviceid":f"{data['1']} - {device['SerialNumber']}", - "itemtype":"Computer" - } - # Récupération des applications présents sur les appareils - airwatchAPIAppsSearchURI = f"/api/mdm/devices/{device['Uuid']}/apps/search" - airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIAppsSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) - uri = f"{airwatchServer}{airwatchAPIAppsSearchURI}" - apps = requests.get(uri, headers=airwatchHeaders).json() - - for app in apps["app_items"]: - if(app["installed_status"] != "Installed"): - continue - - install_date = datetime.strptime(app["latest_uem_action_time"], "%Y-%m-%dT%H:%M:%S.%f").strftime("%Y-%m-%d") - if(install_date == "1-01-01"): - inventory["content"]["softwares"] += [{ - "name": app["name"], - "guid": app["bundle_id"], - "version": app["installed_version"], - "filesize": app["size"], - "arch": softwareArch - }] - else: - inventory["content"]["softwares"] += [{ - "name": app["name"], - "guid": app["bundle_id"], - "version": app["installed_version"], - "install_date": install_date, - "filesize": app["size"], - "arch": softwareArch - }] - - - - # Mise à jour du friendly name sur Airwatch - if(device["DeviceFriendlyName"] != f"{data['1']} {platformName} {device['OperatingSystem']} - {device['UserName']}"): - airwatchAPIURI = f"/API/mdm/devices/{device['Id']['Value']}" - airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) - uri = f"{airwatchServer}{airwatchAPIURI}" - updateDeviceDetails = { - "DeviceFriendlyName":f"{data['1']} {platformName} {device['OperatingSystem']} - {device['UserName']}" - } - requests.put(uri, headers=airwatchHeaders, json=updateDeviceDetails) + inventory = glpiapi.CreateInventoryForAirwatchDevice(device, data["1"], apps) + # Mise à jour du friendly name sur Airwatch + platformName = inventory.operatingsystem["name"] + if(device.FriendlyName != f"{data['1']} {platformName} {device.OS} - {device.User}"): + newFriendlyName = f"{data['1']} {platformName} {device.OS} - {device.User}" + logger.info(f"Updating device friendlyname to {newFriendlyName}") + airwatch.SetDeviceFriendlyName(device, newFriendlyName) - headers = { - "Content-Type":"Application/x-compress", - "user-agent":settings["userAgent"] - } - if(debug): - print(f"Updating {device_id} on GLPI") - - # filtre des plateformes - if(platformFilterEnabled): - if device["PlatformId"]["Id"]["Value"] in platformFilterOut: - continue - result = requests.post(GLPIServer, headers=headers, json=inventory) - - if(debug): - print(result.json()) + # filtre des plateformes + if(platformFilterEnabled): + if device.PlatformId in platformFilterOut: + logger.info(f"Device platform ({device.PlatformId}) is filtered out, not updating GLPI") + continue + + logger.info(f"Updating {deviceID} on GLPI") + glpiapi.UpdateInventory(inventory.Json()) + + if(data['5'] != device.SerialNumber): + logger.info(f"Updating serial number from {data['5']} to {device.SerialNumber} in GLPI") + glpiapi.UpdateSerialNumber(deviceID, device.SerialNumber) -if(debug): - print('Removing lock') +logger.info("========= End of synchronization =========") + +logger.debug('Removing lock file') os.remove(lockFile) \ No newline at end of file