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>
192 lines
8.7 KiB
Python
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
|