Skip to content

Commit a81ff80

Browse files
h4x0rclaude
andcommitted
docs: add Phase 18 ESE Integrity Hardening section to PLAN.md
Documents Phases 18-A/18-B/18-C (Severity enum, EseIntegrity unified analyser, input robustness hardening) — the spec that was implemented in the preceding six commits. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ecaff3a commit a81ff80

1 file changed

Lines changed: 222 additions & 0 deletions

File tree

PLAN.md

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1028,3 +1028,225 @@ New capabilities:
10281028
| srum-parser | `src/filetime.rs` | NEW |
10291029
| srum-parser | `src/(energy\|connectivity\|timeline\|wireless\|push\|generic).rs` | NEW |
10301030
| sr-cli | `src/main.rs` | EXTEND |
1031+
1032+
---
1033+
1034+
## 18. ESE Integrity Hardening — Severity Stratification + Expanded Anomaly Coverage
1035+
1036+
Modelled on `vhdx-forensic`'s `VhdxIntegrityAnomaly::severity()` and `WinevtIntegrity` unified analyser pattern. **Scope: detection and robustness only — no repair.**
1037+
1038+
ESE databases (including SRUM's `SRUDB.dat`) are frequent targets of anti-forensic manipulation. The existing `ese-integrity` crate has three anomaly variants but lacks severity stratification, a unified entry-point analyser, and coverage of the two highest-value structural checks: per-page checksum validation and B-tree consistency.
1039+
1040+
---
1041+
1042+
### Phase 18-A — `Severity` enum + `severity()` on `EseStructuralAnomaly`
1043+
1044+
**Crate:** `ese-integrity`
1045+
**File modified:** `crates/ese-integrity/src/lib.rs`
1046+
1047+
Add `Severity` enum (identical definition to `vhdx-forensic`'s `Severity`; no shared dep — each crate owns its own copy per the no-cross-crate-import rule):
1048+
1049+
```rust
1050+
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1051+
pub enum Severity {
1052+
/// Consistent with legitimate operation; worth noting.
1053+
Info,
1054+
/// Suspicious; plausible legitimate explanation but warrants investigation.
1055+
Warning,
1056+
/// Strong indicator of tampering or structural corruption.
1057+
Error,
1058+
/// Database cannot be reliably decoded; forensic conclusions unsupported.
1059+
Critical,
1060+
}
1061+
```
1062+
1063+
Implement `EseStructuralAnomaly::severity()`:
1064+
1065+
| Anomaly | Severity | Rationale |
1066+
|---|---|---|
1067+
| `DirtyDatabase` | Info | Expected from live captures; normal unclean shutdown; not an attack signal on its own |
1068+
| `TimestampSkew` | Error | Page newer than file header: bypasses normal write path — strong indicator of manual manipulation |
1069+
| `SlackRegionData` | Warning | Residual deleted-record fragments; forensically interesting but not definitively malicious |
1070+
1071+
Add severity filtering helper:
1072+
1073+
```rust
1074+
impl EseStructuralAnomaly {
1075+
pub fn severity(&self) -> Severity { ... }
1076+
pub fn at_least(&self, min: Severity) -> bool {
1077+
self.severity() >= min
1078+
}
1079+
}
1080+
```
1081+
1082+
**New anomaly variants added in Phase 18-A:**
1083+
1084+
```rust
1085+
/// The XOR or ECC checksum stored in the page header does not match
1086+
/// the recomputed checksum over page bytes. Bytes changed after write.
1087+
PageChecksumMismatch {
1088+
page_number: u32,
1089+
expected: u32,
1090+
actual: u32,
1091+
},
1092+
1093+
/// A B-tree node's sibling-page pointer chain is broken: the declared
1094+
/// next/previous page does not reciprocate the link.
1095+
BTreeLinkBroken {
1096+
page_number: u32,
1097+
/// Page number that was supposed to link back.
1098+
broken_sibling: u32,
1099+
},
1100+
1101+
/// A page is reachable via the B-tree but its `page_flags` are
1102+
/// inconsistent with its position (e.g. leaf flag set on an internal node).
1103+
PageFlagInconsistency {
1104+
page_number: u32,
1105+
flags: u16,
1106+
context: &'static str,
1107+
},
1108+
1109+
/// A SRUM table identified by GUID in the catalog is referenced by
1110+
/// a record but no corresponding B-tree root page can be found.
1111+
OrphanedSrumTable {
1112+
table_guid: String,
1113+
},
1114+
1115+
/// A required SRUM table (known GUID from forensicnomicon) is absent
1116+
/// from the catalog — the table was deleted or never populated.
1117+
MissingSrumTable {
1118+
table_guid: &'static str,
1119+
table_name: &'static str,
1120+
},
1121+
1122+
/// The file ends before the declared page count implies. The database
1123+
/// was truncated — either during acquisition or deliberately.
1124+
TruncatedDatabase {
1125+
declared_pages: u32,
1126+
actual_pages: u32,
1127+
},
1128+
```
1129+
1130+
Severity for new variants:
1131+
1132+
| New Anomaly | Severity | Rationale |
1133+
|---|---|---|
1134+
| `PageChecksumMismatch` | Error | ESE page checksums are always computed at write; mismatch means bytes changed post-write |
1135+
| `BTreeLinkBroken` | Error | Broken sibling links indicate structural surgery — cannot occur from normal I/O |
1136+
| `PageFlagInconsistency` | Warning | May indicate partial write or deliberate tree restructuring |
1137+
| `OrphanedSrumTable` | Warning | Catalog references a tree that no longer exists — normal table drop or deliberate erasure |
1138+
| `MissingSrumTable` | Warning | Expected SRUM table absent — may indicate selective log clearing |
1139+
| `TruncatedDatabase` | Critical | Cannot decode; declared extent exceeds file; acquisition error or deliberate truncation |
1140+
1141+
**TDD plan — Phase 18-A:**
1142+
1143+
RED commit: Add stub `Severity` enum; add stub `severity()` returning `Severity::Info` for all variants; add new variant stubs with empty detection bodies; write tests asserting exact severity for every variant (old and new).
1144+
1145+
GREEN commit: Implement `severity()` per table above; implement detection logic for new variants.
1146+
1147+
---
1148+
1149+
### Phase 18-B — `EseIntegrity` unified analyser
1150+
1151+
**Crate:** `ese-integrity`
1152+
**File modified:** `crates/ese-integrity/src/lib.rs`
1153+
1154+
Add a single entry-point struct analogous to `VhdxIntegrity`:
1155+
1156+
```rust
1157+
/// Read-only forensic analyser for a raw ESE database byte buffer.
1158+
///
1159+
/// Operates directly on raw bytes so it can detect anomalies that would
1160+
/// prevent normal parsing (bad checksums, missing pages, truncation).
1161+
pub struct EseIntegrity<'a> {
1162+
data: &'a [u8],
1163+
}
1164+
1165+
impl<'a> EseIntegrity<'a> {
1166+
pub fn new(data: &'a [u8]) -> Self { ... }
1167+
1168+
/// Run all checks and return every detected anomaly.
1169+
/// Returns an empty Vec for a structurally sound database.
1170+
pub fn analyse(&self) -> Vec<EseStructuralAnomaly> { ... }
1171+
1172+
// Layer-specific checks (also public for targeted use):
1173+
pub fn check_header(&self) -> Vec<EseStructuralAnomaly> { ... }
1174+
pub fn check_pages(&self) -> Vec<EseStructuralAnomaly> { ... } // checksums, timestamps, flags
1175+
pub fn check_btree(&self) -> Vec<EseStructuralAnomaly> { ... } // sibling links
1176+
pub fn check_catalog(&self) -> Vec<EseStructuralAnomaly> { ... } // orphaned/missing tables
1177+
pub fn check_layout(&self) -> Vec<EseStructuralAnomaly> { ... } // truncation
1178+
}
1179+
```
1180+
1181+
`analyse()` short-circuits after `Critical` findings (analogous to `VhdxIntegrity::analyse()` halting after `ContainerTruncated`): if `check_layout()` returns `TruncatedDatabase`, the page and B-tree checks are skipped (they would produce false positives on missing pages).
1182+
1183+
`check_layout()` runs first; if the buffer is smaller than `ESE_HEADER_SIZE` (8192 bytes for the dual-shadow header), `analyse()` returns a single `TruncatedDatabase` immediately.
1184+
1185+
**Severity-gated filtering helper:**
1186+
1187+
```rust
1188+
pub fn anomalies_at_least(anomalies: &[EseStructuralAnomaly], min: Severity) -> Vec<&EseStructuralAnomaly> {
1189+
anomalies.iter().filter(|a| a.at_least(min)).collect()
1190+
}
1191+
```
1192+
1193+
**TDD plan — Phase 18-B:**
1194+
1195+
RED commit: Add `EseIntegrity` stub returning empty `Vec`; write tests for unified API (empty buffer returns `TruncatedDatabase`; clean minimal database returns empty vec; page with bad checksum returns `PageChecksumMismatch`).
1196+
1197+
GREEN commit: Implement `analyse()` by composing layer checks; implement `check_layout()` truncation guard; implement `check_pages()` page-checksum loop using existing `ese-core` checksum functions.
1198+
1199+
---
1200+
1201+
### Phase 18-C — Input robustness hardening
1202+
1203+
**Crate:** `ese-core`
1204+
**Files modified:** `crates/ese-core/src/page.rs`, `crates/ese-core/src/database.rs`
1205+
1206+
#### Specific robustness gaps to close
1207+
1208+
**1. Page count integer overflow**
1209+
1210+
`EseHeader` stores `last_page_number: u32`. Iteration multiplies by `DB_PAGE_SIZE` (4096 or 8192 for large pages). No overflow check.
1211+
1212+
Fix: explicit `usize` overflow check on `page_number as usize * page_size`; return `Err` or emit `TruncatedDatabase` anomaly.
1213+
1214+
**2. Page size trust**
1215+
1216+
`EseHeader::page_size` is read directly as a slice offset multiplier. A crafted header with `page_size = 0` or `page_size > 32768` panics.
1217+
1218+
Fix: validate `page_size ∈ {4096, 8192, 16384, 32768}` immediately after header parse; reject all other values.
1219+
1220+
**3. Record offset trust within page**
1221+
1222+
Tag array entries in a page contain `offset` + `size` fields. No bounds check against `DB_PAGE_SIZE`.
1223+
1224+
Fix: check `offset + size <= page_bytes.len()` before slicing; return `Err` on violation.
1225+
1226+
**4. Checksum algorithm selection**
1227+
1228+
Two checksum variants (XOR-based legacy, ECC-based Vista+). The selection flag `PAGE_FLAG_NEW_FORMAT` may not be set on all Vista+ images. Add fallback: try both; report mismatch only if neither matches.
1229+
1230+
**TDD plan — Phase 18-C:**
1231+
1232+
For each gap: write a test with a crafted malformed buffer that currently panics or gives wrong output; then fix the parsing path. Tests live in `ese-core/tests/robustness_tests.rs` and `ese-integrity/tests/robustness_tests.rs`.
1233+
1234+
---
1235+
1236+
### Implementation order
1237+
1238+
1. Phase 18-A RED → Phase 18-A GREEN (severity + new variants)
1239+
2. Phase 18-B RED → Phase 18-B GREEN (unified analyser) — depends on Phase 18-A severity enum
1240+
3. Phase 18-C (robustness hardening) — independent; can be done in parallel with 18-B
1241+
1242+
### Files to create / modify (Phase 18)
1243+
1244+
| Action | Path | Purpose |
1245+
|---|---|---|
1246+
| Modify | `crates/ese-integrity/src/lib.rs` | Add `Severity`, `severity()`, 6 new anomaly variants, `EseIntegrity` struct |
1247+
| Create | `crates/ese-core/tests/robustness_tests.rs` | Malformed input tests for page/header parsing |
1248+
| Create | `crates/ese-integrity/tests/robustness_tests.rs` | Malformed input tests for unified analyser |
1249+
1250+
### What is deliberately excluded
1251+
1252+
**No `ese-repair` crate.** ESE databases have a dual-shadow header (shadow copy at page 1), which is the only structurally repairable component. However, rewriting evidence file checksums or page-level bytes without an out-of-band reference is forensically dangerous. Detection and documentation of the anomaly is the correct forensic response. Callers who need working-but-annotated data can use `ese-carver` for page-level recovery.

0 commit comments

Comments
 (0)