"""GE-Enforce manifest-store models. The manifest that GE-Enforce runs on each PC becomes shopdb data. A `ManifestScope` is one imaging PC type (a `gea-shopfloor-*` scope, the `common` fleet-wide scope, or the single flat `preinstall` scope). Each scope owns an ordered list of `ManifestEntry` rows - one per Applications[] entry in the on-share manifest.json - plus child rows for the multi-value targeting filters and the nested InUseCheck. Design choices (see docs/proposals/ge-enforce-plugin.md): - ONE wide entries table with an `entrytype` discriminator and nullable per-type columns. Not STI, not a JSON blob: the whole fleet is ~64 entries, so sparse columns are free and every field stays queryable + plain-SQL readable by IT. - Array order IS execution order, so `sortorder` is a real column and the contract; config-restore entries sit after their installer on purpose. - `computertypeid` / `measuringtooltypeid` are SOFT references (plain Integer, no DB foreign key) to other plugins' tables, which may not be installed. - Published manifests are immutable snapshots that freeze the rendered JSON document (`ManifestPublishedVersion`); the draft (`ManifestEntry`) is never served to a client. """ from shopdb.api import db, BaseModel # Allowed enumerations, validated in the API/service layer (stored as strings # for portability, matching the rest of the codebase). PHASES = ('runtime', 'preinstall') ENTRY_TYPES = ('MSI', 'EXE', 'CMD', 'BAT', 'PS1', 'INF', 'File', 'Registry') REG_TYPES = ('String', 'DWord', 'QWord', 'MultiString', 'ExpandString', 'Binary') PAYLOAD_SOURCES = ('smb', 'http', 'inline') DETECTION_METHODS = ('Registry', 'File', 'FileVersion', 'Hash', 'MarkerFile', 'ValueMatches', 'pnputil', 'Always') APPLY_MODES = ('Nightly', 'Immediate', 'ImmediateReboot') INUSE_BEHAVIORS = ('Defer', 'CloseAndReopen', 'ForceClose', 'ScheduleForReboot') class ManifestScope(BaseModel): """One imaging PC type / manifest scope (the working/draft head).""" __tablename__ = 'manifestscopes' scopeid = db.Column(db.Integer, primary_key=True) # 'common', 'gea-shopfloor-cmm', 'preinstall', ... scopename = db.Column(db.String(64), nullable=False) # 'runtime' (per-pctype manifest.json) or 'preinstall' (one flat manifest) phase = db.Column(db.String(16), nullable=False, default='runtime') # Soft refs (no FK): the plugins that own these tables are optional. computertypeid = db.Column(db.Integer, nullable=True) measuringtooltypeid = db.Column(db.Integer, nullable=True) manifestversion = db.Column(db.String(16), nullable=False, default='1.0') description = db.Column(db.String(255), nullable=True) # The manifest-level '_comment' (documentation), preserved verbatim so # export-to-share round-trips it. Excluded from behavioral parity. topcomment = db.Column(db.Text, nullable=True) iscommon = db.Column(db.Boolean, nullable=False, default=False) # Preinstall manifests carry a top-level 'Site' field. site = db.Column(db.String(100), nullable=True) entries = db.relationship( 'ManifestEntry', back_populates='scope', cascade='all, delete-orphan', order_by='ManifestEntry.sortorder', lazy='selectin') publishedversions = db.relationship( 'ManifestPublishedVersion', back_populates='scope', cascade='all, delete-orphan', order_by='ManifestPublishedVersion.versionnumber', lazy='dynamic') __table_args__ = ( db.UniqueConstraint('scopename', 'phase', name='uq_scope_name_phase'), ) def __repr__(self): return f"" class ManifestEntry(BaseModel): """One Applications[] entry (draft). Wide table, entrytype discriminator.""" __tablename__ = 'manifestentries' entryid = db.Column(db.Integer, primary_key=True) scopeid = db.Column( db.Integer, db.ForeignKey('manifestscopes.scopeid', ondelete='CASCADE'), nullable=False, index=True) sortorder = db.Column(db.Integer, nullable=False, default=0) name = db.Column(db.String(128), nullable=False) entrytype = db.Column(db.String(16), nullable=False) comment = db.Column(db.Text, nullable=True) # manifest '_comment' # Type-specific payload references (nullable; validated per entrytype). installer = db.Column(db.String(255), nullable=True) installargs = db.Column(db.Text, nullable=True) # can run long scriptpath = db.Column(db.String(255), nullable=True) # 'Script' scriptargs = db.Column(db.String(255), nullable=True) # 'Args' sourcepath = db.Column(db.String(255), nullable=True) # 'Source' destination = db.Column(db.String(255), nullable=True) regpath = db.Column(db.String(255), nullable=True) regname = db.Column(db.String(128), nullable=True) # Raw JSON literal preserved verbatim (1 vs "1"); DWord typing depends on it. regvalue = db.Column(db.Text, nullable=True) regtype = db.Column(db.String(16), nullable=True) # Payload transport + integrity (integrity hash is SEPARATE from detection). payloadsource = db.Column(db.String(8), nullable=False, default='smb') payloadref = db.Column(db.String(512), nullable=True) payloadsha256 = db.Column(db.String(64), nullable=True) # Detection (decides whether the action fires / self-heals). detectionmethod = db.Column(db.String(16), nullable=True) detectionpath = db.Column(db.String(255), nullable=True) detectionname = db.Column(db.String(128), nullable=True) detectionvalue = db.Column(db.String(255), nullable=True) detectionpattern = db.Column(db.String(255), nullable=True) # Gates + control. cmmversion = db.Column(db.String(16), nullable=True) # '_CmmVersion' logfile = db.Column(db.String(255), nullable=True) waittimeoutsec = db.Column(db.Integer, nullable=True) applymode = db.Column(db.String(16), nullable=True) # inert in engine today updatewindow = db.Column(db.String(11), nullable=True) # 'HH:MM-HH:MM', inert # Preinstall-only flags. preenrollment = db.Column(db.Boolean, nullable=False, default=False) killafterdetection = db.Column(db.Boolean, nullable=False, default=False) pctypesstrict = db.Column(db.Boolean, nullable=False, default=False) scope = db.relationship('ManifestScope', back_populates='entries') pctypes = db.relationship( 'ManifestEntryPcType', back_populates='entry', cascade='all, delete-orphan', order_by='ManifestEntryPcType.sortorder', lazy='selectin') hostnames = db.relationship( 'ManifestEntryHostname', back_populates='entry', cascade='all, delete-orphan', order_by='ManifestEntryHostname.sortorder', lazy='selectin') machinenumbers = db.relationship( 'ManifestEntryMachineNumber', back_populates='entry', cascade='all, delete-orphan', order_by='ManifestEntryMachineNumber.sortorder', lazy='selectin') inusecheck = db.relationship( 'ManifestInUseCheck', back_populates='entry', uselist=False, cascade='all, delete-orphan', lazy='selectin') __table_args__ = ( db.UniqueConstraint('scopeid', 'name', name='uq_entry_scope_name'), db.Index('idx_entry_scope_order', 'scopeid', 'sortorder'), ) def __repr__(self): return f"" class ManifestEntryPcType(db.Model): """One value of an entry's PCTypes filter (verbatim, incl. '*' and aliases).""" __tablename__ = 'manifestentrypctypes' entrypctypeid = db.Column(db.Integer, primary_key=True) entryid = db.Column( db.Integer, db.ForeignKey('manifestentries.entryid', ondelete='CASCADE'), nullable=False, index=True) sortorder = db.Column(db.Integer, nullable=False, default=0) pctypevalue = db.Column(db.String(64), nullable=False) entry = db.relationship('ManifestEntry', back_populates='pctypes') class ManifestEntryHostname(db.Model): """One value of an entry's TargetHostnames filter (wildcards kept verbatim).""" __tablename__ = 'manifestentryhostnames' entryhostnameid = db.Column(db.Integer, primary_key=True) entryid = db.Column( db.Integer, db.ForeignKey('manifestentries.entryid', ondelete='CASCADE'), nullable=False, index=True) sortorder = db.Column(db.Integer, nullable=False, default=0) hostnamepattern = db.Column(db.String(64), nullable=False) entry = db.relationship('ManifestEntry', back_populates='hostnames') class ManifestEntryMachineNumber(db.Model): """One value of an entry's TargetMachineNumbers filter.""" __tablename__ = 'manifestentrymachinenumbers' entrymachinenumberid = db.Column(db.Integer, primary_key=True) entryid = db.Column( db.Integer, db.ForeignKey('manifestentries.entryid', ondelete='CASCADE'), nullable=False, index=True) sortorder = db.Column(db.Integer, nullable=False, default=0) machinenumber = db.Column(db.String(16), nullable=False) entry = db.relationship('ManifestEntry', back_populates='machinenumbers') class ManifestInUseCheck(db.Model): """The InUseCheck object on an entry (0..1), with its Processes[] children.""" __tablename__ = 'manifestinusechecks' inusecheckid = db.Column(db.Integer, primary_key=True) entryid = db.Column( db.Integer, db.ForeignKey('manifestentries.entryid', ondelete='CASCADE'), nullable=False, unique=True) behavior = db.Column(db.String(20), nullable=False) entry = db.relationship('ManifestEntry', back_populates='inusecheck') processes = db.relationship( 'ManifestInUseCheckProcess', back_populates='inusecheck', cascade='all, delete-orphan', order_by='ManifestInUseCheckProcess.sortorder', lazy='selectin') class ManifestInUseCheckProcess(db.Model): """One process in an InUseCheck's Processes[] list.""" __tablename__ = 'manifestinusecheckprocesses' inusecheckprocessid = db.Column(db.Integer, primary_key=True) inusecheckid = db.Column( db.Integer, db.ForeignKey('manifestinusechecks.inusecheckid', ondelete='CASCADE'), nullable=False, index=True) sortorder = db.Column(db.Integer, nullable=False, default=0) processname = db.Column(db.String(64), nullable=False) exepath = db.Column(db.String(255), nullable=True) # Null = engine default (10); do not bake the default into the row. gracefulclosetimeoutsec = db.Column(db.Integer, nullable=True) inusecheck = db.relationship('ManifestInUseCheck', back_populates='processes') class ManifestPublishedVersion(db.Model): """Immutable published snapshot: the frozen rendered JSON document. The client is always served the `iscurrent` snapshot for a scope, never the live draft. Rollback flips `iscurrent`. Freezing the text (not row-mirroring) makes immutability structural and rollback a one-flag change. """ __tablename__ = 'manifestpublishedversions' publishedversionid = db.Column(db.Integer, primary_key=True) scopeid = db.Column( db.Integer, db.ForeignKey('manifestscopes.scopeid', ondelete='CASCADE'), nullable=False, index=True) versionnumber = db.Column(db.Integer, nullable=False) # MEDIUMTEXT on MySQL (a full scope with comments can exceed 64 KB TEXT). manifestjson = db.Column(db.Text(length=16777215), nullable=False) publishedat = db.Column(db.DateTime, nullable=False) publishedby = db.Column(db.Integer, nullable=True) # soft ref to users iscurrent = db.Column(db.Boolean, nullable=False, default=False) notes = db.Column(db.String(255), nullable=True) scope = db.relationship('ManifestScope', back_populates='publishedversions') __table_args__ = ( db.UniqueConstraint('scopeid', 'versionnumber', name='uq_published_scope_version'), ) class ManifestPayload(db.Model): """Inline payload bytes for payloadsource='inline' (small scripts/configs).""" __tablename__ = 'manifestpayloads' payloadid = db.Column(db.Integer, primary_key=True) entryid = db.Column( db.Integer, db.ForeignKey('manifestentries.entryid', ondelete='CASCADE'), nullable=False, index=True) filename = db.Column(db.String(255), nullable=False) contenttype = db.Column(db.String(128), nullable=True) payloadbytes = db.Column(db.LargeBinary(length=16777215), nullable=False) payloadsha256 = db.Column(db.String(64), nullable=False) uploadedat = db.Column(db.DateTime, nullable=False) class PcTypeAlias(db.Model): """Mirror of the engine lib's PCTypes alias graph (Install-FromManifest.ps1). Rows sharing an `aliasgroup` are one alias set. Server-side resolve/validate only; the engine lib stays the single source of truth (never inverted). """ __tablename__ = 'pctypealiases' aliasid = db.Column(db.Integer, primary_key=True) aliasgroup = db.Column(db.Integer, nullable=False, index=True) aliasname = db.Column(db.String(64), nullable=False) __table_args__ = ( db.UniqueConstraint('aliasgroup', 'aliasname', name='uq_alias_group_name'), )