Persist Phase 2 system telemetry

Adds rebootreasonsjson, bootinfojson and defenderjson with their migration in the same change, so the model never references a column the schema has not applied.

rebootreasons is not redundant with the existing pendingreboot column. That column is a single bool OR-ed from three registry signals and cannot say which fired, and the difference matters: a staged file rename means the box is mid-servicing, while a lone servicing key can sit set for weeks on a machine nobody rebooted.

Null means never reported and is deliberately distinct from an empty list, which means checked and nothing pending. Backfilling [] would assert every pre-existing endpoint was confirmed clean.
This commit is contained in:
benjamin.baker
2026-09-05 06:41:20 -04:00
parent f1e8fe6b4a
commit 2c4d8ab4cb
3 changed files with 128 additions and 0 deletions

View File

@@ -54,6 +54,7 @@ KNOWN_PAYLOAD_KEYS = frozenset({
'scheduledtasks', 'applications', 'events',
'disks', 'bios', 'memory', 'processor', 'osinfo', 'domain',
'pendingreboot', 'usercontext', 'diskhealth',
'rebootreasons', 'bootinfo', 'defender',
})
@@ -790,6 +791,28 @@ class EndpointsPlugin(BasePlugin):
'precomputed predictfailure; null there '
'means the drive did not answer.',
},
'rebootreasons': {
'type': 'array',
'description': 'Which signal(s) drove pendingreboot: '
'componentbasedservicing, windowsupdate, '
'pendingfilerename. Empty array means '
'checked and nothing pending; omit when '
'the check failed.',
},
'bootinfo': {
'type': 'object',
'description': 'hiberbootenabled (fast startup is '
'configured) and lastboottype (what the '
'last boot actually did). Uptime is not '
'trustworthy when fast startup is in '
'play.',
},
'defender': {
'type': 'object',
'description': 'Defender service/realtime state and '
'signature and scan ages in days. '
'Persisted but not surfaced.',
},
'usercontext': {
'type': 'object',
'description': 'Session-scoped data collected by a probe '
@@ -1035,6 +1058,12 @@ class EndpointsPlugin(BasePlugin):
state.pendingreboot = payload.get('pendingreboot')
if 'diskhealth' in payload:
state.diskhealthjson = _as_json(payload.get('diskhealth'))
if 'rebootreasons' in payload:
state.rebootreasonsjson = _as_json(payload.get('rebootreasons'))
if 'bootinfo' in payload:
state.bootinfojson = _as_json(payload.get('bootinfo'))
if 'defender' in payload:
state.defenderjson = _as_json(payload.get('defender'))
# Session-scoped probe output. The agent repeats its last result between
# probe runs and omits the key entirely when nobody is signed in, so the