Updated both scripts to use AirwatchAPI and GLPIAPI modules

This commit is contained in:
2025-07-05 13:09:28 +02:00
parent a4ee8a1ae8
commit 9f04afb153
8 changed files with 526 additions and 543 deletions

View File

@ -1,45 +1,57 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import base64
import requests
import json
import argparse import argparse
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7 import logging
from cryptography.hazmat.primitives import hashes, serialization from functions import getSettings
from includes.airwatchAPI import *
from includes.GLPIAPI import *
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-debug", action=argparse.BooleanOptionalAction) parser.add_argument("-sn", "--serialnumber", dest="serialnumber", type=str)
parser.add_argument("-force", action=argparse.BooleanOptionalAction) parser.add_argument("-u", "--staginguser", dest="staginguser", type=str)
parser.add_argument("-serialnumber") parser.add_argument("-c", "--configPath", dest="configpath", type=str)
parser.add_argument("-staginguser") 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() args = parser.parse_args()
settingsDefault = { # Récupération des informations du fichier de configuration
"airwatchServer":"https://airwatchServer", if(args.configpath != None and args.configpath != ''):
"airwatchAPIKey":"APIKEY", settings = getSettings(args.configpath)
"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)
else: else:
with open("./settings.json", "r") as f: settings = getSettings("./conf/settings.conf")
settings = json.load(f)
#=========== 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 ========# #======== Paramètres du script ========#
@ -48,203 +60,89 @@ lockFile = './airwatchStagingUserAssignation.lock'
debug=args.debug debug=args.debug
# Informations du serveur Airwatch stagingUser = settings["AIRWATCH"]["StagingUser"]
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"]
if(args.staginguser != None): if(args.staginguser != None):
stagingUser = args.staginguser 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 # Vérification de la présence du verrou avant de continuer
if(os.path.isfile(lockFile) and not args.force): if(os.path.isfile(lockFile) and not args.force):
if(debug): logger.debug('Lock file exists, exiting...')
print('Lock file is present, exiting...')
exit(0) exit(0)
else: else:
open(lockFile, "w").close() open(lockFile, "w").close()
# Adresse de recherche des appareils filtré sur l'utilisateur de staging # Initialisation de l'api Airwatch
# avec limite de 500 appareils par page (limite max de l'API) try:
airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?user={stagingUser}&pagesize=500&page=" airwatch = AirwatchAPI(settings)
logger.info("Airwatch server connection succeeded")
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) except requests.exceptions.ConnectionError as error:
logger.critical(f"Connection to Airwatch server failed : {error}")
# 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
os.remove(lockFile) os.remove(lockFile)
exit(1) exit(1)
GLPISessionToken = result.json()["session_token"] # Recherche des appareils filtré sur l'utilisateur de staging
devices = airwatch.GetDevices(stagingUser)
if(debug): if(devices == None):
print(f"GLPI session Token: {GLPISessionToken}") 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 # Initialisation de l'api GLPI
GLPIHeaders = { try:
'Content-Type': 'application/json', glpiapi = GLPIAPI(settings)
"Session-Token": GLPISessionToken, logger.info("GLPI server connection succeeded")
"App-Token": GLPIAppToken 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: 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 continue
if(args.serialnumber != None and device["SerialNumber"] != args.serialnumber): if(args.serialnumber != None and device.SerialNumber != args.serialnumber):
continue 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): 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(deviceCount == 1):
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):
# Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil # 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"] device_user = data["70"]
if(debug):
print(f"user on device in GLPI : {device_user}") 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 # Vérification que l'appareil est associé à un utilisateur dans GLPI
if(device_user != None): if(device_user != None):
# Récupération de l'utilisateur sur Magenta # Récupération de l'utilisateur sur Airwatch
cmdURI = f'/API/system/users/search?username={device_user}' airwatchUser = airwatch.GetUser(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)
# On ne gère pas pour l'instant d'autres code que le code 200 if(airwatchUser == None):
# voir en fonction des codes erreurs retournés par le serveur logger.error(f"User {device_user} not found in Airwatch")
if(user.status_code != 200): continue
break logger.info(f"Assigning device with id {device.Id} to user {device_user} (id={airwatchUser.Id}) in Airwatch")
result = airwatch.SetDeviceUser(device, airwatchUser)
user = user.json() else:
# Changement de l'utilisateur assigné sur l'appareil dans Magenta logger.warning(f"Device with id {device.Id} is not assigned to any user in GLPI, skipping the device")
cmdURI = f'/API/mdm/devices/{device["Id"]["Value"]}/enrollmentuser/{user["Users"][0]["Id"]["Value"]}' elif(deviceCount > 1):
patchUri = f'{airwatchServer}{cmdURI}' logger.info(f"More than one entry found in GLPI for device with id {device.Id}")
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=cmdURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) else:
if(debug): logger.error(f"Device {device.Id} with serialnumber {device.SerialNumber} not found in GLPI (in trash bin ?)")
print(f"patchUri = {patchUri}")
requests.patch(patchUri, headers=airwatchHeaders)
# Suppression du verrou # Suppression du verrou
os.remove(lockFile) os.remove(lockFile)

