Compare commits

..

25 Commits

Author SHA1 Message Date
1168cf62f3 Ajout du fichier requirements pour les dépendances python 2025-11-20 17:38:13 +01:00
73a94c3c50 Ajout d'une suppression du verrou si la synchro dure plus de 3h 2025-11-20 17:26:16 +01:00
8e698cfc49 Ajout du lien vers la page Airwatch de l'appareil sur la fiche GLPI 2025-10-23 17:50:03 +02:00
e25463ff89 Correction erreur de frappe pour le nom de la variable 2025-10-16 09:32:36 +02:00
064ecfad43 Modification du nom de plateforme en Windows Desktop en cas de version non énumérée 2025-10-16 09:14:42 +02:00
13bc1f46d7 Ajout d'une traduction des noms de version Windows dans le friendlyname 2025-10-15 14:10:26 +02:00
a89a17020a Désactivation de la vérif utilisateur pour maj glpi 2025-09-12 10:22:38 +02:00
078d4f0923 Modification de la vérification de l'utilisateur non présent 2025-09-12 10:00:33 +02:00
20f28b71db Ajout d'une fonction pour mettre à jour l'utilisateur GLPI lors de la synschro s'il est vide 2025-09-12 09:50:58 +02:00
2bb3eec219 Correction d'une erreur si l'utilisateur n'a pas d'appareils 2025-09-03 16:24:55 +02:00
2fa704f381 Correctifs 2025-08-26 09:35:02 +02:00
0a5e780546 Correction de la gestion des différents fichiers de logs et utilisation d'une variable qui récupère l'emplacement du script 2025-07-10 16:45:18 +02:00
ce003e206a Modification de la gestion des logs pour gérer les doublons et les manquants dans un fichier séparé 2025-07-10 14:31:34 +02:00
3e19b4d3e6 Modification des informations de log pour que l'identification de l'appareil soit présent pour les erreurs 2025-07-09 18:36:23 +02:00
074a1e5313 Création d'un fichier de log séparé pour les erreurs 2025-07-09 18:32:46 +02:00
f697d2fcb4 Changement de l'envoi pour l'inventaire en chaine de caractère au lieu de format json (json -> data) 2025-07-09 17:57:26 +02:00
eb1bc4cc3b Modification du fichier de configuration 2025-07-05 14:10:16 +02:00
92a6259f04 Création du répertoire certs pour les certificats 2025-07-05 14:00:47 +02:00
570b89e50c Mise à jour des informations concernant les scripts 2025-07-05 13:59:26 +02:00
15a34ba66c Created logs folder 2025-07-05 13:37:52 +02:00
68a0d7d6ab added more explicit error when certificate file is missing 2025-07-05 13:28:44 +02:00
ecbd48cbc0 added comment when fetching devices 2025-07-05 13:28:08 +02:00
914abf5018 Added a more explicit error when certificate file is not found 2025-07-05 13:17:43 +02:00
9f04afb153 Updated both scripts to use AirwatchAPI and GLPIAPI modules 2025-07-05 13:09:28 +02:00
a4ee8a1ae8 Ajout d'un module pour gérer les requêtes à l'API Airwatch 2025-06-26 11:22:47 +02:00
12 changed files with 915 additions and 573 deletions

View File

