geenforce: harden allowlist + fix share-less kiosk client and display scope
- allowlist auth uses remote_addr, not the spoofable first X-Forwarded-For hop (adds _trusted_client_ip + a regression test); rate-limit path unchanged - client psm1: fix Set-StrictMode crashes reading absent keys in Get-ShopdbConfig (token-less mode) and Resolve-ShopdbPayloads (no-payload entries); validate the manifest response is JSON before overwriting the last-known-good cache - runner: pass the engine its required -InstallerRoot/-LogFile; create the log directory so enforce logging is not silently lost on a fresh kiosk - display scope: dispatcher writes an all-users Startup shortcut instead of Start-Process (SYSTEM cannot show a window in session 0), resolves the base URL from HKLM, and adds an always-on power/no-lock entry; tests updated for the 6-entry scope
This commit is contained in:
@@ -11,7 +11,7 @@ from plugins.geenforce.models import (
|
||||
)
|
||||
from plugins.geenforce.seed_display_scope import (
|
||||
seed_display_scope, build_display_manifest, build_dispatcher_script,
|
||||
DISPLAY_TYPE_TARGETS, SCOPE_NAME, DISPATCHER_FILENAME,
|
||||
DISPLAY_TYPE_TARGETS, SCOPE_NAME, DISPATCHER_FILENAME, ALWAYSON_FILENAME,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ def test_seed_creates_display_scope(db):
|
||||
# and do NOT inherit common.
|
||||
assert scope.iscommon is False
|
||||
|
||||
# Four Registry drift-heal entries + one PS1 dispatcher, in order.
|
||||
assert summary['entrycount'] == 5
|
||||
# Four Registry drift-heal entries + two inline PS1 (dispatcher, always-on).
|
||||
assert summary['entrycount'] == 6
|
||||
assert summary['entrytypes'] == ['Registry', 'Registry', 'Registry',
|
||||
'Registry', 'PS1']
|
||||
'Registry', 'PS1', 'PS1']
|
||||
|
||||
|
||||
def test_registry_entries_use_valuematches_detection(db):
|
||||
@@ -53,7 +53,8 @@ def test_registry_entries_use_valuematches_detection(db):
|
||||
def test_dispatcher_is_inline_and_data_driven(db):
|
||||
summary = seed_display_scope()
|
||||
|
||||
dispatcher = ManifestEntry.query.filter_by(entrytype='PS1').one()
|
||||
dispatcher = ManifestEntry.query.filter_by(
|
||||
payloadref=DISPATCHER_FILENAME).one()
|
||||
assert dispatcher.payloadsource == 'inline'
|
||||
assert dispatcher.payloadref == DISPATCHER_FILENAME
|
||||
assert dispatcher.payloadsha256 == summary['dispatchersha256']
|
||||
@@ -66,6 +67,29 @@ def test_dispatcher_is_inline_and_data_driven(db):
|
||||
assert display_type in scripttext
|
||||
assert route in scripttext
|
||||
assert 'display-type.txt' in scripttext
|
||||
# H3: SYSTEM writes an all-users Startup shortcut (CreateShortcut/.Save),
|
||||
# rather than Start-Process-ing Edge (which would open invisibly in session 0).
|
||||
assert 'CreateShortcut' in scripttext
|
||||
assert '.Save()' in scripttext
|
||||
assert 'Startup' in scripttext
|
||||
# H4: real base URL, not the localhost placeholder.
|
||||
assert 'localhost' not in scripttext
|
||||
|
||||
|
||||
def test_alwayson_entry_is_inline(db):
|
||||
summary = seed_display_scope()
|
||||
|
||||
alwayson = ManifestEntry.query.filter_by(
|
||||
payloadref=ALWAYSON_FILENAME).one()
|
||||
assert alwayson.entrytype == 'PS1'
|
||||
assert alwayson.payloadsource == 'inline'
|
||||
assert alwayson.detectionmethod == 'Always'
|
||||
assert alwayson.payloadsha256 == summary['alwaysonsha256']
|
||||
|
||||
payload = ManifestPayload.query.filter_by(entryid=alwayson.entryid).one()
|
||||
scripttext = payload.payloadbytes.decode('utf-8')
|
||||
assert 'powercfg' in scripttext
|
||||
assert 'monitor-timeout-ac 0' in scripttext
|
||||
|
||||
|
||||
def test_seed_publish_freezes_a_version(db):
|
||||
@@ -87,9 +111,10 @@ def test_seed_draft_is_idempotent(db):
|
||||
assert first['scopeid'] == second['scopeid']
|
||||
assert first['entrycount'] == second['entrycount']
|
||||
assert first['dispatchersha256'] == second['dispatchersha256']
|
||||
assert first['alwaysonsha256'] == second['alwaysonsha256']
|
||||
|
||||
entries = ManifestEntry.query.filter_by(scopeid=second['scopeid']).all()
|
||||
assert len(entries) == 5
|
||||
assert len(entries) == 6
|
||||
|
||||
|
||||
def test_build_manifest_has_no_smb_exe_payloads(db):
|
||||
|
||||
@@ -147,6 +147,17 @@ def test_empty_allowlist_keeps_token_required(client, db, app):
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_spoofed_forwarded_for_does_not_bypass_allowlist(client, db, app):
|
||||
# SECURITY: the allowlist uses remote_addr, not X-Forwarded-For. A caller
|
||||
# whose real IP (127.0.0.1) is NOT allowlisted must NOT gain token-less access
|
||||
# by forging X-Forwarded-For to an allowlisted address.
|
||||
_seed_and_publish(app)
|
||||
_set_allowlist(app, '10.134.48.0/23') # test client 127.0.0.1 is NOT in it
|
||||
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
||||
headers={'X-Forwarded-For': '10.134.48.10'})
|
||||
assert resp.status_code == 401, 'spoofed X-Forwarded-For bypassed the allowlist'
|
||||
|
||||
|
||||
def test_wrong_scope_rejected(client, db, app, auth_headers):
|
||||
_seed_and_publish(app)
|
||||
resp = client.post('/api/apitokens',
|
||||
|
||||
Reference in New Issue
Block a user