Add GE-Enforce observed-state reporting: receipt + self-heal from PCs
All checks were successful
CI / backend (push) Successful in 1m37s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 8s

PCs now report enforcement results back to shopdb, closing the desired-vs-observed
loop.

- POST /api/geenforce/report (geenforce.report service token): each cycle a PC
  posts the published version it applied, install/skip/fail/filtered counts, and
  per-entry outcomes.
- Two tables: manifestenforcementreports (latest-per-host + history: applied
  version, enforcer version, counts, derived status ok/selfhealed/failed) and
  manifestenforcementresults (per entry: action installed/skipped/failed,
  selfhealed flag, exit code, warning/error message).
- RECEIVED: reports carry the applied version; the admin view derives
  receivedlatest by comparing it to the scope's current published version, so
  the fleet view shows which PCs picked up an update.
- SELF-HEAL: per-entry action captures drift correction (installed when it
  should already be present) vs skipped (already good) vs failed, with messages.
- Admin reads: GET /reports (fleet compliance rollup) and GET /reports/<id>
  (per-entry detail). New geenforce.report permission.
- Tables added to the (undeployed) 0001 baseline; geenforce.post_report is a
  service-token endpoint so it is exempt from the JWT authz sweep, like the
  collector blueprint. 8 reporting tests; full suite green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 17:04:07 -04:00
parent d85b33bd68
commit 6dc31c6149
10 changed files with 491 additions and 24 deletions

View File

@@ -0,0 +1,152 @@
"""GE-Enforce observed-state reporting: PCs POST enforcement results.
Covers the report ingest (applied version + per-entry self-heal outcomes), the
latest-per-host upsert, received-latest derivation (did the PC pick up the newest
published manifest?), status derivation (ok / selfhealed / failed), and the admin
fleet-compliance views.
"""
import json
from plugins.geenforce import service
SCOPE = {
'Version': '2.6',
'Applications': [
{'Name': 'Alpha', 'Type': 'MSI', 'Installer': 'apps/alpha.msi'},
{'Name': 'Beta', 'Type': 'PS1', 'Script': 'scripts/beta.ps1'},
],
}
def _seed_and_publish(app, scopename='gea-shopfloor-cmm'):
with app.app_context():
service.replace_scope_draft(scopename, 'runtime', SCOPE)
version = service.publish_scope(scopename, 'runtime', notes='v')
service.db.session.commit()
return version
def _token(client, auth_headers, scopes):
resp = client.post('/api/apitokens',
json={'name': 'svc', 'scopes': scopes},
headers=auth_headers)
assert resp.status_code == 201, resp.get_json()
return resp.get_json()['data']['secret']
def test_report_recorded_and_listed(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _token(client, auth_headers, ['geenforce.report'])
resp = client.post('/api/geenforce/report', json={
'hostname': 'WJCMM01', 'scopename': 'gea-shopfloor-cmm',
'appliedversion': 1, 'enforcerversion': '2.6',
'counts': {'installed': 1, 'skipped': 1, 'failed': 0, 'filtered': 0},
'results': [
{'name': 'Alpha', 'action': 'installed', 'selfhealed': True},
{'name': 'Beta', 'action': 'skipped'},
],
}, headers={'X-API-Key': secret})
assert resp.status_code == 200, resp.get_json()
assert resp.get_json()['data']['status'] == 'selfhealed'
listing = client.get('/api/geenforce/reports', headers=auth_headers)
rows = listing.get_json()['data']
assert len(rows) == 1
row = rows[0]
assert row['hostname'] == 'WJCMM01'
assert row['appliedversion'] == 1
assert row['latestversion'] == 1
assert row['receivedlatest'] is True
assert row['installed'] == 1 and row['skipped'] == 1
def test_received_latest_flips_when_new_version_published(client, db, app,
auth_headers):
_seed_and_publish(app)
secret = _token(client, auth_headers, ['geenforce.report'])
client.post('/api/geenforce/report',
json={'hostname': 'WJCMM02', 'scopename': 'gea-shopfloor-cmm',
'appliedversion': 1, 'counts': {}},
headers={'X-API-Key': secret})
# Publish a newer version; the PC is now behind.
with app.app_context():
service.publish_scope('gea-shopfloor-cmm', 'runtime', notes='v2')
service.db.session.commit()
row = client.get('/api/geenforce/reports?hostname=WJCMM02',
headers=auth_headers).get_json()['data'][0]
assert row['appliedversion'] == 1
assert row['latestversion'] == 2
assert row['receivedlatest'] is False
def test_latest_report_upserts_per_host(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _token(client, auth_headers, ['geenforce.report'])
for installed in (0, 1):
client.post('/api/geenforce/report',
json={'hostname': 'WJCMM03', 'scopename': 'gea-shopfloor-cmm',
'appliedversion': 1,
'counts': {'installed': installed}},
headers={'X-API-Key': secret})
rows = client.get('/api/geenforce/reports?hostname=WJCMM03',
headers=auth_headers).get_json()['data']
assert len(rows) == 1 # only the latest is current
assert rows[0]['installed'] == 1
def test_status_failed_when_failures(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _token(client, auth_headers, ['geenforce.report'])
resp = client.post('/api/geenforce/report',
json={'hostname': 'WJCMM04', 'scopename': 'gea-shopfloor-cmm',
'counts': {'failed': 1},
'results': [{'name': 'Alpha', 'action': 'failed',
'exitcode': 1603,
'message': 'MSI 1603'}]},
headers={'X-API-Key': secret})
assert resp.get_json()['data']['status'] == 'failed'
def test_report_detail_shows_per_entry_results(client, db, app, auth_headers):
_seed_and_publish(app)
secret = _token(client, auth_headers, ['geenforce.report'])
post = client.post('/api/geenforce/report',
json={'hostname': 'WJCMM05', 'scopename': 'gea-shopfloor-cmm',
'results': [
{'name': 'Alpha', 'action': 'installed',
'selfhealed': True},
{'name': 'Beta', 'action': 'failed',
'exitcode': 1, 'message': 'boom'}]},
headers={'X-API-Key': secret})
reportid = post.get_json()['data']['reportid']
detail = client.get(f'/api/geenforce/reports/{reportid}',
headers=auth_headers).get_json()['data']
results = {r['entryname']: r for r in detail['results']}
assert results['Alpha']['selfhealed'] is True
assert results['Beta']['action'] == 'failed'
assert results['Beta']['message'] == 'boom'
def test_report_requires_report_scope(client, db, app, auth_headers):
_seed_and_publish(app)
# A fetch-only token cannot report.
secret = _token(client, auth_headers, ['geenforce.fetch'])
resp = client.post('/api/geenforce/report',
json={'hostname': 'WJCMM06'},
headers={'X-API-Key': secret})
assert resp.status_code == 401
def test_report_unauthenticated_rejected(client, db, app):
resp = client.post('/api/geenforce/report', json={'hostname': 'WJCMM07'})
assert resp.status_code == 401
def test_report_missing_hostname_rejected(client, db, app, auth_headers):
secret = _token(client, auth_headers, ['geenforce.report'])
resp = client.post('/api/geenforce/report', json={'scopename': 'x'},
headers={'X-API-Key': secret})
assert resp.status_code == 400