The quiet ESE database that records what ran, for how long, and how much it moved over the network — per application, per user, per hour, for weeks. How SRUDB.dat is built, what each of its five provider tables holds, when it overwrites itself, and how Crow-Eye decodes it.
SRUDB.dat, and keeps roughly 30–60 days of it. For an investigator it answers a question few other artifacts can: not just that a program ran, but how much it moved over the wire, and when.
C:\Windows\System32\sru\SRUDB.dat · ESE / JET Blue engine, B-tree tablesSRU.chk (checkpoint), SRUDB.jfm (flush map) and the SRU*.log transaction logs. Held open (locked) by the DPS service on a running system.Why six provider GUIDs become five Crow-Eye tables: two of the six — a short-term and a long-term energy provider — hold the same kind of battery data, so Crow-Eye merges them into one srum_energy_usage table. That leaves five data providers. The SruDbIdMapTable is not a provider at all — it is the lookup table every provider row depends on: each row stores only numeric AppId and UserId values, and the ID map turns those back into real app paths and user SIDs (see section 6). And SruDbCheckpointTable is neither a provider nor data — it is ESE's own transaction bookkeeping, which is why it has no inspector here and Crow-Eye writes no table for it. It does, however, collect the on-disk checkpoint set (SRU.chk, SRUDB.jfm and the SRU*.log logs) alongside the database, so a dirty SRUDB.dat can be soft-recovered rather than repaired — and records that it did in srum_metadata (see section 10).
SRUM is not a log that can be watched in real time. Each provider is an "extension" registered under HKLM\SYSTEM\CurrentControlSet\Services\SRUM\Extensions\{GUID}, and it keeps the current hour's running counters in the registry. The SRUM service — part of the Diagnostic Policy Service — flushes those counters into SRUDB.dat periodically (about once an hour) and at shutdown, aggregating them into hourly windows: one row per application, per user, per hour, per interface.
That two-stage design has a direct forensic consequence: SRUM survives a reboot, but it is never perfectly live. The most recent activity may still be sitting in the registry buffer, not yet written to the database — so the last hour before an image was taken can be incomplete on disk.
…\Services\SRUM\Extensions\{GUID} holds the current hour.Crow-Eye reads the persistent SRUDB.dat directly — the registry buffer is the operating system's staging area, shown here to explain the timing.
SRUDB.dat, so the last minutes-to-an-hour of activity sit here before they reach the database. Replaying them recovers data not yet saved. SRU.log is the current log; SRU<gen>.log are the earlier generations.Crow-Eye collects the whole set (see section 10). On a dirty database — one imaged mid-write, where committed rows are still only in the logs — it replays the logs against the checkpoint to recover that unsaved activity before reading.
The Extensible Storage Engine (ESE, historically "JET Blue") is Microsoft's embedded, transactional, indexed-sequential database engine. It is not SQL — it is a low-level B-tree store that Windows uses wherever it needs a fast local database: Windows Search, the certificate store, and SRUM all sit on it. Three names appear for essentially the same thing — they are defined side by side below.
An ESE database is a set of tables whose definitions live in an internal catalog, MSysObjects. SRUM's tables are named by provider GUID (e.g. {D10CA2FE-…}), plus the SruDbIdMapTable and a SruDbCheckpointTable. Large values are stored out-of-row as long values. The engine keeps a checkpoint (SRU.chk), a flush map (SRUDB.jfm) and transaction logs (SRU*.log) so an unclean shutdown can be recovered.
Two practical consequences follow. First, the file is locked on a running system — the DPS service holds it open, so it cannot simply be copied. Second, a database that was not cleanly closed is "dirty" and must be recovered (log replay) or repaired before it will open — which is exactly why forensic tooling reaches for the native ESE engine or a dirty-tolerant reader rather than a naive parser.
| Term | What it actually is |
|---|---|
| ESE | Extensible Storage Engine — the database engine itself: Microsoft's embedded, transactional B-tree store. The code that reads and writes the file. |
| JET Blue | The original internal codename for ESE — same engine, older name. Not to be confused with JET Red, the unrelated engine behind Microsoft Access; the shared word "JET" is the only thing they have in common. |
| EDB | The on-disk database file format ESE produces (historically the .edb extension). SRUDB.dat is an ESE/EDB database that simply doesn't use the .edb name — the format is the same. |
| Provider GUID | A GUID is a 128-bit globally-unique identifier written like {D10CA2FE-6FCF-4F6D-848E-B2E99266FA89}. Each SRUM provider is registered under one, and ESE stores that provider's table named by the GUID — which is why the raw table names look like GUIDs, not words. |
Each hourly window is a wide row of counters. Across the providers, SRUM captures CPU cycle time (foreground and background), disk bytes read and written, network bytes sent and received per interface, connection windows, focus / keyboard / mouse seconds, and battery / energy state. Multiply that by every application and every user, once an hour, for weeks, and SRUM becomes one of the densest behavioural records on the machine — often tens of megabytes of SRUDB.dat holding many thousands of rows.
The cost of that density is that the database is a rolling window. Once activity ages past the retention period (roughly 30–60 days), the oldest hourly buckets are purged to make room. SRUM answers "the last month or two," not "the life of the machine" — so it is a time-limited artifact, best collected promptly.
Why a case often holds far less than 60 days. That range is a typical ceiling, not a guarantee — retention is bounded by volume, not by a timer counting down days. A busy machine with many applications and several user accounts writes wide hourly rows quickly, fills the window, and starts overwriting its oldest hours sooner — heavy use can shrink real coverage to a couple of weeks. A lightly used machine can hold longer. And anything that resets the store cuts it shorter still: stopping the SRUM service, deleting or clearing SRUDB.dat, a corruption-triggered rebuild, or reimaging. So the earliest row present is the floor of what this machine kept, not as proof of when activity began.
Faded buckets on the left have aged out of the retention window and been overwritten; the bright buckets on the right are what an examiner recovers.
SRUM splits its data across five providers — each a registered ESE extension identified by a GUID, each its own table inside SRUDB.dat — plus the internal SruDbIdMapTable. Crow-Eye writes each provider to its own SQLite table in srum_data.db. Every row of every provider table carries the same resolved identity and window time, so those shared columns are listed once below; each provider card then adds only its own columns, one per row.
| Table | The question it answers | Signature columns |
|---|---|---|
| srum_application_usage | How much did it cost to run? The resource price of execution — processor and disk — per app, per hour. | *_cycle_time, *_bytes_read/written |
| srum_energy_usage | What did it cost the battery? Power and charge state over time — populated mostly on laptops. | charge_level, state_transition |
| srum_app_timeline | How was it used, and was a human there? Interaction and lifecycle, not just resources (Windows 10+). | keyboard_input_s, in_focus_s, hosted_services |
AppId and UserId — they live on the row itself, not in a separate index. Crow-Eye resolves both through SruDbIdMapTable (see section 6) into the real app path and user SID, then attaches the columns below to every row, in every table.| Column | Type | What it means |
|---|---|---|
| app_name | TEXT | Executable name, resolved from the numeric AppId |
| app_path | TEXT | Full path the app ran from (a device path for svchost.exe — see section 6) |
| user_sid | TEXT | SID of the user the activity is attributed to, resolved from UserId |
| user_name | TEXT | Account name for that SID — a live LookupAccountSid, or offline from the SOFTWARE hive ProfileList |
| timestamp | DATETIME (UTC) | The hourly aggregation window this row covers — the event time; use this for the timeline |
| parsed_at | TEXT | Bookkeeping — when Crow-Eye read the DB; never evidence |
srum_application_usage — the CPU & disk cost of running{D10CA2FE-6FCF-4F6D-848E-B2E99266FA89} · one row per app, per user, per hour · the primary evidence-of-execution table.| Column | Type | What it means |
|---|---|---|
| foreground_cycle_time | INTEGER | CPU cycles used while the app was in the foreground (100-ns units) |
| background_cycle_time | INTEGER | CPU cycles used in the background |
| face_time | INTEGER | Seconds the app was on screen / in focus this window |
| foreground_bytes_read | INTEGER | Disk bytes read while focused |
| foreground_bytes_written | INTEGER | Disk bytes written while focused |
| background_bytes_read | INTEGER | Disk bytes read while backgrounded |
| background_bytes_written | INTEGER | Disk bytes written while backgrounded |
| foreground_num_read_operations | INTEGER | Read operations behind the foreground byte total |
| foreground_num_write_operations | INTEGER | Write operations, foreground |
| background_num_read_operations | INTEGER | Read operations, background |
| background_num_write_operations | INTEGER | Write operations, background |
| foreground_number_of_flushes | INTEGER | Cache flushes, foreground |
| background_number_of_flushes | INTEGER | Cache flushes, background |
| foreground_context_switches | INTEGER | Scheduler context switches, foreground |
| background_context_switches | INTEGER | Scheduler context switches, background |
*_num_read_operations, *_num_write_operations) are the number of individual I/O operations, not bytes: divide bytes by operations for the average I/O size, and a pattern of many tiny operations versus a few large ones is itself a behavioural tell (log spam vs a bulk copy).srum_network_data_usage — bytes on the wire{973F5D5C-1D90-4944-BE8E-24B94231A174} · one row per app, per interface, per hour.| Column | Type | What it means |
|---|---|---|
| bytes_sent | INTEGER | Bytes the app sent this hour — the exfiltration signal |
| bytes_received | INTEGER | Bytes the app received this hour |
| interface_luid | INTEGER | LUID of the network interface that carried the traffic |
| l2_profile_id | INTEGER | Layer-2 (wireless) profile the traffic went over — which SSID/network |
| l2_profile_flags | INTEGER | Flags on that L2 profile (0 when unused) |
| wake_count | INTEGER | Times this traffic woke the machine from a low-power state |
l2_profile_id) identifies which network the interface was actually on — a specific named Wi-Fi SSID or wired network profile — while interface_luid only says which adapter. Together they tie an app's traffic to a particular network: home vs the corporate LAN vs an unknown coffee-shop hotspot. The same profile fields appear in Network Connectivity below.srum_network_connectivity — connection windows{DD6636C4-8929-4683-974E-22C046A43763} · when an interface was connected, and for how long.| Column | Type | What it means |
|---|---|---|
| connected_time | INTEGER | Seconds the interface was connected in this window |
| connect_start_time | FILETIME | Precise moment the connection began (Int64 FILETIME → UTC) |
| interface_luid | INTEGER | LUID of the network interface |
| l2_profile_id | INTEGER | Layer-2 (wireless) profile |
| l2_profile_flags | INTEGER | Flags on that L2 profile |
srum_energy_usage — battery & power state{FEE4E14F-02A9-4550-B5CE-5FA2DA202E37} (short-term), with a long-term variant {DA73FB89-2BEA-4DDC-86B8-6E048C6DA477} Crow-Eye falls back to when the short-term table is absent — both feed this one table.| Column | Type | What it means |
|---|---|---|
| charge_level | INTEGER | Battery charge (percentage, or mWh depending on the row) |
| state_transition | INTEGER | Power / charge state change |
| event_timestamp | FILETIME | Precise moment of the transition (Int64 FILETIME → UTC) |
| cycle_count | INTEGER | Battery charge-cycle count (0 on hardware that does not report it) |
| designed_capacity | INTEGER | The battery's original design capacity (mWh) |
| full_charged_capacity | INTEGER | Current full-charge capacity (mWh) — against designed_capacity this is battery wear |
| battery_count | INTEGER | Number of batteries present |
| configuration_hash | INTEGER | Hash identifying the power configuration this row was recorded under |
| battery_charge_limited | INTEGER | Whether charging was capped (e.g. a vendor "battery care" limit) |
srum_app_timeline — how the app was used, not just that it ran{5C8CF1C7-7257-4F13-B223-970EF5939312} · the richest provider · the only table that carries hosted_services.| Column | Type | What it means |
|---|---|---|
| in_focus_s | INTEGER | Seconds the app held foreground focus |
| keyboard_input_s | INTEGER | Seconds of keyboard input — a non-NULL value is strong evidence a person was present |
| mouse_input_s | INTEGER | Seconds of mouse input — likewise, human-presence evidence |
| user_input_s | INTEGER | Combined user-input seconds |
| psm_foreground_s | INTEGER | Process-state-manager foreground seconds |
| display_required_s | INTEGER | Seconds the app kept the display on |
| duration_ms | INTEGER | Milliseconds the app was running in this window |
| span_ms | INTEGER | Milliseconds the window itself spanned |
| end_time | FILETIME | Precise end of the window (Int64 FILETIME → UTC) |
| timeline_end | INTEGER | Timeline end marker for the window |
| flags | INTEGER | Timeline record flags |
| hosted_services | TEXT | For svchost.exe: the services it hosted, from the !! AppId form (see section 6) |
| audio_in_s / audio_out_s | INTEGER | Seconds of audio captured / rendered |
| comp_rendered_s / comp_dirtied_s / comp_propagated_s | INTEGER | Desktop-compositor render / dirty / propagate seconds |
| cycles / cycles_attr / cycles_wob | INTEGER | CPU-cycle accounting for the window (total / attributed / work-on-behalf) |
| disk_raw / network_bytes_raw / network_tail_raw | INTEGER | Raw disk and network roll-up counters for the window |
| cpu_timeline / disk_timeline / network_timeline | INTEGER | Per-window packed activity timelines — a bitfield of when in the window CPU / disk / network was busy |
| in_focus_timeline / user_input_timeline / keyboard_input_timeline / display_required_timeline | INTEGER | The same packed-timeline form of focus, user input, keyboard and display-on activity |
| comp_rendered_timeline / comp_dirtied_timeline / comp_propagated_timeline / audio_in_timeline / audio_out_timeline | INTEGER | Packed timelines for the compositor and audio counters |
| cycles_breakdown / cycles_attr_breakdown / cycles_wob_breakdown | INTEGER | Per-window breakdowns behind the CPU-cycle totals |
| mbb_timeline / mbb_bytes_raw / mbb_tail_raw | INTEGER | Mobile-broadband (cellular) activity, timeline and byte counters — populated only on machines with a WWAN radio |
*_s second-counts above and these raw *_timeline / *_breakdown packed counters — so nothing SRUM records is dropped.keyboard_input_s and mouse_input_s come only from this Application Timeline provider (srum_app_timeline), on Windows 10 and later — no other SRUM table records them. That is why "was a person at the keyboard?" is always a question for this table, and why a non-NULL value here (and nowhere else) is the human-presence signal.srum_metadata — one row per parse| Column | Type | What it means |
|---|---|---|
| parsed_at | TEXT | When Crow-Eye read the database (UTC) |
| srudb_path | TEXT | Source path of the SRUDB.dat that was parsed |
| total_records_parsed | INTEGER | How many rows the parse produced across all tables |
| parsing_duration_seconds | REAL | How long the parse took |
| windows_version | TEXT | Recorded as Unknown — SRUM does not stamp the source OS build, and offline must not report the analyst's own |
| notes | TEXT | Free-text run notes |
A provider row does not store an app name or a user name. It stores small integers — an AppId, a UserId, an interface id. The SruDbIdMapTable maps each IdIndex to its real value in IdBlob, tagged by IdType:
IdType = 3 → a binary SID — decoded structurally into S-1-5-… so it joins cleanly against the registry's user list. (Decoding it as a string via the wrong API yields a broken PySID: value that never matches — a real trap.)IdType = 0 / 1 / 2 → an application identity, a UTF-16LE string. Two forms appear: a device path (\Device\HarddiskVolume3\…\svchost.exe [DcomLaunch], used by the resource and network tables) and the timeline's !!svchost.exe!2054/02/06:15:19:25!1642e![netsvcs] [Winmgmt] form.app_path, not app_name (app_name is svchost.exe for every one of them). The Application Timeline provider instead splits the service list into its own hosted_services column.| Special Id | Resolves to |
|---|---|
| App 1 / 2 | System · Unknown Application (their IdBlob is NULL) |
| User 1 | S-1-0-0 — Nobody |
| User 2 | S-1-5-18 — SYSTEM |
| User 3 | S-1-5-19 — LOCAL SERVICE |
| User 4 | S-1-5-20 — NETWORK SERVICE |
Every provider table carries a timestamp — the aggregation-window event time, taken from the provider's own TimeStamp column. That is the value a timeline plots: it dates the hourly bucket, not a single instant.
Some tables carry a second, more precise time inside that window: event_timestamp (energy state transitions), connect_start_time (connectivity) and end_time (timeline) are Int64 FILETIME values naming the exact moment the row is really about. Do not confuse either with parsed_at in the metadata table — that is only when Crow-Eye read the database, and is never evidence.
bytes_sent per application, per hour, is often the cleanest evidence that a process moved gigabytes off the machine, and exactly when.keyboard_input_s or mouse_input_s is strong evidence a person was physically at the keyboard in that window. A service accrues CPU for hours and never sees a keystroke, so NULL there means "no such activity," not a decode failure.SELECT app_name, app_path, bytes_sent, bytes_received, timestamp FROM srum_network_data_usage ORDER BY bytes_sent DESC LIMIT 20;
SELECT timestamp, app_name, in_focus_s, keyboard_input_s, mouse_input_s, user_name FROM srum_app_timeline WHERE keyboard_input_s > 0 OR mouse_input_s > 0 ORDER BY timestamp DESC;
SELECT timestamp, hosted_services, in_focus_s, duration_ms FROM srum_app_timeline WHERE app_name = 'svchost.exe' AND hosted_services != '';
duration_ms, byte totals, cycle time) are stored as large raw integers. Any thousands-separator formatting is a display concern — comparing or sorting must use the stored integer, or a formatted "1,024" silently sorts as text.SRUDB.dat destroys history (though the current hour may still be in the registry buffer, and file recovery may recover the deleted database).SRUDB.dat needs ESE recovery or repair before it opens — a naive reader will fail or return partial data.SRU.chk), flush map (SRUDB.jfm) and SRU*.log logs beside it.esent.dll JET API (live) or dissect.esedb (offline). A dirty database is soft-recovered by replaying the collected logs (esentutl /r) before falling back to an esentutl /p repair.The evidence file is opened read-only and never written to; recovery for a dirty database is handled by the ESE engine itself. The live path uses the Windows native JET API through ctypes; the offline path prefers dissect.esedb because it tolerates dirty databases better, falling back to pyesedb / libesedb and, in the worst case, an esentutl /p repair. The result is one queryable SQLite database, srum_data.db.
Every native column, nothing dropped. Crow-Eye reads every column each provider table defines — not a fixed subset. That includes the battery-health columns (designed_capacity vs full_charged_capacity is battery wear), the network wake_count, and the Application Timeline's full set of *_timeline and *_breakdown packed counters. Every metric is stored as a raw integer; formatting is left to the display layer so sorting and range filters stay correct.
Nothing left in the logs. Because the newest activity is committed to SRU*.log before it is flushed into SRUDB.dat (see section 2), Crow-Eye records what state the database was in: a clean database reports db_state = clean with the ESE header's “Clean Shutdown, Log Required 0-0” proof — meaning everything was already flushed and there was nothing in the logs to recover. A dirty database (an image captured mid-write) reports recovered, because the logs were replayed to fold in exactly that unflushed activity. Either way the metadata row states plainly whether any data was still pending.
srum_data table, the name is the file.| Table | From provider | What it holds |
|---|---|---|
| srum_application_usage | App Resource | Per-app, per-hour CPU & disk cost |
| srum_network_data_usage | Network Data | Bytes sent / received per app, per interface |
| srum_network_connectivity | Network Conn. | When and how long an interface was connected |
| srum_energy_usage | Energy (×2) | Battery charge & state — short- and long-term providers merged |
| srum_app_timeline | App Timeline | Focus / input seconds, hosted services (Win 10+) |
| srum_metadata | — | One row per parse: source path, counts, duration, and the recovery record — whether the checkpoint/logs were collected and whether the database was read clean, recovered or repaired (db_state) |
Provider rows store only numeric ids, so Crow-Eye loads SruDbIdMapTable before any provider table and builds the lookup once. Each map entry has an IdType that says how to decode its blob:
decode_binary_sid. It deliberately does not route this through win32security: that path produced a PySID object whose text form corrupted a large fraction of the rows, so the raw-byte decoder is the correct one.!!svchost.exe!…![netsvcs] form only appears in the timeline provider and is split out into hosted_services.S-1-0-0, 2 = S-1-5-18 (Local System), 3 = S-1-5-19, 4 = S-1-5-20.A SID is only half an identity. Live, Crow-Eye turns it into an account name with LookupAccountSid; offline, there is no live registry to ask, so it reads the account names from the SOFTWARE hive's ProfileList instead — the answer belongs to the image, never the analyst's own machine.
SRUM mixes two clocks in one row (see section 7): the hourly window TimeStamp is an OLE-automation date, while event_timestamp, connect_start_time and end_time are Int64 FILETIME (100-ns ticks since 1601). Crow-Eye normalises both to timezone-aware UTC and applies a 1980–2200 sanity bound, which is what catches the classic "year 6916" wrong-offset misread before it ever reaches a table.
Every metric — cycles, bytes, seconds — is stored as a raw integer, never a pre-formatted string. That is not a stylistic choice: SQLite keeps a value it cannot convert as TEXT, and TEXT sorts above every integer, so a single "1,024" in a numeric column silently breaks every ORDER BY and > comparison over it. Formatting is the display layer's job.
The finished srum_data.db is a first-class citizen of the case: Timeline plots its window timestamps alongside every other artifact, Database Search queries it directly, Eye AI reads it to answer questions like “what sent data the night of the incident?”, and the activity dashboard charts it interactively — the same evidence, seen four ways.
Parsing writes five tables; the Charts button on any SRUM table tab opens the same
srum_data.db as an interactive dashboard. One rule holds everywhere: provider = colour.
Each of the five provider tables owns a colour, and that colour means the same thing in every chart on the page.
| Provider table | On the dashboard | What its heat-map counts |
|---|---|---|
| srum_application_usage | App Resource | Records of CPU & disk cost, per app / hour |
| srum_network_data_usage | Network Data | Records of bytes sent / received |
| srum_network_connectivity | Net Conn. | Records of interface connect time |
| srum_energy_usage | Energy | Records of battery charge & state |
| srum_app_timeline | App Timeline | Records of focus / input seconds |
A heat-map counts records, and all five providers write records — so there are five heat-maps. The timeline below is different: it plots one bubble per app per hour and sizes each bubble by a magnitude. Only four metric families give an app a per-hour magnitude worth sizing — network bytes, CPU cycles, disk bytes and presence seconds — so there are four timeline modes.
Network Connectivity and Energy have no per-app activity magnitude to size a bubble: connectivity is about when an interface was up, and energy is a battery charge level, not an amount an app “did”. They still earn a heat-map (record volume still shows when they were active), and Energy's charge shows up as the battery % line inside an app's pop-up — just not as a timeline mode.
| Mode | Comes from | Bubble size / marker |
|---|---|---|
| Network (sent / received) | srum_network_data_usage | Bytes moved that hour — ▲ filled = sent, ▿ outline = received |
| CPU cycles | srum_application_usage | Foreground + background cycle time |
| Disk bytes | srum_application_usage | Foreground + background bytes read + written |
| User presence | srum_app_timeline | Seconds the app was in focus |
Left — the five heat-maps, then the day section for whichever day is selected: the bubble timeline above, and below it Activity by hour (stacked bars, one colour per provider), Top applications (each app's bar split by provider), a Records by provider breakdown for that day, and user-presence tiles (keyboard / mouse / in-focus).
Right — the overview, scoped to the whole selected date range rather than one day: totals tiles (active days, distinct apps, bytes sent, bytes received, CPU cycles, user presence); a Network activity card (bytes sent vs received per day, plus the top apps by network); Most active applications and Activity by user, both bars split by provider colour; and a Records by provider total.
SRUDB.dat under System32\sru.{5C8CF1C7-…} provider adds focus, keyboard and mouse seconds — the "was a human present" signal.