View File

@ -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<75> 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

43
scripts/functions.py Normal file
View File

@ -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

210
scripts/includes/GLPIAPI.py Normal file
View File

@ -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)

View File

View File

@ -86,7 +86,7 @@ class AirwatchAPI:
return None return None
def ResetDEPProfiles(self, groupUuid): 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}" uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI) airwatchHeaders = self.GetHeaders(cmdURI)
result = requests.get(uri, headers=airwatchHeaders) result = requests.get(uri, headers=airwatchHeaders)
@ -101,6 +101,32 @@ class AirwatchAPI:
return True return True
return False 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): def SyncDEPDevices(self, groupUuid):
cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices?action=sync" cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices?action=sync"
uri = f"{self.Server}{cmdURI}" uri = f"{self.Server}{cmdURI}"

View File

@ -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"
}

View File

@ -1,78 +1,75 @@
#!/usr/bin/python3 #!/usr/bin/python3
import os import os
import base64
import requests
import json
import argparse import argparse
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7 import logging
from cryptography.hazmat.primitives import hashes, serialization
from datetime import datetime from datetime import datetime
from functions import getSettings
from includes.airwatchAPI import *
from includes.GLPIAPI import *
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("-debug", action=argparse.BooleanOptionalAction) parser.add_argument("-sF", "--searchFilter", dest="searchfilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"])
parser.add_argument("-searchFilter", type=str, choices=["Id", "SerialNumber", "Imei", "UserName"]) parser.add_argument("-sV","--searchValue", dest="searchvalue", type=str)
parser.add_argument("-searchValue", type=str) parser.add_argument("-c", "--configPath", dest="configpath", type=str)
parser.add_argument("-force", action=argparse.BooleanOptionalAction) 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() 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 # Récupération des informations du fichier de configuration
if(args.configpath != None and args.configpath != ''):
if(not os.path.isfile("./settings.json")): settings = getSettings(args.configpath)
f = open("./settings.json", "w")
f.write(json.dumps(settingsDefault, indent=4))
f.close()
exit(1)
else: else:
with open("./settings.json", "r") as f: settings = getSettings("./conf/settings.conf")
settings = json.load(f)
#=========== 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 ========# #======== Paramètres du script ========#
# Emplacement du verrou # Emplacement du verrou
lockFile = './airwatchSyncGLPI.lock' 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 # Filtres
searchFilter = args.searchFilter searchFilter = args.searchfilter
searchValue = args.searchValue searchValue = args.searchvalue
# Platform exclusion (12 = computer) # Platform exclusion (12 = computer)
platformFilterEnabled = True 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 # Vérification de la présence du verrou avant de continuer
if(os.path.isfile(lockFile) and not args.force): if(os.path.isfile(lockFile) and not args.force):
if(debug): logger.debug('Lock file exists, exiting...')
print('Lock file is present, exiting...')
exit(0) exit(0)
else: else:
open(lockFile, "w").close() open(lockFile, "w").close()
logger.info("========= Synchronization started =========")
try:
# Adresse de recherche des appareils airwatch = AirwatchAPI(settings)
# avec limite de 500 appareils par page (limite max de l'API) airwatch.GetDevices()
airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?pagesize=500&page=" logger.info("Airwatch server connection succeeded")
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) except Exception as error:
logger.critical(f"Connection to Airwatch server failed : {error}")
# 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
os.remove(lockFile) 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 # Recherche des appareils
# d'appareils trouvé dans la première requête devices = airwatch.GetDevices()
while(len(devices) != result["Total"]):
uri = f"{airwatchServer}{airwatchAPIDevicesSearchURI}{pageNumber}"
result = requests.get(uri, headers=airwatchHeaders).json()
devices += result["Devices"]
pageNumber += 1
logger.info(f"Number of devices found in Airwatch : {len(devices)}")
if(debug):
print(f"Nombre d'appareils {len(devices)}")
# ====================== Début suppression des doublons ================================= # # ====================== Début suppression des doublons ================================= #
# On récupére les numéros de série # 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 # 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'} 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 = {} devicesDouble = {}
for serial in serials: for serial in serials:
# on fait une liste des appareils avec le même numéro de série # 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é # 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]
logger.info(f"Duplicates detected : {len(devicesDouble)}")
if(debug):
print(f"Doublons détectés: {len(devicesDouble)}")
# On supprime les doublons qui ne se sont pas enrôlés en dernier # On supprime les doublons qui ne se sont pas enrôlés en dernier
devicesToDelete = [] devicesToDelete = []
@ -189,220 +140,61 @@ for k,v in devicesDouble.items():
else: else:
devicesToDelete += [d[0]] devicesToDelete += [d[0]]
# On retire ces appareils de la liste # envoi de la requête de suppression des appareils sur airwatch
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)
for device in devicesToDelete: for device in devicesToDelete:
uri = f"{airwatchServer}{airwatchAPIDeleteURI}{device}" logger.info(f"Deleting {device.Id} - {device.FriendlyName} in Airwatch")
if(debug): airwatch.DeleteDevice(device)
print(f"Suppression de {uri}")
requests.delete(uri, headers=airwatchHeaders) devices = airwatch.GetDevices()
# ====================== Fin suppression des doublons ================================= # # ====================== Fin suppression des doublons ================================= #
if(searchFilter != None): if(searchFilter != None):
if(debug): logger.debug(f"SearchFilter set to {searchFilter}")
print(f"SearchFilter set to {searchFilter}") logger.debug(f"SearchValue set to {searchValue}")
print(f"SearchValue set to {searchValue}")
if(searchFilter == 'Id'): 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: else:
devices = [device for device in devices if device[searchFilter] == searchValue] devices = [device for device in devices if getattr(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"
}
}
for device in devices: 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 continue
if(device["Imei"] != ''): logger.info(f"Searching device {device.FriendlyName} (id={device.Id}) on GLPI")
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)
deviceID, data, count = glpiapi.GetDevice(device)
# On ne gère pas pour l'instant d'autres code que le code 200 apps = airwatch.GetDeviceApps(device)
# voir en fonction des codes erreurs retournés par le serveur if(count > 1):
if(search.status_code != 200): logger.error(f"{count} devices matching airwatch device in GLPI (GLPI ids = {', '.join(deviceID)}), skipping this device...")
break continue
if(count == 0):
logger.error(f"Device not found in GLPI, is it in the trash bin ? Skipping device...")
continue
search = search.json() inventory = glpiapi.CreateInventoryForAirwatchDevice(device, data["1"], apps)
# Mise à jour du friendly name sur Airwatch
if(search["totalcount"] == 1): platformName = inventory.operatingsystem["name"]
# Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil if(device.FriendlyName != f"{data['1']} {platformName} {device.OS} - {device.User}"):
for device_id, data in search["data"].items(): newFriendlyName = f"{data['1']} {platformName} {device.OS} - {device.User}"
platformId = device["PlatformId"]["Id"]["Value"] logger.info(f"Updating device friendlyname to {newFriendlyName}")
if(platformId in platforms.keys()): airwatch.SetDeviceFriendlyName(device, newFriendlyName)
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)
headers = { # filtre des plateformes
"Content-Type":"Application/x-compress", if(platformFilterEnabled):
"user-agent":settings["userAgent"] if device.PlatformId in platformFilterOut:
} logger.info(f"Device platform ({device.PlatformId}) is filtered out, not updating GLPI")
if(debug): continue
print(f"Updating {device_id} on GLPI")
logger.info(f"Updating {deviceID} on GLPI")
# filtre des plateformes glpiapi.UpdateInventory(inventory.Json())
if(platformFilterEnabled):
if device["PlatformId"]["Id"]["Value"] in platformFilterOut: if(data['5'] != device.SerialNumber):
continue logger.info(f"Updating serial number from {data['5']} to {device.SerialNumber} in GLPI")
result = requests.post(GLPIServer, headers=headers, json=inventory) glpiapi.UpdateSerialNumber(deviceID, device.SerialNumber)
if(debug):
print(result.json())
if(debug): logger.info("========= End of synchronization =========")
print('Removing lock')
logger.debug('Removing lock file')
os.remove(lockFile) os.remove(lockFile)