Files
shopdb-flask/plugins/endpoints/eventcatalog.py
benjamin.baker 862b290545 Report each Windows event once, and persist the inventory already sent
Curated events were re-read from the newest 25 entries per channel on every
collect, and the server appended whatever arrived. One real event was
therefore stored again on every cadence tick for as long as it stayed in
that window, so event counts measured how long a box had been up rather
than what had happened to it.

The agent now keeps a per-channel watermark of the last Windows event
record id it reported, persisted under ProgramData so a service restart
does not replay history, and sends eventrecordid with each event. The
server dedupes on (assetid, eventchannel, eventrecordid) behind a unique
index, so a replay from an older agent cannot double-store either.

Six inventory keys the agent had been sending all along - bios, disks,
domain, memory, osinfo, processor - were parsed, logged as unpersisted,
and dropped. They now have columns and are written on check-in, along
with user context, pending reboot, and the cpupercent/memorypercent/
idleseconds fields that were declared in the contract but always null.

Session-scoped data cannot be read from the agent's LocalSystem session 0
at all: mapped drives and user-owned listeners do not exist there. A probe
runs in the interactive user's session and hands results back through a
spool file. It is not yet producing output on a live endpoint; the column
and panel are in place and read empty until that is fixed.

Also bounds the user-context script wait. It was WaitForSingleObject with
INFINITE, so a hung script pinned the command worker permanently.
2026-09-04 19:09:12 -04:00

54 lines
2.4 KiB
Python

"""Curated Windows event catalog: id -> human title and severity.
Single server-side source of truth shared by the recent-events asset panel and
the stability dashboard card, so a 4625 reads as "Failed logon (warning)" in
both places without either restating the mapping. The set of IDs here MUST stay
in step with CURATED_EVENT_IDS in plugin.py; a test asserts they match.
Severity vocabulary matches the dashboard card renderer: 'critical', 'warning',
'info'. The IDs marked critical are the ones an operator/tech should act on
without being asked - unexpected shutdown, crash, filesystem corruption, SCM
timeout. Logon activity is informational history unless it is a failure.
"""
# id -> (title, severity, channel)
EVENT_CATALOG = {
4624: ('Successful logon', 'info', 'Security'),
4625: ('Failed logon', 'warning', 'Security'),
4634: ('Logoff', 'info', 'Security'),
4672: ('Special privileges assigned', 'info', 'Security'),
4697: ('Service installed', 'warning', 'Security'),
4698: ('Scheduled task created', 'warning', 'Security'),
41: ('Unexpected shutdown', 'critical', 'System'),
4618: ('Monitored security event', 'warning', 'Security'),
55: ('File system corruption', 'critical', 'System'),
7011: ('Service timeout', 'critical', 'System'),
10016: ('DCOM permission error', 'warning', 'System'),
# Windows Error Reporting trio on the Application channel. 1000 is the
# faulting-application record (app name, version, faulting module, exception
# code) and is the one that says what actually broke; 1001 is the WER bucket
# record Windows writes alongside it; 1002 is a hang rather than a crash, so
# it is a warning - the app is unresponsive, not gone.
1000: ('Application error', 'critical', 'Application'),
1001: ('Application crash', 'critical', 'Application'),
1002: ('Application hang', 'warning', 'Application'),
}
# The IDs a tech should see surfaced fleet-wide on the dashboard stability card.
CRITICAL_EVENT_IDS = tuple(
eventid for eventid, (_t, severity, _c) in EVENT_CATALOG.items()
if severity == 'critical'
)
def event_title(eventid):
"""Human title for a curated event id, or a generic fallback."""
entry = EVENT_CATALOG.get(eventid)
return entry[0] if entry else 'Event {}'.format(eventid)
def event_severity(eventid):
"""Severity ('critical'|'warning'|'info') for a curated event id."""
entry = EVENT_CATALOG.get(eventid)
return entry[1] if entry else 'info'