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>
This commit is contained in:
@@ -80,6 +80,34 @@ def test_entry_crud_and_reorder(client, db, auth_headers):
|
||||
assert [e['Name'] for e in entries] == ['Alpha']
|
||||
|
||||
|
||||
def test_entry_edit_preserves_full_fidelity(client, db, auth_headers):
|
||||
"""Editing an entry with the complete payload keeps InUseCheck ExePath +
|
||||
timeout, LogFile, and preinstall flags (the UI now sends all of them)."""
|
||||
scopeid = _create_scope(client, auth_headers)
|
||||
rich = {
|
||||
'Name': 'PC-DMIS', 'Type': 'MSI', 'Installer': 'apps/pcdmis.msi',
|
||||
'LogFile': 'C:\\Logs\\pcdmis.log',
|
||||
'PreEnrollment': True, 'PCTypesStrict': True,
|
||||
'InUseCheck': {'Behavior': 'CloseAndReopen', 'Processes': [
|
||||
{'Name': 'PCDLRN', 'ExePath': 'C:\\PCDLRN.exe',
|
||||
'GracefulCloseTimeoutSec': 15}]},
|
||||
}
|
||||
entryid = _add_entry(client, auth_headers, scopeid, rich)
|
||||
|
||||
# Re-send the same full payload (what the editor now does) and confirm
|
||||
# nothing is stripped.
|
||||
upd = client.put(f'/api/geenforce/entries/{entryid}', json=rich,
|
||||
headers=auth_headers)
|
||||
assert upd.status_code == 200
|
||||
entry = upd.get_json()['data']
|
||||
assert entry['LogFile'] == 'C:\\Logs\\pcdmis.log'
|
||||
assert entry['PreEnrollment'] is True
|
||||
assert entry['PCTypesStrict'] is True
|
||||
proc = entry['InUseCheck']['Processes'][0]
|
||||
assert proc['ExePath'] == 'C:\\PCDLRN.exe'
|
||||
assert proc['GracefulCloseTimeoutSec'] == 15
|
||||
|
||||
|
||||
def test_invalid_entry_type_rejected(client, db, auth_headers):
|
||||
scopeid = _create_scope(client, auth_headers)
|
||||
resp = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
|
||||
@@ -117,6 +145,34 @@ def test_simulate_cmm_version_gate(client, db, auth_headers):
|
||||
assert filtered['PC-DMIS 2016'] == ['_CmmVersion']
|
||||
|
||||
|
||||
def test_duplicate_entry_name_is_400_not_500(client, db, auth_headers):
|
||||
scopeid = _create_scope(client, auth_headers)
|
||||
_add_entry(client, auth_headers, scopeid, {'Name': 'Alpha', 'Type': 'MSI'})
|
||||
dup = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
|
||||
json={'Name': 'Alpha', 'Type': 'PS1'}, headers=auth_headers)
|
||||
assert dup.status_code == 400
|
||||
|
||||
|
||||
def test_simulate_pctypesstrict_disables_alias(client, db, auth_headers):
|
||||
"""A collections-only strict entry must NOT match a nocollections PC via the
|
||||
shared Standard alias group (mirrors the preinstall UDC entry)."""
|
||||
scopeid = _create_scope(client, auth_headers, 'preinstall')
|
||||
_add_entry(client, auth_headers, scopeid,
|
||||
{'Name': 'UDC (strict)', 'Type': 'EXE', 'Installer': 'apps/udc.exe',
|
||||
'PCTypes': ['gea-shopfloor-collections'], 'PCTypesStrict': True})
|
||||
_add_entry(client, auth_headers, scopeid,
|
||||
{'Name': 'UDC (loose)', 'Type': 'EXE', 'Installer': 'apps/udc.exe',
|
||||
'PCTypes': ['gea-shopfloor-collections']})
|
||||
|
||||
sim = client.get(
|
||||
f'/api/geenforce/scopes/{scopeid}/simulate?pctype=gea-shopfloor-nocollections',
|
||||
headers=auth_headers).get_json()['data']
|
||||
# loose entry matches via the Standard alias group; strict entry does not.
|
||||
assert sim['applied'] == ['UDC (loose)']
|
||||
filtered = {f['name']: f['filteredby'] for f in sim['filtered']}
|
||||
assert filtered['UDC (strict)'] == ['PCTypes']
|
||||
|
||||
|
||||
def test_simulate_machine_number_gate(client, db, auth_headers):
|
||||
scopeid = _create_scope(client, auth_headers, 'gea-shopfloor-collections')
|
||||
_add_entry(client, auth_headers, scopeid,
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -97,6 +97,24 @@ def test_latest_report_upserts_per_host(client, db, app, auth_headers):
|
||||
assert rows[0]['installed'] == 1
|
||||
|
||||
|
||||
def test_hostname_case_variation_keeps_single_current(client, db, app, auth_headers):
|
||||
"""A PC reporting its hostname in different casing must not leave two
|
||||
iscurrent rows (the demote is case-insensitive, matching the ilike read)."""
|
||||
_seed_and_publish(app)
|
||||
secret = _token(client, auth_headers, ['geenforce.report'])
|
||||
for host in ('WJCMM09', 'wjcmm09'):
|
||||
client.post('/api/geenforce/report',
|
||||
json={'hostname': host, 'scopename': 'gea-shopfloor-cmm',
|
||||
'appliedversion': 1, 'counts': {'installed': 1}},
|
||||
headers={'X-API-Key': secret})
|
||||
from plugins.geenforce.models import ManifestEnforcementReport
|
||||
with app.app_context():
|
||||
current = ManifestEnforcementReport.query.filter(
|
||||
ManifestEnforcementReport.hostname.ilike('WJCMM09'),
|
||||
ManifestEnforcementReport.iscurrent == True).all()
|
||||
assert len(current) == 1
|
||||
|
||||
|
||||
def test_status_failed_when_failures(client, db, app, auth_headers):
|
||||
_seed_and_publish(app)
|
||||
secret = _token(client, auth_headers, ['geenforce.report'])
|
||||
|
||||
Reference in New Issue
Block a user