@ -3,28 +3,50 @@
## Explication de l'usage des scripts et de la configuration : ## Explication de l'usage des scripts et de la configuration :
### Fichier de configuration global settings.json ### Fichier de configuration global settings.json
Les scripts prennent les informations de configuration du fichier settings.json, si celui-ci n'existe pas au lancement d'un script, il est automatiquement créé avec des valeurs d'exemples. Les scripts prennent les informations de configuration du fichier de configuration présent dans le répertoire conf, si celui-ci n'existe pas au lancement d'un script, il est automatiquement créé avec des valeurs d'exemples. Un chemin personnalisé vers un fichier de configuration peut être renseigné avec le paramètre -c.
Voici une liste des paramètres du fichier de configuration et les valeurs attendues (chaque valeur doit être entre " ") :
- **"airwatchServer"**: l'adresse du serveur Airwatch Voici un exemple du fichier de configuration :
- **"airwatchAPIKey"**: la clé API récupéré dans Airwatch
- **"airwatchAuthMethod"**: défini la méthode d'authentification, peut prendre la valeur ***"CMSURL"*** pour l'authentification par certificat ou ***"password"*** pour l'authentification par mot de passe.
- **"airwatchCertPath"**: le chemin vers le certificat si la méthode d'authentification est par certificat.
- **"airwatchCertPass"**: le mot de passe du certificat si celui-ci en possède un, laissez une chaîne vide si vous n'avez pas mis de mot de passe.
- **"airwatchAPIUser"**: le nom de l'utilisateur pour l'authentification API dans Airwatch.
- **"airwatchAPIPassword"**: le mot de passe de l'utilisateur si l'authentification par mot de passe est choisie.
- **"glpiServer"**: l'adresse du serveur GLPI avec un / à la fin.
- **"glpiAppToken"**: le token d'application GLPI.
- **"glpiUserToken"**: le token de l'utilisateur GLPI utilisé pour les requêtes API.
- **"stagingUser"**: le nom du compte de staging présent dans Airwatch.
- **"userAgent"**: Un nom pour l'user agent tel qu'il sera visible dans GLPI, cela permet d'identifier l'instance Airwatch (Prod / Pré-prod) par exemple.
```toml
[AIRWATCH]
Server = "https://airwatchServer"
APIKey = "APIKEY"
# Méthode d'authentification (CMSURL or PASSWORD)
# CMSURL permet l'authentification avec un certificat utilisateur (CertificatePath, CertificatePassword)
# 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
# Chemin où seront créé les fichiers de log
Path = "./logs/"
# Mode debug pour avoir plus d'informations
Debug = false
```
--- ---
### syncGLPI.py ### syncGLPI.py
Le script syncGLPI.py permet de synchroniser les données des appareils présents dans Airwatch avec un inventaire GLPI. Le script syncGLPI.py permet de synchroniser les données des appareils présents dans Airwatch avec un inventaire GLPI. Les actions du script :
- vérification et suppression des doublons en fonction du numéro de série et de la date de dernier enrôlement
Au début, le script va vérifier la présence de doublons en fonction du numéro de série et garder seulement le dernier à s'être enrôlé, puis il va procéder à la vérification de la présence des appareils dans l'inventaire GLPI pour procéder à la synchronisation des données. - vérification de la présence des appareils dans l'inventaire GLPI et envoi d'un inventaire à partir des données d'Airwatch pour mettre à jours les informations
- modification du numéro de série de l'appareil sur GLPI si celui-ci n'est pas identique à celui d'Airwatch
- modification du friendlyname de l'appareil sur Airwatch à partir du nom d'inventaire de l'appareil sur GLPI
#### Synchronisation #### Synchronisation
Les éléments synchronisés de Airwatch vers GLPI : Les éléments synchronisés de Airwatch vers GLPI :
@ -33,6 +55,7 @@ Les éléments synchronisés de Airwatch vers GLPI :
- UUID - UUID
- le nom du système d'exploitation et sa version - le nom du système d'exploitation et sa version
- les logiciels présents sur la machine - les logiciels présents sur la machine
- le numéro de série
Les éléments synchronisés de GLPI vers Airwatch : Les éléments synchronisés de GLPI vers Airwatch :
- Le nom d'inventaire de la machine qui est mis pour le friendlyname - Le nom d'inventaire de la machine qui est mis pour le friendlyname
@ -40,23 +63,27 @@ Les éléments synchronisés de GLPI vers Airwatch :
#### Paramètres #### Paramètres
Ce script possède les paramètres suivants qui sont optionnels pour son exécution : Ce script possède les paramètres suivants qui sont optionnels pour son exécution :
- **-debug** : affiche des informations lors de son exécution, utile pour résoudre des problèmes liés à des droits d'accès API ou des problèmes d'ouvertures réseaux - **-sF / --searchFilter** : permet de filtrer la recherche des appareils dans airwatch sur un attribut spécifique parmi la liste suivante : "Id", "SerialNumber", "Imei", "UserName"
- **-searchFilter** : permet de filtrer la recherche des appareils dans airwatch sur un attribut spécifique parmi la liste suivante : "Id", "SerialNumber", "Imei", "UserName" - **-sV / --searchValue** : la valeur pour la recherche lorsque -searchFilter est utilisé
- **-searchValue** : la valeur pour la recherche lorsque -searchFilter est utilisé - **-f / --force** : permet d'outrepasser la vérification du verrou posé par le script lors de son exécution
- **-force** : permet d'outrepasser la vérification du verrou posé par le script lors de son exécution - **-c / --configPath** : permet de définir un chemin vers un fichier de configuration a utilisé pour l'exécution du script
- **-s / --silent** : exécute le script sans faire de retour dans la console, les informations seront toujours présentes dans les fichiers de log
- **-v / --verbose** : affiche des informations lors de son exécution, utile pour résoudre des problèmes liés à des droits d'accès API ou des problèmes d'ouvertures réseaux
--- ---
### StagingUserAssignation.py ### StagingUserAssignation.py
Le script StagingUserAssignation.py permet d'assigner les appareils en staging qui sont assignés à un utilisateur de staging à l'utilisateur renseigné dans l'inventaire GLPI. Le script StagingUserAssignation.py permet d'assigner les appareils en staging qui sont assignés à un utilisateur de staging à l'utilisateur renseigné dans l'inventaire GLPI.
Il récupère le nom de l'utilisateur de staging dans le fichier settings.json. Il récupère le nom de l'utilisateur de staging dans le fichier de configuration.
#### Paramètres #### Paramètres
- **-debug** : affiche des informations lors de son exécution, utile pour résoudre des problèmes liés à des droits d'accès API ou des problèmes d'ouvertures réseaux - **-u / --staginguser** : permet de préciser l'utilisateur de staging pour la recherche des appareils à modifier (override le fichier de paramètres)
- **-force** : permet d'outrepasser la vérification du verrou posé par le script lors de son exécution - **-sn / --serialnumber** : permet de filtrer sur un numéro de série précis
- **-staginguser** : permet de préciser l'utilisateur de staging pour la recherche des appareils à modifier (override le fichier de paramètres) - **-f / --force** : permet d'outrepasser la vérification du verrou posé par le script lors de son exécution
- **-serialnumber** : permet de filtrer sur un numéro de série précis - **-c / --configPath** : permet de définir un chemin vers un fichier de configuration a utilisé pour l'exécution du script
- **-s / --silent** : exécute le script sans faire de retour dans la console, les informations seront toujours présentes dans les fichiers de log
- **-v / --verbose** : affiche des informations lors de son exécution, utile pour résoudre des problèmes liés à des droits d'accès API ou des problèmes d'ouvertures réseaux
## Dépendances ## Dépendances
Installation des paquets linux : Installation des paquets linux :
@ -73,7 +100,7 @@ yum install -y python3 python3-pip git
**Dépendances python** **Dépendances python**
``` ```
python3 -m pip install cryptography requests python3 -m pip install cryptography requests toml
``` ```
## Installation des scripts ## Installation des scripts

