Add GE-Enforce observed-state reporting: receipt + self-heal from PCs
PCs now report enforcement results back to shopdb, closing the desired-vs-observed loop. - POST /api/geenforce/report (geenforce.report service token): each cycle a PC posts the published version it applied, install/skip/fail/filtered counts, and per-entry outcomes. - Two tables: manifestenforcementreports (latest-per-host + history: applied version, enforcer version, counts, derived status ok/selfhealed/failed) and manifestenforcementresults (per entry: action installed/skipped/failed, selfhealed flag, exit code, warning/error message). - RECEIVED: reports carry the applied version; the admin view derives receivedlatest by comparing it to the scope's current published version, so the fleet view shows which PCs picked up an update. - SELF-HEAL: per-entry action captures drift correction (installed when it should already be present) vs skipped (already good) vs failed, with messages. - Admin reads: GET /reports (fleet compliance rollup) and GET /reports/<id> (per-entry detail). New geenforce.report permission. - Tables added to the (undeployed) 0001 baseline; geenforce.post_report is a service-token endpoint so it is exempt from the JWT authz sweep, like the collector blueprint. 8 reporting tests; full suite green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -271,6 +271,66 @@ class ManifestPayload(db.Model):
|
||||
uploadedat = db.Column(db.DateTime, nullable=False)
|
||||
|
||||
|
||||
class ManifestEnforcementReport(db.Model):
|
||||
"""One enforcement cycle reported by a PC (observed state).
|
||||
|
||||
Each cycle the client POSTs its result for a scope: the published version it
|
||||
actually applied (so shopdb knows whether the PC RECEIVED the latest update),
|
||||
the enforcer version, and the installed/skipped/failed/filtered counts. The
|
||||
latest report per (hostname, scopename, phase) carries `iscurrent`; older
|
||||
ones are history. Pairs desired state (the manifest) with observed state.
|
||||
"""
|
||||
__tablename__ = 'manifestenforcementreports'
|
||||
|
||||
reportid = db.Column(db.Integer, primary_key=True)
|
||||
hostname = db.Column(db.String(100), nullable=False, index=True)
|
||||
scopename = db.Column(db.String(64), nullable=False)
|
||||
phase = db.Column(db.String(16), nullable=False, default='runtime')
|
||||
# Published version the client actually ran; compare to the scope's current
|
||||
# published version to see whether this PC received the latest manifest.
|
||||
appliedversion = db.Column(db.Integer, nullable=True)
|
||||
enforcerversion = db.Column(db.String(20), nullable=True)
|
||||
installedcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
skippedcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
failedcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
filteredcount = db.Column(db.Integer, nullable=False, default=0)
|
||||
# 'ok' | 'failed' (any failure) | 'selfhealed' (drift corrected, no failure).
|
||||
status = db.Column(db.String(16), nullable=False, default='ok')
|
||||
lastcheckin = db.Column(db.DateTime, nullable=True) # PC-reported time
|
||||
receivedat = db.Column(db.DateTime, nullable=False) # server time
|
||||
iscurrent = db.Column(db.Boolean, nullable=False, default=True, index=True)
|
||||
|
||||
results = db.relationship(
|
||||
'ManifestEnforcementResult', back_populates='report',
|
||||
cascade='all, delete-orphan', lazy='selectin')
|
||||
|
||||
__table_args__ = (
|
||||
db.Index('idx_report_host_scope', 'hostname', 'scopename', 'phase'),
|
||||
)
|
||||
|
||||
|
||||
class ManifestEnforcementResult(db.Model):
|
||||
"""One entry's outcome within an enforcement cycle (self-heal detail)."""
|
||||
__tablename__ = 'manifestenforcementresults'
|
||||
|
||||
resultid = db.Column(db.Integer, primary_key=True)
|
||||
reportid = db.Column(
|
||||
db.Integer,
|
||||
db.ForeignKey('manifestenforcementreports.reportid', ondelete='CASCADE'),
|
||||
nullable=False, index=True)
|
||||
entryname = db.Column(db.String(128), nullable=False)
|
||||
# 'installed' (action fired - a self-heal when it should already be present),
|
||||
# 'skipped' (detected present), 'failed', 'filtered'.
|
||||
action = db.Column(db.String(16), nullable=False)
|
||||
# True when this install was a drift correction (self-heal), not a first
|
||||
# install. Client-supplied; defaults to whether the action installed.
|
||||
selfhealed = db.Column(db.Boolean, nullable=False, default=False)
|
||||
exitcode = db.Column(db.Integer, nullable=True)
|
||||
message = db.Column(db.Text, nullable=True) # warning / error text
|
||||
|
||||
report = db.relationship('ManifestEnforcementReport', back_populates='results')
|
||||
|
||||
|
||||
class PcTypeAlias(db.Model):
|
||||
"""Mirror of the engine lib's PCTypes alias graph (Install-FromManifest.ps1).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user