Fix defects found in session review of GE-Enforce plugin
All checks were successful
CI / backend (push) Successful in 1m43s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s

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>
This commit is contained in:
cproudlock
2026-07-12 20:46:09 -04:00
parent d894f054ac
commit 0dcd186820
9 changed files with 314 additions and 62 deletions

View File

@@ -131,6 +131,55 @@ def test_reference_share_round_trips_losslessly():
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()}