displays: the client module updates itself
Install-ShopdbKiosk.ps1 lays the enforce client down once at bootstrap and never refreshes it. So a client change rode the code deploy to the server and then sat one directory away from where kiosks actually fetch, waiting for someone to re-stage the installer bundle by hand - which is how the new display-type reporting reached prod and changed nothing on any kiosk. The module now ships as a manifest entry like everything else in this scope: inline over HTTPS, Hash detection against the exact bytes shipped, written to the same path the installer uses so bootstrap and self-update cannot disagree. Ordered first, so a stale client refreshes before anything leans on it. The installer keeps its real job - a fresh kiosk still needs something that can talk to shopdb - it just stops being the update path. Self-modifying by design: this module is what stages payloads, but PowerShell loads it into memory at start, so rewriting the file mid-run is harmless and lands on the next cycle. Pilot a client change on ONE kiosk before the fleet: a broken module cannot fetch its own replacement, and on a share-less display that means a site visit.
This commit is contained in:
@@ -27,10 +27,11 @@ def test_seed_creates_display_scope(db):
|
||||
# and do NOT inherit common.
|
||||
assert scope.iscommon is False
|
||||
|
||||
# Four Registry drift-heal entries + four inline PS1 (dispatcher, watchdog,
|
||||
# one-shot Edge force-update, always-on).
|
||||
assert summary['entrycount'] == 8
|
||||
assert summary['entrytypes'] == ['Registry', 'Registry', 'Registry',
|
||||
# The self-updating client module (File), then four Registry drift-heal
|
||||
# entries, then four inline PS1 (dispatcher, watchdog, one-shot Edge
|
||||
# force-update, always-on).
|
||||
assert summary['entrycount'] == 9
|
||||
assert summary['entrytypes'] == ['File', 'Registry', 'Registry', 'Registry',
|
||||
'Registry', 'PS1', 'PS1', 'PS1', 'PS1']
|
||||
|
||||
|
||||
@@ -117,16 +118,17 @@ def test_seed_draft_is_idempotent(db):
|
||||
assert first['alwaysonsha256'] == second['alwaysonsha256']
|
||||
assert first['watchdogsha256'] == second['watchdogsha256']
|
||||
assert first['forceupdatesha256'] == second['forceupdatesha256']
|
||||
assert first['clientmodulesha256'] == second['clientmodulesha256']
|
||||
|
||||
entries = ManifestEntry.query.filter_by(scopeid=second['scopeid']).all()
|
||||
assert len(entries) == 8
|
||||
assert len(entries) == 9
|
||||
# Exactly four inline payloads (dispatcher + watchdog + force-update +
|
||||
# always-on) exist
|
||||
# after a rebuild, not more - a payload-count invariant. (The underlying
|
||||
# re-publish FK crash only reproduces on MySQL, which enforces the
|
||||
# manifestpayloads FK; it was verified there directly. SQLite does not
|
||||
# enforce it.)
|
||||
assert ManifestPayload.query.count() == 4
|
||||
assert ManifestPayload.query.count() == 5
|
||||
|
||||
|
||||
def test_build_manifest_has_no_smb_exe_payloads(db):
|
||||
@@ -135,9 +137,12 @@ def test_build_manifest_has_no_smb_exe_payloads(db):
|
||||
# the inline dispatcher; everything else is a Registry policy heal.
|
||||
for entry in manifest['Applications']:
|
||||
assert entry.get('Installer') is None
|
||||
assert entry.get('Source') is None
|
||||
if entry['Type'] == 'PS1':
|
||||
# Every payload-bearing entry is delivered INLINE over HTTPS, never from
|
||||
# a share: a display has no SFLD credentials at all.
|
||||
if entry['Type'] in ('PS1', 'File'):
|
||||
assert entry.get('PayloadSource') == 'inline'
|
||||
else:
|
||||
assert entry.get('Source') is None
|
||||
|
||||
|
||||
def test_dispatcher_script_is_ascii():
|
||||
@@ -261,3 +266,34 @@ def test_forceupdate_script_does_not_launch_the_browser_itself():
|
||||
|
||||
def test_forceupdate_script_is_ascii():
|
||||
build_forceedgeupdate_script().encode('ascii')
|
||||
|
||||
|
||||
def _client_entry():
|
||||
return next(a for a in build_display_manifest()['Applications']
|
||||
if a['Name'].startswith('GE-Enforce client module'))
|
||||
|
||||
|
||||
def test_client_module_ships_with_a_hash_of_the_bytes_it_ships(db):
|
||||
"""The detection hash must match the shipped payload exactly. A mismatch
|
||||
means every kiosk rewrites the module on every cycle, forever."""
|
||||
import hashlib
|
||||
from plugins.geenforce.seed_display_scope import read_client_module
|
||||
|
||||
entry = _client_entry()
|
||||
assert entry['DetectionMethod'] == 'Hash'
|
||||
assert entry['DetectionValue'] == hashlib.sha256(read_client_module()).hexdigest()
|
||||
# Detection must look at where the file LANDS, not where it came from.
|
||||
assert entry['DetectionPath'] == entry['Destination']
|
||||
|
||||
|
||||
def test_client_module_lands_where_the_installer_puts_it(db):
|
||||
"""Bootstrap and self-update must write the SAME path, or a kiosk ends up
|
||||
running the installer's copy while the enforced one sits beside it."""
|
||||
entry = _client_entry()
|
||||
assert entry['Destination'].endswith('\\ShopdbEnforceClient.psm1')
|
||||
assert 'GE-Enforce' in entry['Destination']
|
||||
|
||||
|
||||
def test_client_module_is_first_so_a_stale_client_refreshes_first(db):
|
||||
names = [a['Name'] for a in build_display_manifest()['Applications']]
|
||||
assert names[0].startswith('GE-Enforce client module')
|
||||
|
||||
Reference in New Issue
Block a user