Release date: 2026-09-05
Baseline: v0.12.7, the previous release. Every figure below was measured against that tree.
This release is about two kinds of evidence: the kind Crow-Eye could not reach, and the kind it had already parsed and never showed you.
The registry parser now reads the hive file, not only the running registry — past the ACLs that deny even an elevated administrator, into the free space where deleted keys and values still sit, and into the two places a tree walk cannot go at all. The registry goes from 29 tables to 81, and nineteen keys that held real data were being read by nothing. The Timeline plots 133 time columns where it plotted 53, because the map deciding what gets plotted lived in two files and had drifted apart — AmCache had never appeared on a timeline at all, and 41,109 of 43,802 Windows Event Log records reached it through nothing. Database Search stops searching the wrong database. And three readings are corrected outright: Shellbag timestamps that were the evidence machine's local clock relabelled as UTC, a "match" that could span a single feather and corroborate nothing, and a confidence score that was a raw count.
| v0.12.7 | 0.13.0 | |
|---|---|---|
| Registry tables | 29 | 81 |
| AmCache tables | 17 | 29 |
| Timeline artifact types / time columns / databases | 13 / 53 / 8 | 17 / 133 / 11 |
| UBA behaviours | 40 | 53 |
| Eye forensic tools | 30 | 31 |
| Default Wings | 10 | 11 |
| Test files in this repository | 59 | 69 |
🗝️ New: the registry read as a file, past the ACL
A live parse reads the running registry through winreg. That is right for most of it — the merged view, volatile keys, CurrentControlSet and the redirected 32-bit view exist nowhere else — but some keys deny winreg outright, even to an elevated administrator, and no error distinguishes "denied" from "empty".
- Measured on a live machine, elevated: walking
HKLM\SYSTEM\CurrentControlSet\Enum\USBthroughwinregreaches 110 keys; the same hive read as a file yields 868. Every devicePropertiessubkey is denied — and those hold the FILETIMEs that say when a USB device was last connected. A live parse reported no USB connection times at all, not because the evidence was absent but because the door was shut. - Crow-Eye already had two ways through and neither was reachable from the registry parser:
crow_claw's file accessor (standard copy → Volume Shadow Copy → raw disk) and theSeBackupPrivilege+NtSaveKeyExexport already used forHKLM\SAMandHKLM\SECURITY. Both are now available to the registry parser, which falls back through them in order and records which one it used. - The evidence is never written to. The hive file is copied out and the copy is what gets read, and the copy's SHA-256 is recorded with the parse — so "the original was never written to" is a fact the case carries rather than something only a test knows.
- Every hive reader now replays the transaction logs. A hive Windows has not finished writing keeps its most recent changes in
.LOG1/.LOG2, and replaying them onto a working copy is the only way to read the state the machine was actually in. Only the offline registry parser did it: AmCache, ShimCache, SRUM, the SECURITY hive reader and the user-identity reader each opened the raw file, so five parsers were reading a stale hive. The replay is now done once per hive and shared by every reader. - A parse can decline to create a Volume Shadow Copy. Acquisition wants one made if none exists; a parse should not have to write to the machine holding the evidence in order to read it. Which of those the analyst agreed to is now their call rather than the accessor's.
🧩 New: deleted keys and values, class names, and key security
Deleting a registry key does not erase it. Windows flips the cell's size field from negative to positive, marks the space free, and moves on — the signature, the name, the timestamp and the pointers are all still there until something allocates over them. Every tool that walks the registry tree is blind to that by construction.
Crow-Eye now walks the hive's allocator as well as its tree, and records what only that pass can see:
- Deleted keys and values. On the hives this was written against, 1,451 keys and 6,347 values sat in free space in
SOFTWAREalone. They land inregistry_carved_keysandregistry_carved_values, with the time still attached where the record carries one. - Class names. An
nkrecord can carry a second string beside its name, in its own cell — and it is where the four keys underControl\Lsakeep the machine's boot key. Most registry viewers do not render the field at all. It is now inregistry_class_names. - Key security. Owner, group and DACL, from the shared security descriptors a hive stores once and points many keys at (
registry_security_descriptors). The SACL is deliberately not requested: it needs a privilege that, if refused, would fail the whole call rather than that one part. - Every row that came out of free space says so, in a
record_statecolumn present in each table that can show one — so an analyst reading any table knows this row was not in the live registry, rather than having to remember which table means what. - A hive is compacted from time to time, which rewrites its free space, so the parser also records how much recoverable history the file actually still holds rather than implying the absence of carved records means nothing was ever deleted.
🔑 New: nineteen keys that nothing was reading
Publishing an article listing every registry key Crow-Eye opens made the opposite question answerable — what does it not open — and nineteen keys came back that hold real data on a reference system and were read by nothing.
One of them changes a finding rather than adding one:
- Explorer's
StartupApprovedrecords whether each autostart entry is actually allowed to launch. Six of ten HKCURunentries on the reference system are disabled. Without that key,AutoStartProgramsreported all ten as live persistence — four of them wrongly. - The rest arrive as new tables:
app_paths,app_permissions,hid_devices,network_cards,NetworkProfiles,safe_boot_services,shared_dlls,startup_approved,system_configuration,zone_map,registry_key_times,registry_value_changes, and the three carving tables above — 52 new registry tables since v0.12.7, 29 → 81, with nothing removed — sixteen of them from the nineteen-key pass above, the rest from the accuracy work that never reached a release. - The collectors take the reader as an argument, so the live parser passes its
winregreaders and the offline parser its hive readers, and the two produce the same rows from the same code. A key absent on a given Windows build produces a shorter list, never a failed parse.
🕒 Fixed: Shellbag timestamps were local time relabelled as UTC
A DOS date/time — the format Shellbags and shell items store — carries no timezone and never did. It is the evidence machine's wall clock. Stamping it as UTC does not convert anything; it relabels a local reading, and every one of those timestamps in the case is then wrong by the evidence machine's offset, with nothing anywhere to say so.
- The parse now takes the evidence machine's UTC bias and converts with it, so the value becomes a real UTC moment.
- Where the bias is not known, the local reading is returned unchanged and recorded as local, rather than being given an invented zone. Not knowing is now something the case can state.
🧬 Improved: AmCache records what the hive actually holds
- 17 → 29 tables. Every column is a registry value name that was observed in a real
Amcache.hve, not inferred from documentation, and the comment above each table records how many entries that subkey held on the reference system. key_last_writeis captured — the moment the Compatibility Appraiser wrote the entry. It was recorded nowhere before. It is a bound, not an event time, and how much ordering it supports differs per table because the Appraiser writes in batches: all 373Mareentries share one timestamp, 90% of 445 driver binaries share one, whileInventoryApplicationFilehas 1,086 distinct times across 5,212 rows. The notes on the column say so.DeviceCensusand nine subkeys that were empty on every available system now use a name/value shape —WHERE name = 'AADDeviceId'— rather than a fixed column list.DeviceCensusalone carries 237 distinct value names across 16 entries and Windows adds more every release, so a hand-written column list is stale on arrival; it used to be a single column of JSON. Anything Microsoft adds next lands inUnknownSubkeysinstead of being dropped.- AmCache's dates are locale-formatted text in at least three shapes. They are now normalised on the way in — which is also why AmCache had never appeared on a timeline (see below).
🧱 Improved: the ShimCache trailing blob is decoded
The data blob at the end of a ShimCache record is not an opaque payload: it is an array of 12-byte slots, each (tag, type, value) as three little-endian DWORDs. Every blob length on 165 live records was a multiple of 12.
Only the tags the bytes actually established are named, each checked against something read independently of the cache:
- The executable's machine type agreed with the PE header of the file on disk on 296 of 300 files that still exist — and the four disagreements are files replaced since the cache recorded them, which is the artifact being right rather than the decode being wrong.
- An operating-system-binary flag, 164 of 165 against "path under
C:\Windows", where the single exception is a third-party driver package staged inDriverStore— so the flag is right and the path was the imperfect proxy. - The remaining tags sit at 94–97% correlation, close enough to the base rates to be coincidence, and are therefore recorded and not named. A field named on a guess is worse than a field left raw.
📅 Fixed: the Timeline was missing most of the case
The map that decides what the Timeline plots lived in two files and the two had drifted: one filed Shellbags under Shellbags and the other under ShellBag; one had DAM and USB storage and the other did not; one carried UserAssist.focus_time, which is a duration and not a time at all. Six artifact types had timestamps defined and no database entry, and the only complaint anywhere was a debug line.
There is now one map (timeline/data/artifact_map.py), and both halves of it are required for an artifact to appear.
- 53 → 133 mapped time columns, 13 → 17 artifact types, 8 → 11 databases.
- AmCache had never plotted at all. Its dates are locale text (
MM/DD/YYYY …), and SQLite'sdatetime()returns NULL for those rather than raising — so the lane drew nothing, silently, for as long as it has existed. - 41,109 of 43,802 Windows Event Log records reached the timeline through nothing. Only a handful of event IDs had a lane; everything else was parsed, stored, and never plotted. 65 significant event IDs are now mapped, and the rest are reachable rather than invisible.
- Deleted-file activity,
$FILE_NAMEtimes and filename changes from the MFT/USN correlation now plot, as do user accounts, network profiles and app permissions. - A registry key's write time is an upper bound, not an event time — writing any value under a key updates the whole key. Those columns are now labelled as bounded (
≤ T) wherever they appear, in the lane, the tooltip and the detail view, and can be toggled off entirely so a timeline can be read as exact times only. parsed_at— when Crow-Eye ran — is bookkeeping and can no longer be plotted as evidence anywhere.- Artifacts the parsers collect had no colour of their own on the timeline — DAM, Scheduled Tasks and registry changes all drew in the fallback. The renderer's palette is kept in step with the map the shipping timeline actually draws from.
🔎 Fixed: Database Search was searching the wrong databases
- Five entries in the search tree named after registry and LNK artifacts each offered the three Windows Event Log tables. Ticking "ShellBags" searched the event log. The resolver tried a name map before the table signatures, and
Log_Claw.db— which exists in every case — was listed there for almost everything, so it always won and the matching that would have found Shellbags inregistry_data.dbnever ran. - Imported evidence could not be searched. Imported and custom databases were discovered, enhanced with their table lists, and then dropped before they reached the tree, because the tree was built by looking each database up in five hand-written category lists. They could not be ticked, so they were never searched, and nothing said so. Databases are now grouped by the category each one already carries — which
DatabaseManagerhad been setting all along. - One read per physical file. Six logical names resolve to the one
Log_Claw.db, so a search read its 43,802 rows six times and reported every hit six times — and because the per-database result cap is divided by the number of databases, the duplicates were also eating five other databases' share of the results. - The timestamp detector now reads the Timeline's map instead of guessing from column names alone, and no longer treats
account_expiresandaccount_createdas counters because "account" contains "count". A time-filtered search could previously key on a duration, a label or an hour-of-day and match nothing.
👁️ Eye: chronology, and knowing what the model can do
- New tool —
query_timeline. One chronological sweep across every database in the case, driven by the same map the Timeline plots from. It needs no correlation run and no embedding server, which matters because the alternative the model reaches for is onequery_databaseper database — and whichever database it does not think of is simply missing from the answer. - Sealed chronology answers record what was searched. The window and the artifacts reached are part of the provenance, because "nothing happened then" is a claim about coverage; without it there is no way to tell an absent database from an empty one. The exact-versus-bounded split is sealed too — a key's upper bound cannot support "X happened at T".
🧠 Improved: User Behavior Analytics — 40 → 53 behaviours
- Thirteen new rule-driven behaviours, chiefly around registry-recorded activity.
- A bag's view kind is now stated where it is known. Windows files an Explorer window's view settings separately from a common file dialog's, and the parser records which — so the blanket caveat is replaced by what actually happened, and kept only where the case genuinely cannot tell.
- A key write time is phrased as a bound in the narrative, for the same reason the Timeline now labels it as one.
- The coverage panel names the new registry artifacts instead of showing the bare table name — the database's vocabulary is not the analyst's.
🪶 Fixed: correlation results that were silently empty or silently incomplete
- 1,114 time-window queries on the reference case returned nothing because of a timezone-awareness mismatch. Timestamps inside the time-based engine are naive UTC, but converting to and from Unix seconds through the standard library's defaults reads a naive value as local — and asking for an aware one produces a value that cannot be compared against the window at all. Each raise was swallowed and cost a whole feather's results for that window. Both conversions now go through helpers that keep the convention, and the reason is written where the next person will look.
- Semantic rules dropped every declared field. Rules were rebuilt by hand-listing constructor arguments, so
technique_id,tactic,rule_type, the multi-indicator flags and the advanced-rule blocks were parsed, discarded, and never applied — with no error anywhere. Rules are now built through the same path that declares them. - Switching a rule off did nothing.
disabledwas read withhasattr()on a class that had no such field, so the check was always false. - A result row was written before the numbers it describes existed, so a run could be recorded with statistics that had not been computed, and two writers of the same table disagreed about its columns.
- The user column was being cut off four feathers. The feather generator dropped the last column by position to strip parser bookkeeping. The schema moved, and by the time anyone looked it was cutting
user_nameoff Shellbags, MUICache, OpenSaveMRU and LastSaveMRU — in four user-activity feathers, the column the wing correlates on. Bookkeeping columns are now excluded by name. - The execution row was wrong about its own run.
total_matches,total_wingsandtotal_records_scannedread 0 while the run's result rows held 127,226 matches for one wing. The execution row is the first thing an analyst opens; its totals are now derived from the result rows it summarises rather than from whatever the caller passed. - Semantic evidence names what actually matched — the value the analyst can see, and the source it came from — instead of echoing the rule's own pattern back.
- The ATT&CK catalogue names the techniques the registry Wings cite. An unknown technique id never blocked a wing, but a coverage roll-up that cannot name half of what it covers is not much of a roll-up.
- The analyst's cascade-tree setting was never read — the identity view's configuration import raised on every launch and the exception was swallowed.
- Code that could never run has been removed, not left as a spare. Python keeps the last definition of a repeated name, so the earlier one is dead: one of those was a
query_identities_by_anchor_timewhose dead copy ignored the time window entirely while the live one honours it, and another was a statistics block sitting after areturn. - A new default Wing: Security Control Tampering (10 → 11 rule packs shipped).
🪺 Fixed: nine of the eleven shipped Wings could not run at all
A wing that cannot run looks exactly like a wing that found nothing: the run still records it, with no matches and no time, and the Summary tab shows a row of zeros. On an eleven-wing run only two wings were producing findings, and nothing on screen said why.
anchor_prioritywas validated against the wrong vocabulary. A wing's anchor priority is the order in which it prefers to anchor a correlation, and anchor selection matches those entries against the artifact types of the wing's own feathers. Validation instead compared them against a list of 17 coarse categories (Registry,Logs,Persistence), so every wing naming a concrete artifact type —SecurityLogs,ShellBags,AutoStartPrograms,SystemConfiguration,SystemLogs— was rejected before it started. Nine of eleven. Anchor priority is now checked against the wing's own feathers, and an entry that matches nothing is a warning, not a refusal: a preference that cannot apply is inert, and the wing still runs.- Eleven artifact types were unknown to the artifact detector. The sixteen registry tables added this release reached it under their snake_case table names (
startup_approved,zone_map) while the feather generator and the Wings use the CamelCase artifact type (StartupApproved,ZoneMap). Feathers for them existed in the case the whole time; the Feather Builder simply never offered them. - Every identity wing wrote its result row twice. A wing streams its matches into a row created when it starts, and the report step is supposed to update that row — but the identity engine never recorded which row it had streamed into, so the report inserted a second, empty copy carrying the same counts. Execution totals are the sum of those rows, so every identity wing reported exactly double the matches it found (237,282 for a wing with 118,641) and appeared twice in the Wing Breakdown. Existing cases are not modified; a run made by an earlier version is now read correctly.
⚖️ Changed: what a "match" is, and what a confidence score means
Two of this release's fixes change how a case correlated by an earlier version reads. Both were producing findings that looked fine and were not.
- A "match" could correlate nothing.
minimum_matchesmeant "the feathers that must corroborate" in the wing schema and in the validator, and "total feathers" in both engines. A wing saying1therefore required one feather — no corroboration at all. On the reference case that emitted 125,029 single-feather rows out of 127,226 "matches": 98.3% of the output correlated nothing, one artifact row echoed back, and the ~2,200 real cross-source correlations were buried under it in a 2 GB database. There is now one definition of the floor — one feather to observe, plusminimum_matchesto corroborate, never below two — and every engine asks for it instead of comparing the field itself. - Sub-threshold groups are no longer emitted by default.
low_confidence_review_modedefaulted to on, so the engine would never silently discard evidence. The concern is right and the default was not: drowning a finding hides it as effectively as dropping it, and costs the analyst more to dig out. Nothing is silent now that it is off — every dropped group is counted, a sample is kept with its identities and feathers, both are reported in the run's evidence accounting, and the log names the flag that shows them as Low matches. - The confidence score was a raw count. With weighted scoring off, the score was simply how many feathers the match spanned — a count written into the field every consumer reads as a normalised 0–1 value and judges against the 0.7 / 0.4 / 0.2 thresholds. A match spanning five feathers scored 5.0 and rendered far above "Confirmed"; the reference case carried a 2.55 labelled "Confirmed Execution". The count is still reported, under a name that says what it is.
⚡ Improved: a multi-wing run stops repeating itself
- Results are built once, when the run finishes. Each completed wing used to rebuild the unified identity view from the database — every match of every wing so far, re-read and re-parsed on the interface thread — while the next wing was already running. Eleven wings meant eleven rebuilds over a growing result set. Wings now report as they finish and the results are assembled once, at the end.
- A feather is read once per run, not once per wing. Each wing executes in its own pipeline run, so each one re-opened every feather it names, read every row and re-derived every identity — and the shipped Wings share feathers heavily (
mft_usnappears in eight of eleven). A run-scoped cache, keyed on the feather file's identity rather than its path, does that work once and hands each wing its own copy. A feather rewritten mid-run is never served from the previous contents, and the cache reports what it reused in the execution log. - Results start loading the moment the run ends. The "Execution Complete" notice used to block on its own event loop, and the signal that builds the Results Viewer fired only after it was dismissed — so the results were waiting on a click, with nothing on screen saying so. The notice is now shown without blocking and the results load underneath it, with the same "Loading identity data…" progress the load-from-database path uses.
- One wing is ticked when a pipeline loads — the widest one. Every wing used to arrive checked, so pressing Execute on a default pipeline ran all eleven shipped Wings. The default is now the wing drawing on the most feathers (Execution Proof, with 14); Select All is one click away.
- The Identity engine is the default. The pipeline default was the Time-Window engine while the Execution tab's dropdown said Identity-Based, so which one ran depended on whether a pipeline had been loaded. A new pipeline — including the one a new case creates for itself — now says
identity_based, and the dropdown resolves its default by name rather than by position in a list whose order is not a contract. Pipelines you already have are left as they are: a saved pipeline naming an engine keeps it, and one saved before the field existed still reads as time-window, because changing that would alter how an existing case runs. - The Correlation Engine opens on Pipeline Manager. It used to reopen wherever the last session stopped — the Results tab, for anyone who had just run a correlation — because start-up loaded the previous pipeline (which jumps to Execution) and then applied the tab stored in
session.json. Startup now lands on Pipeline Manager; the explicit Load Session action still restores the tab it recorded.
🧾 Improved: the Wing Breakdown says what happened to each wing
- A Status column — Completed, Failed or Skipped — with the wing's own error or skip reason in the tooltip. A wing that could not run no longer reads as a wing that found nothing.
- Each wing's own duration, rather than the whole pipeline run's wall clock, so the live run and the same run read back from the database agree.
- Wings are numbered once across the run; an eleven-wing run used to come back with every wing numbered 0 or 1, because the numbering restarted inside each execution.
- The header no longer clips its own labels, and a wing's full name is shown rather than elided to make room for four short columns. A stylesheet on the surrounding frame was being inherited by the table inside it — a
QTableWidgetis aQFrame— which pushed the header into its own border.
🔍 New: Eye-Describe — every section addressable, every byte map drawn from a real artifact
The anatomy pages at crow-eye.com/Eye-Describe take each artifact apart byte by byte. They are part of the product but not part of the download — they are live on the site, and they are what the Anatomy button in the application opens.
- Every section is now addressable. Ninety-five headings could not be reached by a fragment at all.
registry_anatomywas the worst of it: the page documenting UserAssist, BAM and DAM, RecentDocs, TypedPaths, WordWheelQuery, USBSTOR, MountedDevices, TaskCache and MUICache had eleven sections and five sub-headings, and not one id — so a search for "UserAssist forensics" had nowhere on the site to land, and a link into the middle of the page was impossible. Every section on all fifteen pages is reachable now, 130 of 130, with slugs named for the artifact (#userassist,#bam-dam,#usb-devices) rather than for the question the heading happened to be phrased as. - The hub is an index rather than a card wall — a 23-row artifact table saying what each artifact records and where it is taken apart, with the registry artifacts pointing straight at the sections that document them, and the two long-form guides (the boot process, and the registry's own internals) that were not on the front door at all.
- A page that names a structure another page dissects now says so. Cross-page references went 14 → 22, and three were broken before that: a block link dropped mid-sentence stranded the tail of the sentence underneath it — one Prefetch cell read "…USN Journal / Explained in full: ShimCache / — and look for EnablePrefetcher tampering" — one link pointed at a page that does not exist, and the link check matched
href="…"only, so four single-quoted links had never been checked at all. A checker that silently skips a quarter of its subject reports the rest as clean. - The byte maps are drawn from real artifacts, not hand-written hex. Four pages carry a live map — AmCache, Registry, ShimCache and Shellbags — and three of them are regenerated from the machine's own hive by their own generator, so what the page shows cannot drift from what the artifact holds. A test runs each generator and fails if the committed page moves.
📖 New: an Anatomy button above every documented table
An examiner reading a table of Shellbag rows had no route from the row in front of them to what that row actually is.
- 51 tables across 11 artifact pages now carry a button that opens the Eye-Describe explanation at the section describing their own records — not the top of a page and a scroll.
- Only tables a page genuinely documents get one. A button that landed on a directory instead of an explanation would teach an examiner to stop trusting the button.
- The anchors are a contract with the site, and a test checks every one of them against the pages themselves, so a section renamed there cannot quietly break a button here. They are the ids the addressability pass above created — which is why the two halves ship in the same release.
🔧 Fixes
- Every forensic-image parsing strategy failed to load. The five strategies (E01 and the rest) were imported under a bare module name with no package, so inside each one the import ladder fell through to an absolute branch that could not resolve — and every one of them failed. They are imported as a package now, and a test loads all five.
- Parsing in a separate process reports to the loading screen.
print()from a spawned process reaches nobody, because the dialog captures stdout in the parent — those log lines are now forwarded through the same progress channel the rest of the run uses. - Registry tables are filled by column name rather than by position. A tab's columns were populated in the order the database happened to return them, so a schema change reordered what an examiner was reading with nothing failing.
🧪 Under the hood
- 59 → 69 test files in this repository. The correlation engine's own suite (another ninety-odd checks) is not published — its fixtures carry case state, so
correlation_engine/tests/stays out of the repo. The ones here are mostly the uncomfortable kind: that both registry parsers agree on content rather than on row count, that every decoded value matches something read independently of the artifact, that no GUI column is written by nothing, that a semantic rule can actually fire, that a parser's console output survives acp1252terminal, and that the Timeline's map and the databases agree. docs/changing-a-parser.md— the canonical procedure for changing or adding a parser, including the places a new table has to be registered or its rows are written and never displayed.- The README architecture diagram now shows dirty-hive replay: transaction logs applied to a working copy, so a hive that Windows had not finished writing is read in the state it was actually in.