Fleet-install compliance for app-linked manifest entries: new service compliance_for_scope + GET /geenforce/scopes/<id>/compliance count active ComputerInstalledApp rows by curated appid (null-safe when computers plugin absent). ManifestEditor gains a compliance panel. Curated appid stays shopdb metadata and never enters manifest JSON, so behavioral parity is unaffected. Inline manifest payloads: store_inline_payload (sha256, 1MB cap, payloadsource='inline') + POST/GET /geenforce/entries/<id>/payload; editor gains an upload control. Entry payload metadata surfaced in _entry_payload. Frontend test harness: extract the editor's entry-form logic into pure entryForm.js (buildEntryPayload, describeEntry, availableEntryTypes, scope gates, ...) and cover it with 45 vitest tests. ManifestEditor now imports those helpers, so the tests exercise the shipped code path (no duplication). 908 backend tests pass; vitest 45 pass; frontend build green; naming green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
167 lines
7.1 KiB
Python
167 lines
7.1 KiB
Python
"""GE-Enforce compliance: fleet-install coverage per app-linked entry.
|
|
|
|
Compliance pairs each app-linked manifest entry with collected PC data
|
|
(ComputerInstalledApp) to answer 'how many PCs have this app, and how many on
|
|
the expected version'. Only entries carrying a curated appid appear; unlinked
|
|
entries are skipped. FileVersion detection supplies the expected-version target;
|
|
every other detection method yields a null target (no meaningful match). When
|
|
the optional computers plugin is absent, counts degrade to null gracefully.
|
|
"""
|
|
|
|
from shopdb.core.models import Asset, AssetType, Application
|
|
|
|
from plugins.geenforce import service
|
|
|
|
|
|
def _seed_app(db, appname):
|
|
app_row = Application(appname=appname)
|
|
db.session.add(app_row)
|
|
db.session.commit()
|
|
return app_row.appid
|
|
|
|
|
|
def _seed_pc_with_app(db, hostname, appid, installedversion, isactive=True):
|
|
"""Create a Computer + one ComputerInstalledApp row for it."""
|
|
from plugins.computers.models import Computer, ComputerInstalledApp
|
|
atype = AssetType.query.filter_by(assettype='computer').first()
|
|
if not atype:
|
|
atype = AssetType(assettype='computer')
|
|
db.session.add(atype)
|
|
db.session.flush()
|
|
asset = Asset(assetnumber=f'PC-{hostname}', assettypeid=atype.assettypeid)
|
|
db.session.add(asset)
|
|
db.session.flush()
|
|
comp = Computer(assetid=asset.assetid, hostname=hostname)
|
|
db.session.add(comp)
|
|
db.session.flush()
|
|
link = ComputerInstalledApp(computerid=comp.computerid, appid=appid,
|
|
installedversion=installedversion,
|
|
isactive=isactive)
|
|
db.session.add(link)
|
|
db.session.commit()
|
|
return comp
|
|
|
|
|
|
def _create_scope(client, auth_headers, name='gea-shopfloor-cmm'):
|
|
resp = client.post('/api/geenforce/scopes',
|
|
json={'scopename': name, 'phase': 'runtime'},
|
|
headers=auth_headers)
|
|
assert resp.status_code == 201, resp.get_json()
|
|
return resp.get_json()['data']['scopeid']
|
|
|
|
|
|
def _add_entry(client, auth_headers, scopeid, entry):
|
|
resp = client.post(f'/api/geenforce/scopes/{scopeid}/entries',
|
|
json=entry, headers=auth_headers)
|
|
assert resp.status_code == 201, resp.get_json()
|
|
return resp.get_json()['data']['entryid']
|
|
|
|
|
|
def test_compliance_only_lists_app_linked_entries(client, db, auth_headers):
|
|
appid = _seed_app(db, 'PC-DMIS')
|
|
scopeid = _create_scope(client, auth_headers)
|
|
_add_entry(client, auth_headers, scopeid,
|
|
{'Name': 'PC-DMIS 2019', 'Type': 'MSI', 'appid': appid,
|
|
'DetectionMethod': 'FileVersion', 'DetectionPath': 'C:\\p.exe',
|
|
'DetectionValue': '2019 R1'})
|
|
# An unlinked entry (no appid) must NOT appear.
|
|
_add_entry(client, auth_headers, scopeid,
|
|
{'Name': 'Unlinked', 'Type': 'MSI', 'Installer': 'apps/x.msi'})
|
|
|
|
data = client.get(f'/api/geenforce/scopes/{scopeid}/compliance',
|
|
headers=auth_headers).get_json()['data']
|
|
assert data['computersplugin'] is True
|
|
assert [r['entryname'] for r in data['rows']] == ['PC-DMIS 2019']
|
|
row = data['rows'][0]
|
|
assert row['appid'] == appid
|
|
assert row['appname'] == 'PC-DMIS'
|
|
assert row['expectedversion'] == '2019 R1'
|
|
|
|
|
|
def test_compliance_counts_active_installs_and_version_match(client, db,
|
|
auth_headers):
|
|
appid = _seed_app(db, 'PC-DMIS')
|
|
# Two PCs on the expected version, one on a different version, one inactive.
|
|
_seed_pc_with_app(db, 'CMM01', appid, '2019 R1')
|
|
_seed_pc_with_app(db, 'CMM02', appid, '2019 R1')
|
|
_seed_pc_with_app(db, 'CMM03', appid, '2016 R2')
|
|
_seed_pc_with_app(db, 'CMM04', appid, '2019 R1', isactive=False)
|
|
|
|
scopeid = _create_scope(client, auth_headers)
|
|
_add_entry(client, auth_headers, scopeid,
|
|
{'Name': 'PC-DMIS 2019', 'Type': 'MSI', 'appid': appid,
|
|
'DetectionMethod': 'FileVersion', 'DetectionPath': 'C:\\p.exe',
|
|
'DetectionValue': '2019 R1'})
|
|
|
|
row = client.get(f'/api/geenforce/scopes/{scopeid}/compliance',
|
|
headers=auth_headers).get_json()['data']['rows'][0]
|
|
# Inactive row excluded: 3 active installs, 2 on the expected version.
|
|
assert row['installedcount'] == 3
|
|
assert row['versionmatchcount'] == 2
|
|
assert '2 of 3' in row['coveragenote']
|
|
|
|
|
|
def test_compliance_non_fileversion_has_null_version_target(client, db,
|
|
auth_headers):
|
|
appid = _seed_app(db, 'eDNC')
|
|
_seed_pc_with_app(db, 'DNC01', appid, '5.0')
|
|
scopeid = _create_scope(client, auth_headers)
|
|
# Registry detection is not a version string -> no expected-version target.
|
|
_add_entry(client, auth_headers, scopeid,
|
|
{'Name': 'eDNC', 'Type': 'MSI', 'appid': appid,
|
|
'DetectionMethod': 'Registry',
|
|
'DetectionPath': 'HKLM\\Software\\eDNC'})
|
|
|
|
row = client.get(f'/api/geenforce/scopes/{scopeid}/compliance',
|
|
headers=auth_headers).get_json()['data']['rows'][0]
|
|
assert row['expectedversion'] is None
|
|
assert row['installedcount'] == 1
|
|
assert row['versionmatchcount'] is None
|
|
assert 'no version target' in row['coveragenote']
|
|
|
|
|
|
def test_compliance_not_installed_anywhere(client, db, auth_headers):
|
|
appid = _seed_app(db, 'Orphan')
|
|
scopeid = _create_scope(client, auth_headers)
|
|
_add_entry(client, auth_headers, scopeid,
|
|
{'Name': 'Orphan', 'Type': 'MSI', 'appid': appid,
|
|
'DetectionMethod': 'FileVersion', 'DetectionPath': 'C:\\o.exe',
|
|
'DetectionValue': '1.0'})
|
|
|
|
row = client.get(f'/api/geenforce/scopes/{scopeid}/compliance',
|
|
headers=auth_headers).get_json()['data']['rows'][0]
|
|
assert row['installedcount'] == 0
|
|
assert row['versionmatchcount'] == 0
|
|
assert row['coveragenote'] == 'not installed on any collected PC'
|
|
|
|
|
|
def test_compliance_scope_not_found(client, db, auth_headers):
|
|
resp = client.get('/api/geenforce/scopes/999999/compliance',
|
|
headers=auth_headers)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
def test_compliance_requires_manage_permission(client, db, member_headers):
|
|
resp = client.get('/api/geenforce/scopes/1/compliance',
|
|
headers=member_headers)
|
|
assert resp.status_code == 403
|
|
|
|
|
|
def test_compliance_graceful_without_computers_plugin(client, db, auth_headers,
|
|
monkeypatch):
|
|
appid = _seed_app(db, 'PC-DMIS')
|
|
scopeid = _create_scope(client, auth_headers)
|
|
_add_entry(client, auth_headers, scopeid,
|
|
{'Name': 'PC-DMIS 2019', 'Type': 'MSI', 'appid': appid,
|
|
'DetectionMethod': 'FileVersion', 'DetectionPath': 'C:\\p.exe',
|
|
'DetectionValue': '2019 R1'})
|
|
monkeypatch.setattr(service, '_installed_app_model', lambda: None)
|
|
|
|
data = client.get(f'/api/geenforce/scopes/{scopeid}/compliance',
|
|
headers=auth_headers).get_json()['data']
|
|
assert data['computersplugin'] is False
|
|
row = data['rows'][0]
|
|
assert row['installedcount'] is None
|
|
assert row['versionmatchcount'] is None
|
|
assert 'computers plugin not installed' in row['coveragenote']
|