#/** #@file InventoryLib.py #@brief Librería o clase Inventory #@class Inventory #@brief Funciones para recogida de datos de inventario de hardware y software de los clientes. #@warning License: GNU GPLv3+ #*/ import platform import sys import os import subprocess import re import json import shutil import glob import plistlib #import bsddb import ogGlobals import SystemLib import FileSystemLib import RegistryLib import FileLib #/** # ogGetArch #@brief Devuelve el tipo de arquitectura del cliente. #@return str_arch - Arquitectura (i386 para 32 bits, x86_64 para 64 bits). #*/ def ogGetArch(): if os.path.isdir ('/lib64'): return 'x86_64' else: return 'i386' #/** # ogGetOsType int_ndisk int_npartition #@brief Devuelve el tipo del sistema operativo instalado. #@param int_ndisk nº de orden del disco #@param int_npartition nº de orden de la partición #@return OSType - Tipo de sistema operativo. #@see ogGetOsVersion #*/ ## def ogGetOsType(disk, partition): try: os_version = ogGetOsVersion(disk, partition) if os_version: return os_version.split(":", 1)[0] else: return None except Exception as e: print(f"Error en ogGetOsType: {e}") return "Unknown" #/** # ogGetOsUuid int_ndisk int_nfilesys #@brief Devuelve el UUID del sistema operativo instalado en un sistema de archivos. #@param int_ndisk nº de orden del disco #@param int_nfilesys nº de orden de la partición #@return str_uuid - UUID del sistema operativo. #@exception OG_ERR_FORMAT Formato incorrecto. #@exception OG_ERR_NOTFOUND Disco o partición no corresponden con un dispositiv #*/ ## def ogGetOsUuid (disk, par): mntdir = FileSystemLib.ogMount (disk, par) if not mntdir: return None # Obtener UUID según el tipo de sistema operativo. os_type = ogGetOsType (disk, par) if 'Linux' == os_type: # Leer el UUID del sistema de ficheros raíz o el fichero de identificador. uuid = subprocess.run (['findmnt', '-no', 'UUID', mntdir], capture_output=True, text=True).stdout.strip() if not uuid: uuid = open (os.path.join (mntdir, 'etc', 'machine-id')).read().strip() return uuid elif 'Windows' == os_type: # Leer identificador en clave de registro. uuid = RegistryLib.ogGetRegistryValue (mntdir, 'SOFTWARE', r'\Microsoft\Cryptography\MachineGuid') return uuid #/** # ogGetSerialNumber #@brief Obtiene el nº de serie del cliente. #*/ ## def ogGetSerialNumber(): SERIALNO = subprocess.check_output(["dmidecode", "-s", "system-serial-number"]).decode().strip() SERIALNO = re.sub(r"(not specified|to be filled|invalid entry|default string)", "", SERIALNO, flags=re.IGNORECASE) SERIALNO = SERIALNO.replace(" ", "") SERIALNO = SERIALNO[:25] if len(SERIALNO) > 25 else SERIALNO if SERIALNO: return SERIALNO return None #/** # ogIsEfiActive #@brief Comprueba si el sistema tiene activo el arranque EFI. #*/ ## def ogIsEfiActive(): return os.path.isdir("/sys/firmware/efi") #/** # ogListHardwareInfo #@brief Lista el inventario de hardware de la máquina cliente. #@return TipoDispositivo:Modelo (por determinar) #@warning Se ignoran los parámetros de entrada. #@note TipoDispositivo = { bio, boa, bus, cha, cdr, cpu, dis, fir, mem, mod, mul, net, sto, usb, vga } #@note Requisitos: dmidecode, lshw, awk #*/ ## def ogListHardwareInfo(): ret = '' # Ejecutar dmidecode y obtener tipo de chasis dmi_out = subprocess.run (['dmidecode', '-s', 'chassis-type'], capture_output=True, text=True).stdout dmi_out = '\n'.join ([ x for x in dmi_out.splitlines() if 'Other' not in x ]) ret += f'cha={dmi_out}\n' if os.path.exists ('/sys/firmware/efi'): ret += f'boo=UEFI\n' else: ret += f'boo=BIOS\n' awk_script = r''' BEGIN {type="mod";} /product:/ {sub(/ *product: */,""); prod=$0;} /vendor:/ {sub(/ *vendor: */,""); vend=$0;} /version:/ {sub(/ *version: */,"v.");vers=$0;} /size:/ {size=$2;} /clock:/ {clock=$2;} /slot:/ {sub(/ *slot: */,""); slot=$0;} /\*-/ {if (type=="mem"){ if (size!=""){ numbank++; print type"="vend,prod,size,clock" ("slot")";} }else{ if (type=="totalmem"){ if (size!=""){ totalmemory="mem="size;} }else{ if (type!="" && prod!=""){ if (prod=="v."vers) vers=""; print type"="vend,prod,size,vers;} } } type=prod=vend=vers=size=clock=slot="";} $1~/-core/ {type="boa";} $1~/-firmware/ {type="bio";} $1~/-cpu/ {type="cpu";} $1~/-bank/ {type="mem";} $1~/-memory/ {type="totalmem";} $1~/-ide/ {type="ide";} $1~/-storage/ {type="sto";} $1~/-disk/ {type="dis";} $1~/-cdrom/ {type="cdr";} $1~/-display/ {type="vga";} $1~/-network/ {type="net";} $1~/-multimedia/ {type="mul";} $1~/-usb/ {type="usb";} $1~/-firewire/ {type="fir";} $1~/-serial/ {type="bus";} END {if (type!="" && prod!="") print type"="vend,prod,size,vers; if (length(numbank)==0 && length(totalmemory)>=4) print totalmemory; } ''' lshw_out = subprocess.run (['lshw'], capture_output=True, text=True).stdout awk_out = subprocess.run (['awk', awk_script], input=lshw_out, capture_output=True, text=True).stdout ret += awk_out return ret #/** # ogListSoftware int_ndisk int_npartition #@brief Lista el inventario de software instalado en un sistema operativo. #@param int_ndisk nº de orden del disco #@param int_npartition nº de orden de la partición #@return programa versión ... #@warning Se ignoran los parámetros de entrada. #@note Requisitos: ... #@todo Detectar software en Linux #*/ ## def ogListSoftware (disk, par): mntdir = FileSystemLib.ogMount (disk, par) if not mntdir: return None ostype = ogGetOsType (disk, par) if not ostype: return None apps = [] if 'Linux' == ostype: # Procesar paquetes dpkg. pkgdir = f'{mntdir}/var/lib/dpkg' if os.path.exists (pkgdir): status_file = os.path.join(pkgdir, "status") awk_script = ''' /Package:/ {if (pack!="") print pack,vers; sub(/-dev$/,"",$2); pack=$2} /Version:/ {sub(/^.*:/,"",$2); sub(/-.*$/,"",$2); vers=$2} /Status:/ {if ($2!="install") pack=vers=""} END {if (pack!="") print pack,vers} ''' awk_out = subprocess.run (['awk', awk_script, status_file], capture_output=True, text=True).stdout apps = awk_out.splitlines() # Procesar paquetes RPM. pkgdir = f'{mntdir}/var/lib/rpm' if os.path.exists (pkgdir): if shutil.which ('rpm'): for f in glob.glob (f'{pkgdir}/__db.*'): os.unlink (f) rpm_out = subprocess.run (['rpm', '--dbpath', pkgdir, '-qa', '--qf', '%{NAME} %{VERSION}\n'], capture_output=True, text=True).stdout for l in rpm_out.splitlines(): words = l.split() if (not re.search ('-devel$', words[0])): words[1] = re.sub ('-.*', '', words[1]) apps.append (' '.join (words)) for f in glob.glob (f'{pkgdir}/__db.*'): os.unlink (f) else: pass #db = bsddb.hashopen (f'{pkgdir}/Name', 'r'); #for k in db.keys(): # apps.append (re.sub ('-devel$', '', k)) # Procesar paquetes pacman. pkgdir = f'{mntdir}/var/lib/pacman/local' if os.path.exists (pkgdir): for f in glob.glob (f'{pkgdir}/*'): if '-' not in f: continue idx = f[0:f.rfind ('-')].rfind ('-') ## index of 2nd-to-last dash apps.append (f[0:idx] + ' ' + f[idx+1:]) # Procesar aplicaciones Snappy. pkgdir = f'{mntdir}/snap' out = '' awk_script = ''' /name:/ {pack=$2} /version:/ {vers=$2} END {if (pack!="") print pack,"(snap)",vers} ''' files = subprocess.run (['find', f'{pkgdir}/*/current/meta', '-name', 'snap.yaml'], capture_output=True, text=True).stdout.splitlines() for f in files: awk_out = subprocess.run (['awk', awk_script, f], capture_output=True, text=True).stdout out += awk_out apps += out.splitlines() # Procesar aplicaciones Flatpak. pkgdir = f'{mntdir}/var/lib/flatpak' files = glob.glob (f'{pkgdir}/app/*/current/active/deploy/*') for f in files: p = open (f.strip()).read().split ('\0') try: if (p[0] != 'flathub'): raise ValueError apps.append ('{} (flatpak) {}'.format (p[p.index('appdata-name') + 4], p[p.index('appdata-version') + 1])) except ValueError: pass elif 'Windows' == ostype: if shutil.which ('hivexregedit'): hive = RegistryLib.ogGetHivePath (mntdir, 'software') if hive: cmd1_out = subprocess.run (['hivexregedit', '--unsafe-printable-strings', '--export', hive, r'\Microsoft\Windows\CurrentVersion\Uninstall'], capture_output=True, text=True).stdout cmd1_out += subprocess.run (['hivexregedit', '--unsafe-printable-strings', '--export', hive, r'\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'], capture_output=True, text=True).stdout out = name = '' for l in cmd1_out.splitlines(): words = l.split ('"') if len(words) < 4: continue if (re.match (r'\[', words[0])): name='' if (re.search ('DisplayName', words[1])): name=words[3] if (re.search ('DisplayVersion', words[1])): apps.append (f'{name} {words[3]}') else: keys = RegistryLib.ogListRegistryKeys (mntdir, 'software', r'\Microsoft\Windows\CurrentVersion\Uninstall') keys32 = RegistryLib.ogListRegistryKeys (mntdir, 'software', r'\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall') for k in keys: prog = RegistryLib.ogGetRegistryValue (mntdir, 'software', rf'\Microsoft\Windows\CurrentVersion\Uninstall\{k}\DisplayName') if prog: vers = RegistryLib.ogGetRegistryValue (mntdir, 'software', rf'\Microsoft\Windows\CurrentVersion\Uninstall\{k}\DisplayVersion') apps.append (f'{prog} {vers}') for k in keys32: prog = RegistryLib.gGetRegistryValue (mntdir, 'software', rf'\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{k}\DisplayName') if prog: vers = RegistryLib.ogGetRegistryValue (mntdir, 'software', rf'\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\{k}\DisplayVersion') apps.append (f'{prog} {vers}') elif 'MacOS' == ostype: files = subprocess.run (['find', f'{mntdir}/Applications', '-type', 'd', '-name', '*.app', '-prune', '-print'], capture_output=True, text=True).stdout.splitlines() for k in files: FILE = f'{k}/Contents/version.plist' if not os.stat (FILE).st_size: FILE = f'{k}/Contents/version.plist.uncompress' if os.stat (FILE).st_size: VERSION = subprocess.run (['awk', '-F[<>]', '/ShortVersionString/ {getline;v=$3} END {print v}', FILE], capture_output=True, text=True).stdout.strip() bn = os.path.basename (k) bn = re.sub ('.app$', '', bn) apps.append (f'{bn} {VERSION}') elif 'BSD' == ostype: sqlite_out = subprocess.run (['sqlite3', f'{mntdir}/var/db/pkg/local.sqlite'], input='SELECT name FROM pkg_search;\n', capture_output=True, text=True).stdout for l in sqlite_out.splitlines(): apps.append (' '.join (re.search ('(.*)-(.*)', l).groups())) else: SystemLib.ogRaiseError ([], ogGlobals.OG_ERR_NOTOS, f'{disk}, {par} ({ostype})') return None os_version = ogGetOsVersion (disk, par).split (':')[1] return [os_version] + sorted (set (apps)) ## https://stackoverflow.com/questions/2522651/find-a-key-inside-a-deeply-nested-dictionary/2522706#2522706 def _find_key_recursive(plist_dict, key_substr): for k in plist_dict.keys(): if key_substr in k: return plist_dict[k] for k, v in plist_dict.items(): if type(v) is dict: # Only recurse if we hit a dict value value = _find_key_recursive(v, key_substr) if value: return value return '' #/** # ogGetOsVersion int_ndisk int_nfilesys #@brief Devuelve la versión del sistema operativo instalado en un sistema de archivos. #@param int_ndisk nº de orden del disco #@param int_nfilesys nº de orden de la partición #@return OSType:OSVersion - tipo y versión del sistema operativo. #@note OSType = { Android, BSD, GrubLoader, Hurd, Linux, MacOS, Solaris, Windows, WinLoader } #@note Requisitos: awk, head, chroot #@exception OG_ERR_FORMAT Formato incorrecto. #@exception OG_ERR_NOTFOUND Disco o partición no corresponden con un dispositiv #@exception OG_ERR_PARTITION Fallo al montar el sistema de archivos. #*/ ## #ogGetOsVersion ("1", "2") => "Linux:Ubuntu precise (12.04 LTS) 64 bits" def ogGetOsVersion(disk, part): mntdir = FileSystemLib.ogMount (disk, part) if not mntdir: return None type = version = None is64bit = '' # Buscar tipo de sistema operativo. # Para GNU/Linux: leer descripción. os_release = os.path.join(mntdir, "etc/os-release") if os.path.isfile(os_release): type = 'Linux' with open(os_release, "r") as f: for line in f: if line.startswith("PRETTY_NAME"): version = line.split("=", 1)[1].strip().strip('"') break if not version: lsb_release = os.path.join(mntdir, "etc/lsb-release") if os.path.isfile(lsb_release): type = 'Linux' with open(lsb_release, "r") as f: for line in f: if line.startswith("DISTRIB_DESCRIPTION"): version = line.split("=", 1)[1].strip().strip('"') break if not version: for distrib in ["redhat", "SuSE", "mandrake", "gentoo"]: distrib_file = os.path.join(mntdir, f"etc/{distrib}-release") if os.path.isfile(distrib_file): type = 'Linux' with open(distrib_file, "r") as f: version = f.readline().strip() break if not version: arch_release_file = os.path.join(mntdir, "etc/arch-release") if os.path.isfile(arch_release_file): type = 'Linux' version = "Arch Linux" if not version: slack_release_file = os.path.join(mntdir, "slackware-version") if os.path.isfile(slack_release_file): type = 'Linux' with open (slack_release_file, 'r') as fd: c = fd.read() version = "Slackware {c}" # Si no se encuentra, intentar ejecutar "lsb_release". if not version: out = subprocess.run (['chroot', mntdir, 'lsb_release', '-d'], capture_output=True, text=True).stdout m = re.search (':\t(.*)', out) if m: type = 'Linux' version = m.group(1) # Comprobar Linux de 64 bits. if version and os.path.exists(os.path.join(mntdir, "lib64")): is64bit = ogGlobals.lang.MSG_64BIT # Para Android, leer fichero de propiedades. if not version: type = 'Android' files = glob.glob (os.path.join (mntdir, 'android*/system/build.prop')) if files and os.path.isfile (files[0]): v = [] with open (files[0], 'r') as f: for line in f: if 'product.brand' in line or 'build.version.release' in line: v.append (line.split('=')[1].strip()) version = ' '.join (v) if os.path.exists(os.path.join(mntdir, "lib64")): is64bit = ogGlobals.lang.MSG_64BIT # Para GNU/Hurd, comprobar fichero de inicio (basado en os-prober). if not version: type = 'Hurd' if os.path.exists(os.path.join(mntdir, "hurd/init")): version = 'GNU/Hurd' # Para Windows: leer la version del registro. if not version: type = 'Windows' build = 0 file = RegistryLib.ogGetHivePath (mntdir, 'SOFTWARE') if file: # Nuevo método más rápido para acceder al registro de Windows.. i = '\n'.join ([ f'load {file}', r'cd \Microsoft\Windows NT\CurrentVersion', 'lsval ProductName', 'lsval DisplayVersion', ]) version = subprocess.run (['hivexsh'], input=i, capture_output=True, text=True).stdout version = version.replace ('\n', ' ') # Recoge el valor del número de compilación para ver si es Windows 10/11 i = '\n'.join ([ f'load {file}', r'cd \Microsoft\Windows NT\CurrentVersion', 'lsval CurrentBuildNumber', ]) build = subprocess.run (['hivexsh'], input=i, capture_output=True, text=True).stdout if subprocess.run (['reglookup', '-H', '-p', 'Microsoft/Windows/CurrentVersion/ProgramW6432Dir', file], capture_output=True, text=True).stdout: is64bit = ogGlobals.lang.MSG_64BIT if not version: # Compatibilidad con métrodo antiguo y más lento de acceder al registro. version = RegistryLib.ogGetRegistryValue (mntdir, 'software', r'\Microsoft\Windows NT\CurrentVersion\ProductName') if RegistryLib.ogGetRegistryValue (mntdir, 'software', r'\Microsoft\Windows\CurrentVersion\ProgramW6432Dir'): is64bit = ogGlobals.lang.MSG_64BIT # Si la compilación es mayor o igual a 22000 es Windows 11 if int (build) >= 22000: version = version.replace ('10', '11') # Para cargador Windows: buscar versión en fichero BCD (basado en os-prober). if not version: type = 'WinLoader' file = FileLib.ogGetPath (file=f'{mntdir}/boot/bcd') if not file: file = FileLib.ogGetPath (file=f'{mntdir}/EFI/Microsoft/boot/bcd') if file: for distrib in 'Windows Recovery', 'Windows Boot': with open (file, 'rb') as fd: contents = fd.read() distrib_utf16_regex = re.sub (r'(.)', '\\1.', distrib) distrib_utf16_regex = bytes (distrib_utf16_regex, 'ascii') if re.search (distrib_utf16_regex, contents): version = f'{distrib} loader' # Para macOS: detectar kernel y completar con fichero plist de información del sistema. if not version: type = 'MacOS' # Kernel de Mac OS (no debe ser fichero de texto). file = f'{mntdir}/mach_kernel' out = subprocess.run (['file', '--brief', file], capture_output=True, text=True).stdout if not 'text' in out: # Obtener tipo de kernel. if 'Mach-O' in out: version = 'macOS' if 'Mach-O 64-bit' in out: is64bit = ogGlobals.lang.MSG_64BIT # Datos de configuración de versión de Mac OS. file = f'{mntdir}/System/Library/CoreServices/SystemVersion.plist' if os.path.exists (file): with open (file, 'rb') as fd: contents = fd.read() plist_dict = plistlib.loads (contents) n = _find_key_recursive (plist_dict, 'ProductName') v = _find_key_recursive (plist_dict, 'ProductVersion') version = f'{n} {v}'.strip() # Datos de recuperación de macOS. if version and os.path.exists (f'{mntdir}/com.apple.recovery.boot'): version += ' recovery' # Para FreeBSD: obtener datos del Kernel. ### TODO Revisar solución. if not version: type = 'BSD' file = f'{mntdir}/boot/kernel/kernel' if os.path.exists (file): lines = subprocess.run (['strings', file], capture_output=True, text=True).stdout.splitlines() release_search = list (filter (lambda x: re.search ('@.*RELEASE', x), lines)) if release_search: first, second, *rest = release_search[0].split() first = first.replace ('@(#)', '') version = f'{first} {second}' out = subprocess.run (['file', '--brief', file], capture_output=True, text=True).stdout if 'x86-64' in out: is64bit = ogGlobals.lang.MSG_64BIT # Para Solaris: leer el fichero de versión. ### TODO Revisar solución. if not version: type = 'Solaris' file = f'{mntdir}/etc/release' if os.path.exists (file): with open (file, 'r') as fd: version = fd.readline().strip # Para cargador GRUB, comprobar fichero de configuración. if not version: type = 'GrubLoader' for file in f'{mntdir}/grub/menu.lst', f'{mntdir}/boot/grub/menu.lst': if os.path.exists (file): VERSION = 'GRUB Loader' for entry in f'{mntdir}/grub/grub.cfg', f'{mntdir}/grub2/grub.cfg', f'{mntdir}/EFI/*/grub.cfg', f'{mntdir}/boot/grub/grub.cfg', f'{mntdir}/boot/grub2/grub.cfg', f'{mntdir}/boot/EFI/*/grub.cfg': for file in glob.glob (entry): if os.path.exists (file): version = 'GRUB2 Loader' # Mostrar resultado y salir sin errores. if version: return f"{type}:{version} {is64bit}" return None