Cloud Forensics & Investigation
Definition
Section titled “Definition”Cloud forensics is the acquisition and analysis of evidence from cloud environments — audit logs, storage snapshots, identity-provider events, container telemetry, and provider-side artifacts — under the same chain-of-custody discipline as disk forensics (BT5), adapted to a substrate where you own the logs but not the hardware. The structural differences from on-prem forensics define the craft: evidence is API-mediated (every acquisition — snapshot, log export, ticket export — is an API call that itself generates audit records, which must be captured as part of the evidence), order of volatility inverts (ephemeral compute dies fastest, but the audit log persists longer than any disk — the timeline lives in the log lake, not on the instance), multi-tenancy limits acquisition (you snapshot your resources; the provider’s hypervisor evidence requires legal process — a scope boundary the investigation plan must state upfront), and identity is the crime scene (most cloud intrusions are credential-based, so the principal’s action history across services — not a single host — is the primary evidence object). The craft’s center: cloud forensics is investigation by log query done with forensic discipline — timestamps normalized to UTC, every acquisition hashed and registered, scope changes logged, and the analysis reproducible from the preserved exports alone.
Why It Matters
Section titled “Why It Matters”The BT16 module established that cloud intrusions are API-speed and log-complete; forensics is the discipline that turns that completeness into findings: what exactly did the compromised principal do, when, from where, with what blast radius, and what did they touch that we haven’t found yet. The post-incident questions leadership, regulators (BT19’s clocks), and insurers ask are all forensic questions — and cloud gives the best raw material any forensic discipline has ever had, if the team can acquire it correctly: logs retained long enough (BT16’s retention doctrine), snapshots taken before teardown (BT16’s evidence-first response), identity events joined across services (BT15’s session governance), and every step documented to BT5’s standard. Without that discipline, the same richness becomes chaos: timestamps in three timezones, exports without hashes, an instance terminated before snapshot, and an investigation that cannot defend its own conclusions.
The failure modes are forensic-shaped. The teardown loss: responders clean up (delete the compromised keys, terminate the instance) before evidence capture — the attacker’s actions on disk die with the disk; BT16’s snapshot-before-teardown rule exists but needs the forensics discipline behind it. Acquisition blindness: collecting the audit log of the compromised account but not the whole account estate — the lateral movement into a second principal is invisible until someone queries across principals. Timestamp soup: events correlated across services without UTC normalization — the timeline reads as impossible (response before access) and the analysis collapses under cross-examination. Untracked acquisitions: snapshots and exports taken without hashes, names, or registry entries — the evidence pack that cannot prove its own integrity is worth roughly nothing in a dispute. And scope drift: the investigation that starts at one bucket and ends up reading the whole estate — without a scope ledger, the findings’ defensibility erodes with every undocumented expansion.
Detailed Explanation
Section titled “Detailed Explanation” EVIDENCE SOURCES (per cloud, join by principal + time) +---------------------+ +---------------------+ +---------------------+ | CONTROL PLANE LOGS | | DATA PLANE | | IDENTITY PLANE | | CloudTrail / Activity| | storage access logs,| | IdP sign-ins, MFA, | | / Audit Logs | | S3 data events, DB | | token issuance, | | (who did what API) | | query logs | | conditional-access | +----------+----------+ +----------+----------+ | decisions | | | +----------+----------+ +------------+-----------+------------------------+ v +----------------------------+ | EVIDENCE REGISTER (S1) | | every acquisition: id, ts, | | source, scope, hash, actor | +-------------+--------------+ | join + normalize (UTC) v +----------------------------+ snapshots BEFORE teardown | TIMELINE (per principal) | (BT16 evidence-first rule) | what / when / from where / | | with what parameters | +-------------+--------------+ v +----------------------------+ | FINDINGS + REPRODUCIBILITY | | queries preserved with the | | exports they ran against | +----------------------------+Acquisition discipline is BT5’s chain of custody, cloud-adapted. Every evidence object gets a register entry: evidence ID, acquisition timestamp (UTC), source (service, account, region), scope (what exactly), hash (SHA-256 of the export; snapshot IDs with provider hashes where offered), acquiring actor, and the reason (which investigation question motivated it). Acquisitions are API calls — the audit records of the acquisition itself join the evidence pack (the investigation must be able to prove it didn’t alter the scene beyond acquisition, and the acquisition’s own log entries are that proof). Snapshots precede teardown, always: the BT16 evidence-first rule becomes here a forensic requirement with an ordering (snapshot volumes → export logs → export config → then remediate).
Timeline construction is the analysis core: normalize every event to UTC (the log sources record in UTC, but analyst tooling, ticket systems, and human memory do not — the normalization happens once, in the join, and every downstream artifact states its timezone), join by principal across services (the control-plane action, the identity event that authenticated it, the data-plane access it enabled), and order. The principal-centric view (not host-centric) is the cloud’s structural gift: one query returns the compromised identity’s complete action history — every API call with parameters, source IP, and user-agent — which is the intrusion narrative at a fidelity disk forensics rarely reaches.
Deep dive: the acquisition audit trail — proving you didn’t become the attacker
Every forensic acquisition is an API call against the live estate, and the evidence pack must be able to prove the investigation changed nothing beyond reading: the acquisition’s own audit entries (the CloudTrail/Activity records of your snapshot, export, and read calls) are captured as evidence themselves, keyed to the register entries they correspond to. This is more than hygiene — it answers the defense question “who else touched this?” with a complete list that includes the investigators, and it catches the genuinely dangerous mistakes: the analyst who ran their queries with an over-privileged role (their own API calls in the pack show the blast radius of the investigation), the second responder whose export overwrote a file (the register’s timestamps expose the collision), and the investigator whose home-IP appears in the acquisition log alongside the attacker’s (a career moment best discovered by yourself, first). The discipline: acquisitions run from a dedicated, minimal-permission forensic role — its every action lands in the pack, and its permissions are the scope ledger’s technical enforcement.
Reproducibility is the cloud forensic superpower when kept, and the Achilles heel when lost: the log exports are the evidence, but the queries that produced findings must be preserved with them — because the exports are point-in-time snapshots of append-only logs, re-running the query against the preserved export reproduces the finding exactly; re-running it against the live log lake produces different results as new events arrive. The investigation pack therefore contains: exports (hashed), queries, the join code, and the findings’ derivations — anyone can re-derive every conclusion from the pack alone.
Common Techniques
Section titled “Common Techniques”| Technique | What it does | Where it fails | Notes |
|---|---|---|---|
| Evidence register | Every acquisition hashed + reasoned | Acquisitions untracked | BT5 discipline, cloud edition |
| Snapshot-before-teardown | Volume/state preserved pre-remediation | Responders clean up first | BT16 join, enforced order |
| Acquisition self-logging | The investigation’s own API calls captured | Investigator alters the scene | Audit-of-the-audit |
| Principal-centric timeline | Identity’s actions across all services | Host-centric habits | Join by principal + UTC |
| UTC normalization | One clock across all sources | Tooling-local timestamps | Normalize once, in the join |
| Query preservation | Findings re-derivable from exports | Queries lost, live-log re-runs | Reproducibility pack |
| Multi-account scoping | Lateral movement across principals found | Single-account collection | Estate-wide query discipline |
| Metadata/versioning analysis | Object versions show overwrite history | Versioning disabled buckets | Pre-incident control |
| Legal-process boundary | Provider-side evidence scoped correctly | Assumed provider cooperation | Counsel engagement early |
| Order-of-volatility cloud map | Ephemeral→persistent acquisition order | On-prem instincts applied | Compute < logs < snapshots |
Full Working Code
Section titled “Full Working Code”cloudevi — the cloud evidence register and collection planner: reads an asset list (CSV: asset_id, type, cloud, region, priority) and an investigation window, emits the collection plan (ordered acquisition steps with snapshot-before-teardown enforcement, per-asset commands as documented templates) and initializes the evidence register (every acquisition recorded with id, ts, source, scope, hash placeholder, actor) — the register both tools and humans append to during the investigation.
#!/usr/bin/env python3"""cloudevi — cloud evidence register + collection plan.
Inputs: assets.csv: asset_id,type,cloud,region,priority (type: vm|disk|bucket|log|identity; priority: 1-3, 1 first) window.txt: two ISO timestamps (start, end) — investigation windowCommands: plan --assets assets.csv --window window.txt (collection plan) record --register evidence_register.csv --asset A --source S --scope SC --sha256 H --actor ANA1 --note "..." (append acquisition) register --register evidence_register.csv (view, verify hashes)"""import argparseimport csvimport hashlibimport sysfrom datetime import datetimefrom pathlib import Path
REQ_A = ["asset_id", "type", "cloud", "region", "priority"]REQ_R = ["evidence_id", "acquired_utc", "asset_id", "source", "scope", "sha256", "actor", "note"]TYPE_ORDER = {"identity": 0, "log": 1, "disk": 2, "vm": 3, "bucket": 4}
def fail(msg): sys.exit(f"fail-loud: {msg}")
def parse_window(path): try: lines = [l.strip() for l in path.read_text().splitlines() if l.strip()] except OSError as e: fail(f"window unreadable: {e}") if len(lines) < 2: fail("window file needs start and end ISO timestamps") try: start = datetime.fromisoformat(lines[0].replace("Z", "+00:00")) end = datetime.fromisoformat(lines[1].replace("Z", "+00:00")) except ValueError as e: fail(f"window timestamps unparseable: {e}") if end <= start: fail("window end must be after start") return start, end
def cmd_plan(a): if not a.assets.exists(): fail(f"assets not found: {a.assets}") with a.assets.open(encoding="utf-8", errors="replace") as fh: reader = csv.DictReader(fh) missing = [c for c in REQ_A if c not in (reader.fieldnames or [])] if missing: fail(f"assets missing columns: {missing}") rows = list(reader) if not rows: fail("asset list empty — nothing to collect is not a collection plan") start, end = parse_window(a.window)
rows.sort(key=lambda r: (int(r["priority"]) if r["priority"].strip() .isdigit() else 9, TYPE_ORDER.get(r["type"].strip().lower(), 9))) steps = [] for i, r in enumerate(rows, 1): atype = r["type"].strip().lower() cmd = { "vm": f"snapshot vm {r['asset_id']} (before ANY teardown)", "disk": f"snapshot disk {r['asset_id']} (hash on completion)", "bucket": (f"enable/verify versioning on {r['asset_id']}; " "export access logs for window"), "log": (f"export audit logs {r['asset_id']} " f"{start.isoformat()} .. {end.isoformat()}"), "identity": (f"export IdP events for {r['asset_id']} " "including token issuance"), }.get(atype, f"document manual acquisition for {r['asset_id']}") steps.append({"order": i, "asset_id": r["asset_id"], "type": atype, "cloud": r["cloud"], "region": r["region"], "priority": r["priority"], "action": cmd})
args_out = Path("cloudevi_out") args_out.mkdir(parents=True, exist_ok=True) with (args_out / "collection_plan.csv").open("w", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=["order", "asset_id", "type", "cloud", "region", "priority", "action"]) w.writeheader() w.writerows(steps)
print(f"window: {start.isoformat()} .. {end.isoformat()}") print(f"collection plan: {len(steps)} assets, " f"identity/log before compute (order of volatility, cloud-style)") for s in steps[:8]: print(f" {s['order']:>2}. [{s['type']:<8}] {s['asset_id']:<28} " f"{s['action'][:52]}") if len(steps) > 8: print(f" ... {len(steps) - 8} more") print(f"plan: {args_out / 'collection_plan.csv'}") print("rule: snapshot before teardown; hash every export; " "log your own acquisition calls")
def next_eid(register_path): if not register_path.exists(): return "EV-0001" with register_path.open(encoding="utf-8") as fh: rows = list(csv.DictReader(fh)) return f"EV-{len(rows) + 1:04d}"
def cmd_record(a): reg = a.register eid = next_eid(reg) if not a.sha256 or len(a.sha256) != 64: fail("sha256 required (64 hex chars) — an unhashed acquisition " "is not evidence") rec = {"evidence_id": eid, "acquired_utc": datetime.utcnow().isoformat(timespec="seconds") + "Z", "asset_id": a.asset, "source": a.source, "scope": a.scope, "sha256": a.sha256.lower(), "actor": a.actor, "note": a.note} newfile = not reg.exists() with reg.open("a", newline="", encoding="utf-8") as fh: w = csv.DictWriter(fh, fieldnames=REQ_R) if newfile: w.writeheader() w.writerow(rec) print(f"registered {eid}: {rec['asset_id']} ({rec['scope']})")
def cmd_register(a): if not a.register.exists(): fail("register empty — no acquisitions recorded yet") with a.register.open(encoding="utf-8") as fh: rows = list(csv.DictReader(fh)) if not rows: fail("register file exists but is empty — verify it is the " "right file") print(f"{'id':<9}{'acquired (UTC)':<22}{'asset':<22}{'scope':<24}actor") for r in rows: print(f"{r['evidence_id']:<9}{r['acquired_utc']:<22}" f"{r['asset_id']:<22}{r['scope'][:22]:<24}{r['actor']}") print(f"{len(rows)} evidence objects — hashes stored; " "verify against originals before production to counsel")
def main(): ap = argparse.ArgumentParser(description="cloud evidence register") sub = ap.add_subparsers(dest="cmd", required=True)
p1 = sub.add_parser("plan") p1.add_argument("--assets", type=Path, required=True) p1.add_argument("--window", type=Path, required=True)
p2 = sub.add_parser("record") p2.add_argument("--register", type=Path, default=Path( "evidence_register.csv")) p2.add_argument("--asset", required=True) p2.add_argument("--source", required=True) p2.add_argument("--scope", required=True) p2.add_argument("--sha256", required=True) p2.add_argument("--actor", required=True) p2.add_argument("--note", default="")
p3 = sub.add_parser("register") p3.add_argument("--register", type=Path, default=Path( "evidence_register.csv"))
args = ap.parse_args() {"plan": cmd_plan, "record": cmd_record, "register": cmd_register}[args.cmd](args)
if __name__ == "__main__": main()Code Walkthrough
Section titled “Code Walkthrough”The window parser (lines 33-47) is the plan’s first integrity gate: two ISO timestamps, end-after-start, or the plan refuses — because an acquisition window that silently inverts (a timezone slip making “end” before “start”) produces log exports that look complete and miss half the incident. The plan generator (lines 50-79) sorts by priority then by cloud-order-of-volatility (TYPE_ORDER: identity and logs before compute — the inversion from disk forensics, where RAM dies first; in cloud, the logs are the most durable and the identity picture is the most load-bearing, while compute snapshots are urgent only because remediation pressure mounts). The per-type command templates (lines 62-69) encode the ordering rules where they matter: vm and disk say “before ANY teardown”, bucket says “enable/verify versioning” first — because object-version analysis (the overwrite history that shows data tampering) is only possible if versioning existed or is enabled before more overwrites occur.
The register (cmd_record, lines 88-104) enforces the chain-of-custody minimums: a 64-hex SHA-256 (line 92 — “an unhashed acquisition is not evidence”, the fail-loud that prevents the most common integrity hole), UTC acquisition timestamp, acquiring actor, and scope. Sequential evidence IDs (lines 82-86) make references stable across the investigation’s documents. The register view (lines 107-118) prints the pack’s inventory and ends with the verification instruction — hashes stored are claims until verified against originals before production to counsel (BT5’s discipline, verbatim).
How It Reaches The Target
Section titled “How It Reaches The Target”The incident (BT16’s leaked-key scenario) triggers the forensic phase once containment holds. The collection plan runs in order: identity events export first (the compromised principal’s complete sign-in, MFA, and token-issuance history — including the attacker’s own new key creation), audit logs export for the window plus margin (the window edges get 24h padding because first-contact often predates detection), volume snapshots for the affected compute (before the responders’ re-image), and bucket versioning verified before the data-plane analysis — because the S3 object versions will show exactly what the attacker read, overwrote, or staged.
The analysis joins by principal: the audit log’s every API call (timestamped, parameterized, source-IP’d) merges with the IdP events (how the credential was used, from where, with what MFA posture) and the data-plane records (what the stolen key actually touched). The timeline answers the questions with timestamps: first adversarial action (GetCallerIdentity at 02:14 from a residential ASN), escalation (AttachRolePolicy 02:19), persistence (CreateAccessKey 02:21), data staging (43 GetObject calls across two buckets 02:30-02:47), last observed activity (02:58). Every finding carries its query; every export carries its hash in the register; the acquisition’s own audit entries are in the pack — so when counsel, the insurer, or the regulator asks “how do we know?”, the answer is a demonstration, not a narrative. The register closes the loop with BT5’s discipline: evidence IDs referenced in the BT4 incident report, the BT19 notifications, and the lessons-learned — the investigation that can defend itself is the one that improves the next one.
Exploitation Chain
Section titled “Exploitation Chain”- Trigger — BT16 containment holds; forensic phase opens with scope and window declaration
- Plan — cloudevi orders acquisitions: identity and logs first, compute snapshots before teardown
- Acquire — exports and snapshots hashed into the evidence register; the investigation’s own API calls logged
- Normalize — all events to UTC; join by principal across control/data/identity planes
- Reconstruct — the principal’s complete action timeline: first access, escalation, persistence, data touch
- Analyze — blast radius (what data), method (which technique), scope (which other principals/roles)
- Reproduce — queries preserved with exports; every finding re-derivable from the pack alone
- Report — evidence IDs into BT4’s report, BT19’s notifications, BT5’s archive; lessons to BT20
Building Your Own Tool
Section titled “Building Your Own Tool”The timeline is the investigation’s core artifact; logwindow builds it: given the audit-log export (JSONL, CloudTrail-shaped with eventName/principal/source_ip/eventTime) and the investigation window plus principal filter, it extracts the in-window events, normalizes timestamps, and produces the per-principal activity timeline with source-IP transitions — the analyst’s working view.
Design decisions
Section titled “Design decisions”Principal-first: the timeline groups by principal (the investigation’s key object), ordered by normalized UTC time — source-IP changes flagged inline (the transition from corporate ASN to residential is the story’s pivot). Window filtering with margin: events within the window plus a configurable pre-window (first-contact hunting) are included but tagged, so the timeline shows what preceded the incident window without pretending it’s in-scope. Field tolerance: audit exports vary (Azure/GCP mappings); the tool takes normalized fields (the BT16 pipeline contract) and fails loud on unmapped records rather than silently dropping them — an investigation that silently loses events is worse than one that refuses to run.
Full source
Section titled “Full source”#!/usr/bin/env python3"""logwindow — per-principal activity timeline from an audit-log export.
Input: JSONL with eventName, principal, source_ip, eventTime (ISO/UTC)Usage: python3 logwindow.py audit.jsonl --start "..." --end "..." \ [--margin-hours 24] [--principal name]Output: per-principal timeline (chronological, IP transitions flagged)."""import argparseimport jsonimport sysfrom collections import defaultdictfrom datetime import datetime, timedeltafrom pathlib import Path
REQ = ("eventName", "principal", "source_ip", "eventTime")
def parse_ts(raw): try: return datetime.fromisoformat(str(raw).replace("Z", "+00:00")) except (ValueError, TypeError): sys.exit(f"fail-loud: unparseable eventTime {raw!r} — normalize " "timestamps in the pipeline, not here")
def load(path): if not path.exists(): sys.exit(f"fail-loud: export not found: {path}") events, bad = [], 0 with path.open(encoding="utf-8", errors="replace") as fh: for n, line in enumerate(fh, 1): line = line.strip() if not line: continue try: rec = json.loads(line) except json.JSONDecodeError: bad += 1 continue missing = [k for k in REQ if rec.get(k) in (None, "")] if missing: bad += 1 print(f"WARN: line {n}: missing {missing} — skipped " "(logged, not silent)", file=sys.stderr) continue events.append(rec) if not events: sys.exit("fail-loud: zero usable events — export is broken") return events, bad
def asn(ip): parts = ip.split(".") return ".".join(parts[:3]) if len(parts) == 4 else ip
def main(): ap = argparse.ArgumentParser(description="audit timeline builder") ap.add_argument("export", type=Path) ap.add_argument("--start", required=True) ap.add_argument("--end", required=True) ap.add_argument("--margin-hours", type=int, default=24) ap.add_argument("--principal", default=None, help="filter to one principal (default: all)") ap.add_argument("--out", type=Path, default=Path("logwindow_out")) args = ap.parse_args()
start = parse_ts(args.start) end = parse_ts(args.end) if end <= start: sys.exit("fail-loud: window end must be after start") margin = timedelta(hours=args.margin_hours) lo, hi = start - margin, end + margin
events, bad = load(args.export) for e in events: e["_ts"] = parse_ts(e["eventTime"]) e["principal"] = str(e["principal"]) e["source_ip"] = str(e["source_ip"]) e["eventName"] = str(e["eventName"])
in_scope = [e for e in events if lo <= e["_ts"] <= hi] if args.principal: in_scope = [e for e in in_scope if e["principal"] == args.principal] if not in_scope: sys.exit("fail-loud: zero events in window — wrong window, wrong " "export, or wrong principal filter")
by_principal = defaultdict(list) for e in in_scope: by_principal[e["principal"]].append(e)
args.out.mkdir(parents=True, exist_ok=True) out_lines = [f"# Activity timeline window {start.isoformat()} .. " f"{end.isoformat()} (margin {args.margin_hours}h)", f"# events in scope: {len(in_scope)} skipped (bad): {bad}", ""] for principal in sorted(by_principal, key=lambda p: -len(by_principal[p])): evs = sorted(by_principal[principal], key=lambda e: e["_ts"]) out_lines.append(f"## {principal} ({len(evs)} events)") last_asn = None for e in evs: cur = asn(e["source_ip"]) flag = "" if last_asn is not None and cur != last_asn: flag = f" <-- IP TRANSITION {last_asn} -> {cur}" tag = "MARGIN" if not (start <= e["_ts"] <= end) else " " out_lines.append( f"{e['_ts'].isoformat()} {tag} {e['eventName']:<28} " f"{e['source_ip']:<16}{flag}") last_asn = cur out_lines.append("")
(args.out / "timeline.md").write_text("\n".join(out_lines), encoding="utf-8") print(f"principals={len(by_principal)} events={len(in_scope)} " f"skipped={bad}") for p in sorted(by_principal, key=lambda x: -len(by_principal[x]))[:5]: print(f" {p:<36} {len(by_principal[p]):>6} events") print(f"timeline: {args.out / 'timeline.md'}")
if __name__ == "__main__": main()Walkthrough
Section titled “Walkthrough”The load gate (lines 24-46) implements the field-tolerance rule as loud failure: records missing any of the four normalized fields print a stderr warning with line numbers and are counted — the final summary reports the skip count, so “the timeline shows 340 events” never silently means “the export had 380”. The timestamp parser (lines 19-23) refuses to normalize here: the pipeline (BT16’s) owns normalization, and the tool failing on mixed-format input is the guard that keeps timezone soup out of the evidence.
The window logic (lines 73-83) applies the margin with explicit tagging — margin events appear in the timeline tagged MARGIN (first-contact hunting: the attacker’s reconnaissance before the detected window is often the investigation’s most valuable finding) without conflating scope. The principal grouping (lines 86-88) sorts most-active-first (the compromised principal is usually the noisiest), and the IP-transition flagging (lines 95-101) prints the ASN-level pivot inline — the residential-ASN-to-datacenter transition that marks attacker-vs-victim activity is visible on the timeline rather than buried in a spreadsheet.
Compilation and test procedure
Section titled “Compilation and test procedure”after cloudevi planned and the exports landed (hashed, registered)
Section titled “after cloudevi planned and the exports landed (hashed, registered)”$ python3 logwindow.py audit_export.jsonl
—start “2026-10-10T00:00:00Z” —end “2026-10-11T00:00:00Z”
—principal arn:aws:iam::111:user/ci-bot
principals=1 events=214 skipped=0
arn:aws:iam::111:user/ci-bot 214 events
timeline: logwindow_out/timeline.md
$ head -12 logwindow_out/timeline.md
Activity timeline window 2026-10-10T00:00:00+00:00 .. 2026-10-11T00:00:00+00:00
Section titled “Activity timeline window 2026-10-10T00:00:00+00:00 .. 2026-10-11T00:00:00+00:00”events in scope: 214 skipped (bad): 0
Section titled “events in scope: 214 skipped (bad): 0”arn:aws:iam::111:user/ci-bot (214 events)
Section titled “arn:aws:iam::111:user/ci-bot (214 events)”2026-10-09T21:02:11+00:00 MARGIN ListBuckets 198.51.100.7 2026-10-10T02:14:03+00:00 GetCallerIdentity 203.0.113.9 <— IP TRANSITION 198.51.100 -> 203.0.113 2026-10-10T02:19:44+00:00 AttachUserPolicy 203.0.113.9 2026-10-10T02:21:02+00:00 CreateAccessKey 203.0.113.9 …
the transition at 02:14 is the intrusion narrative’s opening line
Section titled “the transition at 02:14 is the intrusion narrative’s opening line”Detection considerations
Section titled “Detection considerations”The timeline builder is investigation tooling, but its inputs are detection-surface artifacts: the same normalized audit stream that feeds BT16’s detections feeds S1’s timelines — the pipeline normalization done once serves both. Two watch items: skip-counts climbing across exports (pipeline field-mapping decay — caught in the investigation instead of the detection, which is late) and margin-event patterns (recurring pre-window reconnaissance in multiple investigations from the same ASN range is BT10 intel, not coincidence). The evidence register joins BT5’s archive and BT20’s program metrics: acquisition discipline (every acquisition registered and hashed) is auditable, and the reproducibility pack is the artifact that survives personnel turnover.
Improvements to add
Section titled “Improvements to add”- Accept multiple exports (multi-account estates) with per-source tagging in the timeline.
- Add data-plane event support (S3 access logs) joined into the same principal timeline.
- Emit a JSON timeline for downstream tools (BT4’s case system, BT19’s evidence pack).
- Add geo/ASN annotation from an offline database for richer IP-transition context.
- Verify register hashes against export files in a single
--verifycommand (the chain-of-custody check the register view describes).
Detection — Defender View
Section titled “Detection — Defender View”| Signal | Source | ID | Notes |
|---|---|---|---|
| Evidence register completeness | cloudevi | per incident | Chain-of-custody truth |
| Snapshot-before-teardown order | Collection plan | per incident | The irreversible rule |
| Acquisition self-logs | Audit trail | per investigation | Investigator hygiene |
| UTC normalization coverage | logwindow gates | per export | Timestamp integrity |
| Timeline skip counts | logwindow WARN | per export | Pipeline decay |
| Margin-event patterns | Timeline review | per investigation | Pre-window recon |
| Reproducibility packs | Investigation archive | per incident | Findings re-derivable |
| Retention vs dwell time | BT16 join | quarterly | Evidence availability |
| Legal-process boundary | Counsel log | per incident | Scope honesty |
| Evidence ID references | BT4 reports | per incident | Chain closed |
Evasion Techniques
Section titled “Evasion Techniques”Adversaries attack cloud investigations at each layer. Log-source disablement (BT16’s T1562.001 class) is the primary counter-forensic move: StopLogging, retention shortened, specific event types excluded from the trail — the forensic answer is the impairment alert (zero-tolerance) plus the logging-gap-as-evidence doctrine: the silence itself timestamps the adversary’s awareness. Acquisition blindness engineering: adversaries create second principals and move laterally precisely because single-principal collections miss them — the estate-wide query discipline (collect across principals, not the flagged one) is the counter, and the reason the plan’s scope step precedes execution. Timestamp manipulation: attacker-controlled sources (a compromised host’s local logs) carry skewed clocks; the forensic answer is provider-side clocks only (audit logs, IdP events — infrastructure-generated timestamps the attacker cannot set) and treating host-local time as corroboration, never anchor. Data-plane noise: mass GetObject noise to bury the exfil reads among legitimate access — versioning analysis and byte-volume baselines separate the reads that mattered. Snapshot awareness: sophisticated actors check for, and sometimes delete, investigator snapshots (DeleTeSnapshot of their own trail is discoverable in the audit log — the deletion event is itself evidence of consciousness of guilt). Region hopping (BT16’s lesson, forensic edition): evidence scattered across regions the collection plan missed — the plan’s scope step must enumerate regions per asset class, and the absence of expected regions in the plan is an investigation finding about the estate’s own hygiene.
⚠️ What caught me: our first cloud forensic run acquired beautifully — hashes, register, the works — and then the RCA meeting asked for the timeline, and we produced one where the attacker’s “first action” post-dated the alert that caught them. Twenty minutes of very senior confusion before someone checked timezones: the IdP exports UTC, one S3 access-log view printed local time, and my join had mixed them. The fix was procedural and now non-negotiable: normalization happens in exactly one place (the pipeline), every downstream artifact states its timezone, and any event whose timestamp fails parsing stops the tool rather than “skipping gracefully.” In forensics, a skipped event is a lie you’ll tell under oath; in detection, it’s just a gap. Same tooling discipline, entirely different stakes.
Lab Practice
Section titled “Lab Practice”Run a full cloud forensic exercise on the lab tenant you’ve used since BT16. Stage: have a colleague (or script) perform the leaked-key chain (BT16’s manifest) with two principals and a data-staging read. Plan: author the asset list (the compromised principal, the affected instance, the staged bucket, the audit logs) and run cloudevi plan — review the ordering against your instinct, then against the tool’s. Acquire: execute the plan — exports and snapshots hashed and registered via record; capture the acquisition’s own audit entries. Analyze: run logwindow over the audit export with a 24h margin; confirm the IP transition, the escalation sequence, and the margin-tagged pre-window recon appear. Reproduce: re-run the timeline from a copy of the export on a second machine — confirm identical output (the reproducibility proof). Debrief: which finding relied on data that would have been destroyed by immediate remediation — and does your incident runbook order evidence capture before teardown?
Interview Questions
Section titled “Interview Questions”- Technical: How does cloud order-of-volatility differ from on-prem, and why do identity and control-plane logs precede compute snapshots in the collection plan?
- Technical: Why must the investigation’s own API calls be part of the evidence pack, and what does that prove?
- Scenario: Responders want to delete the compromised keys and re-image the instance now; your snapshots are 40% done. Walk the decision and the negotiation.
- Scenario: Counsel asks how you know the timeline wasn’t altered. Walk the reproducibility demonstration from the evidence pack.
- Conceptual: Why is principal-centric (not host-centric) investigation the cloud-native model, and what does multi-principal lateral movement do to naive collection scope?
- Deep-dive: Design the cloud forensic capability for a multi-account estate: acquisition tooling, register discipline, timeline construction, legal-process boundaries, and the retention architecture that makes it all possible.
- Practical: You have two hours before the incident bridge and zero cloud-forensic runbook. What do you collect first, what do you hash first, and what do you explicitly defer?
Related Modules
Section titled “Related Modules”BT5 (Digital Forensics & Evidence Handling) · BT16 (Cloud Detection & Response) · BT4 (Incident Response Process) · BT17 (Container & Kubernetes Security Ops) · BT19 (Crisis Communication & Leadership)