3
requirements.txt Normal file
View File

@ -0,0 +1,3 @@
cryptography
requests
toml

View File

@ -1,45 +1,58 @@
#!/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 import time
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 +61,100 @@ 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): # Récupération du temps de création de verrou en minutes
print('Lock file is present, exiting...') lockTime = (time.time() - os.path.getmtime(lockFile)) // 60
exit(0) # Recréation du verrou s'il existe depuis plus de 3 heures (crash)
# sinon on quitte, une synchro est déjà en cours
if(lockTime > 180):
os.remove(lockFile)
open(lockFile, "w").close()
else:
logger.debug('Lock file exists, exiting...')
exit(0)
else: else:
# Création du verrou s'il n'existe pas
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)
# Recherche des appareils filtré sur l'utilisateur de staging
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) devices = airwatch.GetDevices(stagingUser)
logger.info("Airwatch server connection succeeded")
# Page de départ pour la recherche except FileNotFoundError as F:
pageNumber = 0 logger.critical(f"Certificate file not found for CMSURL authentication : {F}")
# 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) os.remove(lockFile)
exit(0) exit(1)
except Exception as error:
result = result.json() logger.critical(f"Connection to Airwatch server failed : {error}")
# 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"] 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})")
if(debug): # Initialisation de l'api GLPI
print(f"GLPI session Token: {GLPISessionToken}") 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)
# 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?'
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}")
searchUri = f"{GLPIServer}{GLPIAPISearchComputer}{search_parameter}" deviceID, data, deviceCount = glpiapi.GetDevice(device)
if(debug):
print(f"searchURI = {searchUri}")
search = requests.get(searchUri, headers=GLPIHeaders)
if(deviceCount == 1):
# 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)
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 ?)")
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)
# Suppression du verrou # Suppression du verrou
os.remove(lockFile) os.remove(lockFile)

