Files
shopdb-flask/tests/test_plugins/test_geenforce_parity.py
cproudlock 0dcd186820
All checks were successful
CI / backend (push) Successful in 1m43s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s
Fix defects found in session review of GE-Enforce plugin
Consolidated fixes from a three-dimension adversarial review.

Data-loss (HIGH): the manifest entry editor stripped fields the form did not
expose, because PUT /entries is a full reset-then-apply. The form now captures
everything - InUseCheck processes as structured name/ExePath/timeout rows (not
just names), LogFile, and the three preinstall flags as checkboxes; the dead
payload-source control (never wired) is removed. New regression test proves an
edit preserves ExePath/timeout/LogFile/PreEnrollment/PCTypesStrict.

Update-entry crash (found by that regression test): replacing an entry's
one-to-one InUseCheck (unique entryid) collided with the old row mid-flush ->
IntegrityError -> 400. update_entry now frees the old InUseCheck (delete+flush)
before populate re-inserts it.

Export truncation (MEDIUM): export_scope_to_share used a plain truncating open,
so a failed/partial write left the live on-share manifest (every PC reads it)
empty. Now writes a temp file in the same dir and os.replace() atomically.

Report dedup case bug (MEDIUM, confirmed by scratch test): the iscurrent demote
matched hostname case-sensitively while the read path uses ilike, so a PC
reporting different casing left two iscurrent rows and double-counted. Demote is
now case-insensitive; regression test added.

Simulator fidelity (MEDIUM): PCTypesStrict was captured but ignored by the
filter mirror, so the simulator wrongly matched a collections-only strict entry
to a nocollections PC via the shared Standard alias group. matches_pctype now
honors PCTypesStrict (disables alias expansion); test added.

