"""Rebuild manifest JSON from DB rows, and canonicalize entries for parity. `scope_to_manifest` renders a ManifestScope back into the exact JSON structure GE-Enforce consumes (Version / _comment / Applications, plus Site for preinstall). `canonical_entry` reduces an Applications[] entry (from either the on-share file or a rebuilt scope) to only the fields the engine reads, dropping `_comment` and key order, so the parity harness can compare behavior not bytes. """ import json # Scalar entry fields: model attribute -> manifest key. Emitted only when set, # so a rebuilt entry has the same sparse key presence as the original. _SCALAR_FIELDS = [ ('name', 'Name'), ('entrytype', 'Type'), ('installer', 'Installer'), ('installargs', 'InstallArgs'), ('scriptpath', 'Script'), ('scriptargs', 'Args'), ('sourcepath', 'Source'), ('destination', 'Destination'), ('regpath', 'RegPath'), ('regname', 'RegName'), ('regtype', 'RegType'), ('detectionmethod', 'DetectionMethod'), ('detectionpath', 'DetectionPath'), ('detectionname', 'DetectionName'), ('detectionvalue', 'DetectionValue'), ('detectionpattern', 'DetectionPattern'), ('cmmversion', '_CmmVersion'), ('logfile', 'LogFile'), ('waittimeoutsec', 'WaitTimeoutSec'), ('applymode', 'ApplyMode'), ('updatewindow', 'UpdateWindow'), ] # Boolean flags emitted only when True (matches how preinstall.json carries them). _BOOL_FLAGS = [ ('preenrollment', 'PreEnrollment'), ('killafterdetection', 'KillAfterDetection'), ('pctypesstrict', 'PCTypesStrict'), ] # The complete set of engine-relevant keys the parity check compares. ENGINE_KEYS = ( [mk for _, mk in _SCALAR_FIELDS] + [mk for _, mk in _BOOL_FLAGS] + ['PCTypes', 'TargetHostnames', 'TargetMachineNumbers', 'RegValue', 'InUseCheck'] ) def entry_to_dict(entry): """Render a ManifestEntry model back into a manifest Applications[] entry.""" result = {} if entry.comment: result['_comment'] = entry.comment for attr, key in _SCALAR_FIELDS: value = getattr(entry, attr) if value is not None and value != '': result[key] = value # RegValue: stored as a raw JSON literal, reconstitute its real type. if entry.regvalue is not None: result['RegValue'] = json.loads(entry.regvalue) # Multi-value filters (only when present). if entry.pctypes: result['PCTypes'] = [p.pctypevalue for p in entry.pctypes] if entry.hostnames: result['TargetHostnames'] = [h.hostnamepattern for h in entry.hostnames] if entry.machinenumbers: result['TargetMachineNumbers'] = [m.machinenumber for m in entry.machinenumbers] for attr, key in _BOOL_FLAGS: if getattr(entry, attr): result[key] = True # InUseCheck (nested object + Processes[]). if entry.inusecheck: procs = [] for proc in entry.inusecheck.processes: pd = {'Name': proc.processname} if proc.exepath is not None: pd['ExePath'] = proc.exepath if proc.gracefulclosetimeoutsec is not None: pd['GracefulCloseTimeoutSec'] = proc.gracefulclosetimeoutsec procs.append(pd) result['InUseCheck'] = { 'Behavior': entry.inusecheck.behavior, 'Processes': procs, } return result def scope_to_manifest(scope): """Render a ManifestScope back into a full manifest.json structure.""" manifest = {'Version': scope.manifestversion} if scope.topcomment: manifest['_comment'] = scope.topcomment if scope.site: manifest['Site'] = scope.site manifest['Applications'] = [entry_to_dict(e) for e in scope.entries] return manifest def scope_to_json(scope, indent=2): """The serialized JSON text a client/export would receive.""" return json.dumps(scope_to_manifest(scope), indent=indent) def canonical_entry(entry_dict): """Reduce an Applications[] entry to engine-relevant fields for parity. Drops `_comment` and key order. Normalizes InUseCheck and the process list so two entries that behave identically compare equal regardless of source. """ canon = {} for key in ENGINE_KEYS: if key == 'InUseCheck': continue if key in entry_dict and entry_dict[key] is not None and entry_dict[key] != '': canon[key] = entry_dict[key] inuse = entry_dict.get('InUseCheck') if inuse: procs = [] for proc in inuse.get('Processes', []): pd = {'Name': proc.get('Name')} if proc.get('ExePath') is not None: pd['ExePath'] = proc.get('ExePath') if proc.get('GracefulCloseTimeoutSec') is not None: pd['GracefulCloseTimeoutSec'] = proc.get('GracefulCloseTimeoutSec') procs.append(pd) canon['InUseCheck'] = {'Behavior': inuse.get('Behavior'), 'Processes': procs} return canon