0
scripts/certs/empty Normal file
View File

View File

@ -0,0 +1,30 @@
[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
# Chemin o<> seront cr<63><72> les fichiers de log
Path = "./logs/"
# Mode debug pour avoir plus d'informations
Debug = false

47
scripts/functions.py Normal file
View File

@ -0,0 +1,47 @@
import os
import toml
def getSettings(settingsPath):
settingsDefault ="""
[AIRWATCH]
Server = "https://airwatchServer"
ConsoleURI = "https://airwatchConsole"
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
# Chemin où seront créé les fichiers de log
Path = "./logs/"
# Mode debug pour avoir plus d'informations
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

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

@ -0,0 +1,272 @@
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):
deviceID = list(search["data"].keys())
return deviceID, search["data"], search["totalcount"]
else:
return None, None, 0
return None, None, None
def GetUser(self, username=None, email=None):
if(username != None):
search_parameter = f'is_deleted=0&criteria[0][field]=1&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{username}$'
elif(email != None):
search_parameter = f'is_deleted=0&criteria[0][field]=5&withindexes=true&criteria[0][searchtype]=contains&criteria[0][value]=^{email}$'
searchUri = f"{self.Server}/apirest.php/search/user?{search_parameter}"
search = requests.get(searchUri, headers=self.Headers)
if(search.status_code == 200):
search = search.json()
if(search["totalcount"] == 1):
userID = list(search["data"].keys())[0]
data = search["data"][userID]
return userID, data, search["totalcount"]
elif(search["totalcount"] > 1):
userID = list(search["data"].keys())
return userID, search["data"], search["totalcount"]
else:
return None, None, 0
def UpdateInventory(self, inventory):
headers = {
"Content-Type":"Application/x-compress",
"user-agent":self.UserAgent
}
return requests.post(self.Server, headers=headers, data=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 UpdateUser(self, deviceid, username):
body = {
"input" : {
"id" : deviceid,
"users_id" : username
}
}
uri = f"{self.Server}/apirest.php/Computer/"
return requests.put(uri, headers=self.Headers, json=body)
def UpdateAirwatchLink(self, deviceid, airwatchlink):
body = {
"input": {
"id": deviceid,
"appareilsurmagentafield": airwatchlink
}
}
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"
}
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"
windowsOSTranslation = {
"10.0.19043":"10 21H1",
"10.0.19044":"10 21H2",
"10.0.19045":"10 22H2",
"10.0.22000":"11 21H2",
"10.0.22621":"11 22H2",
"10.0.22631":"11 23H2",
"10.0.26100":"11 24H2",
"10.0.26200":"11 25H2"
}
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")
if(platformName == "Windows"):
if(device.OS in windowsOSTranslation.keys()):
inventory.SetOperatingSystem(platformName, windowsOSTranslation[str(device.OS)], osArch)
else:
platformName = "Windows Desktop"
inventory.SetOperatingSystem(platformName, device.OS, osArch)
else:
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

@ -0,0 +1,212 @@
import base64
import requests
from cryptography.hazmat.primitives.serialization import pkcs12, pkcs7
from cryptography.hazmat.primitives import hashes, serialization
class AirwatchAPI:
def __init__(self, settings):
self.Server = settings["AIRWATCH"]["Server"]
self.APIKey = settings["AIRWATCH"]["APIKey"]
self.AuthMethod = (settings["AIRWATCH"]["AuthenticationMethod"]).upper()
if(self.AuthMethod == "PASSWORD"):
self.APIUser = settings["AIRWATCH"]["APIUser"]
self.APIPassword = settings["AIRWATCH"]["APIPassword"]
if(self.AuthMethod == "CMSURL"):
self.CertificatePath = settings["AIRWATCH"]["CertificatePath"]
self.CertificatePassword = settings["AIRWATCH"]["CertificatePassword"]
def GetHeaders(self, uri):
if(self.AuthMethod == "PASSWORD"):
airwatchAPIUserToken = base64.b64encode(f"{self.APIUser}:{self.APIPassword}".encode('ascii')).decode("ascii")
return {
"Authorization": f"Basic {airwatchAPIUserToken}",
"aw-tenant-code": self.APIKey,
"Accept": "application/json"
}
else:
signing_data = uri.split('?')[0]
with open(self.CertificatePath, 'rb') as certfile:
cert = certfile.read()
key, certificate, additional_certs = pkcs12.load_key_and_certificates(cert, self.CertificatePassword.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": self.APIKey,
"Accept": "application/json"
}
def GetUser(self, username):
cmdURI = f'/API/system/users/search?username={username}'
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
user = requests.get(uri, headers=airwatchHeaders)
if(user.status_code == 200):
return AirwatchUser(user.json()["Users"][0])
return None
def GetDevices(self, user: str = ""):
if(user == ""):
cmdURI = f"/API/mdm/devices/search?pagesize=500&page="
else:
cmdURI = f"/API/mdm/devices/search?user={user}&pagesize=500&page="
airwatchHeaders = self.GetHeaders(cmdURI)
pageNum = 0
devices = []
uri = f"{self.Server}{cmdURI}{pageNum}"
result = requests.get(uri, headers=airwatchHeaders)
if(result.status_code == 200):
deviceTotalCount = result.json()["Total"]
while(len(devices) != deviceTotalCount):
uri = f"{self.Server}{cmdURI}{pageNum}"
result = requests.get(uri, headers=airwatchHeaders).json()["Devices"]
for device in result:
devices += [AirwatchDevice(device)]
pageNum += 1
return devices
return None
def GetDeviceApps(self, device):
cmdURI = f"/api/mdm/devices/{device.Uuid}/apps/search"
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
apps = []
result = requests.get(uri, headers=airwatchHeaders)
if(result.status_code == 200):
for app in result.json()["app_items"]:
apps += [AirwatchApplication(app)]
return apps
return None
def ResetDEPProfiles(self, groupUuid):
cmdURI = f"/API/mdm/dep/groups/{groupUuid}/devices"
uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI)
result = requests.get(uri, headers=airwatchHeaders)
if(result.status_code == 200):
for device in result.json():
if (device["enrollmentStatus"] != "Unenrolled"):
continue
assignDEPProfileURI = f"/API/mdm/dep/profiles/{device['profileUuid']}/devices/{device['deviceSerialNumber']}?action=Assign"
uri = f"{airwatchServer}{assignDEPProfileURI}"
airwatchHeaders = self.GetHeaders(assignDEPProfileURI)
requests.put(uri, headers=airwatchHeaders)
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}"
airwatchHeaders = self.GetHeaders(cmdURI)
return requests.put(uri, headers=airwatchHeaders).status_code
def SetDeviceFriendlyName(self, device, friendlyName):
cmdURI = f"/API/mdm/devices/{device.Id}"
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
body = {
"DeviceFriendlyName":friendlyName
}
return requests.put(uri, headers=airwatchHeaders, json=body).status_code
def SetDeviceUser(self, device, airwatchUser):
cmdURI = f'/API/mdm/devices/{device.Id}/enrollmentuser/{airwatchUser.Id}'
uri = f"{self.Server}{cmdURI}"
airwatchHeaders = self.GetHeaders(cmdURI)
return requests.patch(uri, headers=airwatchHeaders).status_code
def DeleteDevice(self, device):
cmdURI = f"/API/mdm/devices/{device.Id}"
airwatchHeaders = self.GetHeaders(cmdURI)
uri = f"{self.Server}{cmdURI}"
return requests.delete(uri, headers=airwatchHeaders).status_code
class AirwatchUser:
def __init__(self, user):
self.Id = user["Id"]["Value"]
self.Uuid = user["Uuid"]
self.UserName = user["UserName"]
self.FirstName = user["FirstName"]
self.LastName = user["LastName"]
self.Enabled = user["Status"]
self.Email = user["Email"]
self.DisplayName = user["DisplayName"]
self.Group = user["Group"]
self.GroupId = user["LocationGroupId"]
self.OrgUuid = user["OrganizationGroupUuid"]
if(user["EnrolledDevicesCount"] != ''):
self.DeviceCount = int(user["EnrolledDevicesCount"])
else:
self.DeviceCount = 0
class AirwatchDevice:
def __init__(self, device):
self.Id = device["Id"]["Value"]
self.Uuid = device["Uuid"]
self.SerialNumber = device["SerialNumber"]
self.Imei = device["Imei"]
self.MacAddress = device["MacAddress"]
self.FriendlyName = device["DeviceFriendlyName"]
self.GroupId = device["LocationGroupId"]["Id"]["Value"]
self.Group = device["LocationGroupName"]
self.GroupUuid = device["LocationGroupId"]["Uuid"]
if(device["UserId"].get("Id") != None):
self.UserId = device["UserId"]["Id"]["Value"]
self.User = device["UserName"]
else:
self.UserId = None
self.User = None
self.UserEmail = device["UserEmailAddress"]
self.PlatformId = device["PlatformId"]["Id"]["Value"]
self.Platform = device["Platform"]
self.OS = device["OperatingSystem"]
self.Arch = device["ProcessorArchitecture"]
self.TotalMemory = device["TotalPhysicalMemory"]
self.EnrollmentStatus = device["EnrollmentStatus"]
self.LastEnrolledOn = device["LastEnrolledOn"]
self.LastSeen = device["LastSeen"]
class AirwatchApplication:
def __init__(self, application):
self.Name = application["name"]
self.Guid = application["bundle_id"]
self.Size = application["size"]
self.Version = application["installed_version"]
self.InstallDate = application["latest_uem_action_time"]
self.Status = application["installed_status"]

0
scripts/logs/empty Normal file
View File

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,104 @@
#!/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 import time
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(f"{os.path.realpath(os.path.dirname(__file__))}/conf/settings.conf")
settings = json.load(f)
#=========== Configuration des logs ===========#
# handler pour les logs de base
logger = logging.getLogger(__name__)
# handler pour log les doublons dans GLPI
loggerDouble = logging.getLogger('doubleGLPI')
# hander pour log les appareils manquants dans GLPI
loggerMissing = logging.getLogger('missingGLPI')
if(args.debug or settings["LOGS"]["Debug"]):
logginglevel = logging.DEBUG
else:
logginglevel = logging.INFO
logger.setLevel(logginglevel)
loggerDouble.setLevel(logginglevel)
loggerMissing.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"]):
# File Handler
if(settings["LOGS"].get("Path") and settings["LOGS"].get("Path") != ""):
fileHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI.log")
fileErrorHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI-errors.log")
fileDoubleHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI-double.log")
fileMissingHandler = logging.FileHandler(f"{settings['LOGS'].get('Path')}syncGLPI-missing.log")
else:
fileHandler = logging.FileHandler('{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI.log')
fileErrorHandler = logging.FileHandler("{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI-errors.log")
fileDoubleHandler = logging.FileHandler("{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI-double.log")
fileMissingHandler = logging.FileHandler("{os.path.realpath(os.path.dirname(__file__))}/logs/syncGLPI-missing.log")
# Set Logging Level to files handler
fileHandler.setLevel(logginglevel)
fileErrorHandler.setLevel(logging.ERROR)
fileDoubleHandler.setLevel(logging.ERROR)
fileMissingHandler.setLevel(logging.ERROR)
# Set Formatter to file handler
fileHandler.setFormatter(formatter)
fileErrorHandler.setFormatter(formatter)
fileDoubleHandler.setFormatter(formatter)
fileMissingHandler.setFormatter(formatter)
# Add Handler to loggers
logger.addHandler(fileHandler)
logger.addHandler(fileErrorHandler)
loggerDouble.addHandler(fileDoubleHandler)
loggerMissing.addHandler(fileMissingHandler)
# 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' nameForLockFile = settings["GLPI"]["UserAgent"].replace(' ', '-')
lockFile = f'{os.path.realpath(os.path.dirname(__file__))}/{nameForLockFile}_SyncGLPI.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 +106,66 @@ 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): # Récupération du temps de création de verrou en minutes
print('Lock file is present, exiting...') lockTime = (time.time() - os.path.getmtime(lockFile)) // 60
exit(0) # Recréation du verrou s'il existe depuis plus de 3 heures (crash)
# sinon on quitte, une synchro est déjà en cours
if(lockTime > 180):
os.remove(lockFile)
open(lockFile, "w").close()
else:
logger.debug('Lock file exists, exiting...')
exit(0)
else: else:
# Création du verrou s'il n'existe pas
open(lockFile, "w").close() open(lockFile, "w").close()
logger.info("========= Synchronization started =========")
# Adresse de recherche des appareils try:
# avec limite de 500 appareils par page (limite max de l'API) airwatch = AirwatchAPI(settings)
airwatchAPIDevicesSearchURI = f"/API/mdm/devices/search?pagesize=500&page=" # recherche des appareils
airwatchHeaders = getAirwatchHeaders(airwatchAuthMethod, airwatchAPIKey, uri=airwatchAPIDevicesSearchURI, User=airwatchAPIUser, password=airwatchAPIPassword, CertPath=airwatchCertPath, CertPassword=airwatchCertPass) devices = airwatch.GetDevices()
logger.info("Airwatch server connection succeeded")
# Page de départ pour la recherche except FileNotFoundError as F:
pageNumber = 0 logger.critical(f"Certificate file not found for CMSURL authentication : {F}")
# 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)
except Exception as error:
logger.critical(f"Connection to Airwatch server failed : {error}")
os.remove(lockFile)
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 logger.info(f"Number of devices found in Airwatch : {len(devices)}")
# 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
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 +181,83 @@ 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 Airwatch id {device.Id} not enrolled, skipping this device...")
continue continue
if(device["Imei"] != ''): if(device.SerialNumber == 'HUBNOSERIAL'):
if(debug): logger.info(f"Device with Airwatch id {device.Id} is using work profile, skipping...")
print(f"Imei = {device['Imei']}") continue
# 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): logger.info(f"Searching device {device.FriendlyName} (Airwatch id={device.Id}) on GLPI")
print(f"Serial Number = {device['SerialNumber']}")
searchUri = f"{GLPIServer}{GLPIAPISearchComputer}{search_parameter}" deviceID, data, count = glpiapi.GetDevice(device)
if(debug): apps = airwatch.GetDeviceApps(device)
print(f"searchURI = {searchUri}") if(count > 1):
search = requests.get(searchUri, headers=GLPIHeaders) loggerDouble.error(f"{count} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI (GLPI ids = {', '.join(deviceID)}), skipping this device...")
continue
if(count == 0):
deviceIDTrash, dataTrash, countTrash = glpiapi.GetDevice(device)
if(countTrash > 1):
loggerDouble.error(f"{countTrash} devices matching airwatch device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI ids = {', '.join(deviceIDTrash)}), skipping this device...")
elif(countTrash == 1):
logger.warning(f"Device {device.FriendlyName} (Airwatch id={device.Id}) in GLPI trashbin (GLPI id={deviceIDTrash}), skipping...")
else:
loggerMissing.error(f"Device {device.FriendlyName} (Airwatch id={device.Id}) not found in GLPI.")
continue
inventory = glpiapi.CreateInventoryForAirwatchDevice(device, data["1"], apps)
# Mise à jour du friendly name sur Airwatch
platformName = inventory.operatingsystem["name"]
osVersion = inventory.operatingsystem["version"]
if(device.FriendlyName != f"{data['1']} {platformName} {osVersion} - {device.User}"):
newFriendlyName = f"{data['1']} {platformName} {osVersion} - {device.User}"
logger.info(f"Updating device friendlyname to {newFriendlyName}")
airwatch.SetDeviceFriendlyName(device, newFriendlyName)
# On ne gère pas pour l'instant d'autres code que le code 200 # Mise à jour de l'url vers la page airwatch de l'appareil sur GLPI
# voir en fonction des codes erreurs retournés par le serveur airwatchlink = f"{settings['AIRWATCH']['ConsoleURI']}/AirWatch/#/AirWatch/Device/Details/Summary/{device.Id}"
if(search.status_code != 200): if(data['76689'] != airwatchlink):
break glpiapi.UpdateAirwatchLink(deviceID, airwatchlink)
search = search.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
if(search["totalcount"] == 1): logger.info(f"Updating {deviceID} on GLPI")
# Récupération de l'utilisateur de l'appareil dans la fiche GLPI de l'appareil glpiapi.UpdateInventory(inventory.Json())
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(data['70'] == None and device.User != settings["AIRWATCH"]["StagingUser"]):
if(processorArch in processorArchs.keys()): userID, userData, userCount = glpiapi.GetUser(device.User)
osArch = processorArchs[processorArch]["osArch"] if(userCount == 1):
softwareArch = processorArchs[processorArch]["softwareArch"] logger.info(f"Updating user from {data['70']} to {device.User} in GLPI (id={deviceID})")
else: glpiapi.UpdateUser(deviceID, userID)
osArch = "Unknown"
softwareArch = "Unknown"
inventory = { if(data['5'] != device.SerialNumber):
"action":"inventory", logger.info(f"Updating serial number from {data['5']} to {device.SerialNumber} in GLPI (id={deviceID})")
"content":{ glpiapi.UpdateSerialNumber(deviceID, device.SerialNumber)
"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":[
], logger.info("========= End of synchronization =========")
"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"]: logger.debug('Removing lock file')
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 = {
"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())
if(debug):
print('Removing lock')
os.remove(lockFile) os.remove(lockFile)