Hardening: removed the dead/unscoped GEENFORCE_API_KEY env fallback (never wired
into config; tokens are the only path); create/update entry return 400 on a
duplicate Name instead of 500; parity now asserts scope-level Version/Site; a
new test guards real-manifest field lengths against column limits (the DB-free
parity harness can't see truncation); error handling added to the previously
unguarded editor + reports API calls.

Full suite green; naming + frontend build green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:46:09 -04:00

192 lines
8.7 KiB
Python

"""GE-Enforce P1 parity gate (Gate A): import + export is behaviorally lossless.
Runs the DB-free parity harness (parse -> in-memory rows -> render -> compare)
over a synthetic, site-neutral manifest that exercises every entry type,
detection method, targeting filter, RegValue typing, InUseCheck, and preinstall
flag. The synthetic fixture is what CI gates on (no real site data is vendored
into the framework repo). If the real GE-Enforce reference share is present
(dev), it is also checked.
"""
import os
import pytest
from plugins.geenforce.parity import run_parity, load_fixtures
from plugins.geenforce.importer import discover_share, load_manifest_file
# A site-neutral manifest covering the whole schema surface.
SYNTHETIC = {
'Version': '2.6',
'_comment': 'Synthetic parity fixture (not a real site manifest).',
'Applications': [
{
'_comment': 'MSI with Registry detection, version gate, InUseCheck.',
'Name': 'Sample MSI', 'Type': 'MSI',
'Installer': 'apps/sample.msi',
'InstallArgs': '/qn /norestart ALLUSERS=1',
'_CmmVersion': '2019',
'DetectionMethod': 'Registry',
'DetectionPath': 'HKLM:\\SOFTWARE\\Sample',
'DetectionName': 'DisplayVersion', 'DetectionValue': '1.2.3.4',
'InUseCheck': {
'Behavior': 'CloseAndReopen',
'Processes': [
{'Name': 'sample', 'ExePath': 'C:\\sample.exe',
'GracefulCloseTimeoutSec': 15},
{'Name': 'other'},
],
},
},
{
'Name': 'Sample EXE', 'Type': 'EXE', 'Installer': 'apps/sample.exe',
'InstallArgs': '/quiet', 'WaitTimeoutSec': 60,
'DetectionMethod': 'FileVersion',
'DetectionPath': 'C:\\sample.exe', 'DetectionValue': '1.0.0.0',
},
{
'Name': 'Sample PS1', 'Type': 'PS1', 'Script': 'scripts/run.ps1',
'Args': '-Force', 'DetectionMethod': 'Always',
},
{
'Name': 'Sample File', 'Type': 'File', 'Source': 'configs/app.json',
'Destination': 'C:\\ProgramData\\app.json',
'DetectionMethod': 'Hash', 'DetectionPath': 'C:\\ProgramData\\app.json',
'DetectionValue': 'a' * 64,
'PCTypes': ['gea-shopfloor-collections', 'gea-shopfloor-cmm'],
},
{
'Name': 'Sample Registry DWord', 'Type': 'Registry',
'RegPath': 'HKLM:\\SOFTWARE\\Sample', 'RegName': 'Enabled',
'RegValue': 1, 'RegType': 'DWord',
'DetectionMethod': 'ValueMatches',
'DetectionPath': 'HKLM:\\SOFTWARE\\Sample', 'DetectionName': 'Enabled',
},
{
'Name': 'Sample Registry String', 'Type': 'Registry',
'RegPath': 'HKLM:\\SOFTWARE\\Sample', 'RegName': 'Mode',
'RegValue': 'on', 'RegType': 'String',
},
{
'Name': 'Sample INF', 'Type': 'INF', 'Installer': 'configs/driver.inf',
'DetectionMethod': 'pnputil', 'DetectionPattern': 'SampleDriver',
},
{
'Name': 'Sample bay-gated', 'Type': 'CMD', 'Installer': 'scripts/bay.cmd',
'TargetMachineNumbers': ['3201', '3202'],
},
{
'Name': 'Sample host-gated', 'Type': 'BAT', 'Installer': 'scripts/host.bat',
'TargetHostnames': ['WJS-*', 'WJPC0615'],
},
{
'Name': 'Sample always-installs (no detection)', 'Type': 'PS1',
'Script': 'scripts/every.ps1',
},
{
'Name': 'Sample preinstall-flagged', 'Type': 'EXE',
'Installer': 'apps/pre.exe', 'PreEnrollment': True,
'PCTypesStrict': True, 'KillAfterDetection': True,
'PCTypes': ['gea-shopfloor-nocollections'],
},
],
}
REFERENCE_SHARE = '/home/camp/pxe-images/tsgwp00525-v2/shared/dt/shopfloor'
REFERENCE_PREINSTALL = '/home/camp/projects/pxe/playbook/preinstall/preinstall.json'
def test_synthetic_manifest_round_trips_losslessly():
"""The synthetic manifest imports + exports with full behavioral parity."""
results, ok = run_parity([('synthetic', 'runtime', SYNTHETIC)])
result = results[0]
assert result['entries_identical'] == result['entries_total'], result['firstdiff']
assert result['profiles_same'] == result['profiles_total'], result['firstdiff']
assert ok
def test_regvalue_numeric_type_preserved():
"""A DWord RegValue of 1 round-trips as int 1, not the string '1'."""
from plugins.geenforce.importer import build_scope
from plugins.geenforce.serializer import scope_to_manifest
rebuilt = scope_to_manifest(build_scope('synthetic', 'runtime', SYNTHETIC))
dword = next(e for e in rebuilt['Applications']
if e['Name'] == 'Sample Registry DWord')
assert dword['RegValue'] == 1
assert isinstance(dword['RegValue'], int)
@pytest.mark.skipif(not os.path.isdir(REFERENCE_SHARE),
reason='GE-Enforce reference share not present')
def test_reference_share_round_trips_losslessly():
"""Every real on-share manifest round-trips (the live Gate A)."""
manifests = list(discover_share(REFERENCE_SHARE))
if os.path.isfile(REFERENCE_PREINSTALL):
manifests.append(('preinstall', 'preinstall',
load_manifest_file(REFERENCE_PREINSTALL)))
results, ok = run_parity(manifests)
failed = [r for r in results if not r['passed']]
assert ok, f"parity failures: {[(r['scopename'], r['firstdiff']) for r in failed]}"
@pytest.mark.skipif(not os.path.isdir(REFERENCE_SHARE),
reason='GE-Enforce reference share not present')
def test_reference_manifests_fit_column_limits():
"""Guard the truncation blind spot: the DB-free parity harness can't catch a
value that exceeds its column length (Python strings are unbounded), so a
260-char DetectionPath would pass parity yet truncate on a real insert.
Assert every real-manifest string field fits its declared column, deriving
limits from the model so this never drifts.
"""
from plugins.geenforce.models import (
ManifestEntry, ManifestEntryPcType, ManifestEntryHostname,
ManifestEntryMachineNumber)
def limit(model, attr):
return model.__table__.columns[attr].type.length
# manifest key -> (model, column attribute)
entry_fields = {
'Name': 'name', 'Installer': 'installer', 'Script': 'scriptpath',
'Args': 'scriptargs', 'Source': 'sourcepath', 'Destination': 'destination',
'RegPath': 'regpath', 'RegName': 'regname', 'RegType': 'regtype',
'DetectionPath': 'detectionpath', 'DetectionName': 'detectionname',
'DetectionValue': 'detectionvalue', 'DetectionPattern': 'detectionpattern',
'_CmmVersion': 'cmmversion', 'LogFile': 'logfile',
'UpdateWindow': 'updatewindow', 'ApplyMode': 'applymode',
}
overflows = []
manifests = list(discover_share(REFERENCE_SHARE))
if os.path.isfile(REFERENCE_PREINSTALL):
manifests.append(('preinstall', 'preinstall',
load_manifest_file(REFERENCE_PREINSTALL)))
for scopename, _phase, manifest in manifests:
for entry in manifest.get('Applications', []):
for key, attr in entry_fields.items():
value = entry.get(key)
if isinstance(value, str) and len(value) > limit(ManifestEntry, attr):
overflows.append(f'{scopename}/{entry.get("Name")}.{key}')
for value in entry.get('PCTypes', []):
if len(value) > limit(ManifestEntryPcType, 'pctypevalue'):
overflows.append(f'{scopename}/{entry.get("Name")}.PCTypes')
for value in entry.get('TargetHostnames', []):
if len(value) > limit(ManifestEntryHostname, 'hostnamepattern'):
overflows.append(f'{scopename}/{entry.get("Name")}.TargetHostnames')
for value in entry.get('TargetMachineNumbers', []):
if len(str(value)) > limit(ManifestEntryMachineNumber, 'machinenumber'):
overflows.append(f'{scopename}/{entry.get("Name")}.TargetMachineNumbers')
assert not overflows, f'fields exceed column limits (would truncate): {overflows}'
def test_fixtures_cover_every_pctype():
"""The machine-profile fixtures include one of each imaging pctype."""
labels = {f['pctype'] for f in load_fixtures()}
for pctype in ('gea-shopfloor-collections', 'gea-shopfloor-nocollections',
'gea-shopfloor-common', 'gea-shopfloor-cmm',
'gea-shopfloor-genspect', 'gea-shopfloor-heattreat',
'gea-shopfloor-waxtrace', 'gea-shopfloor-partmarker',
'gea-shopfloor-display'):
assert pctype in labels