Timeline analysis
Reconstruct the intrusion; feed new detections.
Reconstructing an intrusion works like investigating a highway pileup. A dashcam, a traffic camera, and the location history on somebody's phone each caught a fragment: different angles, different formats, clocks that disagree by seconds or by hours. No single camera saw the crash. The story only shows up when you lay every frame on one strip, in true order. Timeline analysis does that with digital evidence. It gathers every timestamped trace the attacker left behind, converts all of it to one clock and one shape, sorts it, and lets you read the incident as a story rather than a pile.
Three words first, because everything below leans on them. An *artifact* is any object that holds evidence: a log file, a record from the NTFS (New Technology File System) index on a Windows disk, a shell history, an entry in CloudTrail (the audit log of every API call made inside an AWS account). An *event* is one timestamped fact pulled out of an artifact, like "this file's content changed at 14:09:12 UTC" (Coordinated Universal Time, the single global clock with no daylight saving). A *super timeline* is the merged, normalized, chronologically sorted set of events from all sources at once. The previous lesson covered acquiring and hashing that evidence so it holds up to scrutiny later. Here the pile becomes a narrative, and the narrative becomes new detections.
How a super timeline gets built
The machinery is two jobs: parsing, then normalizing. A timeline engine is a room full of translators, each fluent in exactly one language and useless outside it. It ships hundreds of *parsers*, small readers that each understand a single artifact format: EVTX (the binary format Windows writes its event logs in), NTFS $MFT records (the master file table, the disk's index of every file on it), syslog, browser history, prefetch files, and cloud audit logs in JSON (JavaScript Object Notation, the text format nearly every cloud API emits). Every parser writes into the same output shape: the timestamp converted to UTC, a timestamp_desc field saying what that timestamp actually *means* (content modified, file created, last login), a source label, and a message a human can read. timestamp_desc earns its keep because one artifact yields many events. A single NTFS file record carries four timestamps, modified, accessed, changed and born, known as "MACB", and each one becomes its own row. The record actually keeps two copies of that set, one in an attribute called $STANDARD_INFORMATION and one in $FILE_NAME, which is what lets you catch forged dates later. Once every row shares a schema and a clock, sorting is arithmetic, and the cross-source story falls out by itself.
The payoff is coverage exactly where you were blind. An action that leaves no trace in one source is often loud in another at the same second. Take a webshell, a small script an attacker uploads to a web server so they can run commands on the machine through a browser. Delete it an hour later and it is gone from the directory listing, but the filesystem metadata still recorded the moment it was created, the web server logged the POST request (the method a client uses to push data up to a server) that dropped it, and the EDR (endpoint detection and response agent, the sensor watching processes on the host) logged the process it spawned. Stitching those fragments together is how you scope an intrusion end to end, from foothold through escalation and lateral movement to data leaving the building, instead of chasing whichever fragment you happened to trip over first.
Build one with plaso
plaso is the standard open-source engine here, the Python successor to the original log2timeline. Treat it as two tools with a storage file wedged between them. log2timeline walks a source (a disk image, a mounted directory, or a single file), runs every parser that applies, and writes the events into a .plaso storage file. psort then filters, sorts and exports whatever you ask it for. Splitting extraction from analysis is the whole point: you pay for the slow parse once, then run cheap queries against the result all week.
# Parse a full disk image into a plaso storage file (all partitions, default parsers):log2timeline.py --storage-file webserver.plaso --partitions all webserver.E01# plaso - log2timeline version 20260512# Source path : /evidence/webserver.E01# Source type : storage media image# Processing started.# ... [expect hours, not minutes, for a 60 GB image]# Processing completed.# What did we get?pinfo.py webserver.plaso# Events generated per parser:# apache_access : 91377 filestat : 1912004# bash_history : 1204 syslog : 388112# ...# Total : 2847391
Nearly three million events off one server is normal, not a sign that something went wrong. filestat alone emits up to four rows per file, one per MACB timestamp. Nobody reads that raw. You *bracket* instead. Pick a pivot event you already trust, such as the alert that opened the case or a known-bad hash executing, then cut a window around it with psort so the export holds only what a person will really read. Keep one distinction straight while you do it: that opening alert is a detection firing, which is a claim. It is not yet a confirmed incident. The timeline is how you settle which one you have. Teams who need answers in minutes take the opposite trade, collecting only a short list of high-value artifacts up front (KAPE-style targeted collection, which grabs a named list of artifacts and nothing else, or plaso's own --artifact-filters) and accepting a thinner timeline in exchange for minutes instead of hours.
# Cut a 90-minute window around the pivot (first alert fired 14:02 UTC):psort.py --output-time-zone UTC -o dynamic -w incident.csv webserver.plaso \"date > '2026-07-11 13:30:00' AND date < '2026-07-11 15:00:00'"# incident.csv (columns trimmed):# datetime timestamp_desc source message# 2026-07-11T14:02:11+00:00 Recorded Time LOG apache: POST /upload.php HTTP/1.1 from 203.0.113.50 code 200# 2026-07-11T14:02:14+00:00 Creation Time FILE OS:/var/www/html/.cache.php Type: file# 2026-07-11T14:05:40+00:00 Content Modification Time LOG useradd[2214]: new user: name=sysadm1n, UID=0, GID=0# 2026-07-11T14:09:12+00:00 Creation Time FILE OS:/tmp/nc Type: file
Read those rows out loud and they make a sentence. A POST to an upload endpoint. A PHP file born three seconds later. A root-level user account created three minutes after that. Then a network tool staged in /tmp. Three unrelated artifacts, the web log, the filesystem metadata and syslog, all telling one story.
Cloud logs belong on the same clock
Cloud intrusions are log-native. Often there is no disk to image at all. The attacker still worked both sides of the fence though: a stolen key used from the public internet, and commands typed inside a running pod. Both halves have to land on the same timeline or you will scope half an incident and call it finished. plaso's jsonl parsers (JSON Lines, one JSON object per line) read exported cloud audit logs (aws_cloudtrail_log, azure_activity_log, gcp_log), and Timesketch, the shared timeline interface from the same open-source family, merges several timelines into one view you can search, tag, and work through with colleagues. Timesketch will even run Sigma rules across the merged timeline and auto-tag the suspicious rows, which wires this work straight back to the detection formats covered earlier in the course.
# CloudTrail exports wrap events in a Records array — flatten to JSON Lines first:jq -c '.Records[]' cloudtrail-2026-07-11.json > cloudtrail.jsonllog2timeline.py --storage-file cloud.plaso --parsers jsonl cloudtrail.jsonl# pinfo.py cloud.plaso -> Total : 18442# Upload both timelines into ONE Timesketch sketch (id 42) — they merge into a single view:timesketch_importer --host https://timesketch.corp.example --username analyst \--sketch_id 42 --timeline_name webserver-disk webserver.plasotimesketch_importer --host https://timesketch.corp.example --username analyst \--sketch_id 42 --timeline_name cloudtrail cloud.plaso# (prompts for the password on first run, then caches a session token;# omit --sketch_id and the importer creates a brand-new sketch instead)
Pivot and bracket straight inside the SIEM
When the incident never touched a disk you own, the fastest super timeline is a query rather than a parse job. Your SIEM (security information and event management platform, the searchable store your logs already flow into) is holding normalized, timestamped events right now. The method does not change. *Anchor* on the pivot event. *Bracket* a window of time around it. *Widen* to every table that mentions the same principal (the user or service account doing the acting) or IP address. Sort ascending. In Microsoft Sentinel that means a union across tables, written in KQL (Kusto Query Language, the query language Sentinel speaks):
union withsource=SourceTable SigninLogs, AuditLogs, AzureActivity| where TimeGenerated between (datetime(2026-07-11 13:30) .. datetime(2026-07-11 15:00))| extend Who = coalesce(UserPrincipalName, Caller,tostring(InitiatedBy.user.userPrincipalName))| where Who =~ "[email protected]"| project TimeGenerated, SourceTable, OperationName,IP = coalesce(IPAddress, CallerIpAddress)| sort by TimeGenerated asc// TimeGenerated SourceTable OperationName IP// 2026-07-11T13:41:07Z SigninLogs Sign-in activity 203.0.113.50// 2026-07-11T13:44:12Z AuditLogs Add service principal credential 203.0.113.50// 2026-07-11T13:52:30Z AzureActivity List Storage Account Keys 203.0.113.50// 2026-07-11T14:18:44Z AzureActivity Regenerate Storage Account Keys 203.0.113.50
Same shape as the disk timeline: access, then persistence, then credential collection, one technique per row. The same approach works in Splunk with sort _time across indexes. The tool changes. Anchor, bracket, widen stays put.
Timestamps lie: how to read one without being fooled
Three things make a timeline lie to you, and a fourth one, in the box below, bites more often than any of them. First, forged timestamps. Attackers *timestomp* files, meaning they rewrite the dates so a dropped tool blends in with the operating system files sitting around it. On NTFS the giveaway is comparing the two sets of times a record keeps. Forgery tools usually rewrite the $STANDARD_INFORMATION times and leave the $FILE_NAME times alone, so a file whose "creation" happened before its own filename record was written is a classic red flag. There is a reason it turns up on every forensics exam. Second, clock drift. An appliance or an unmanaged host running without NTP (Network Time Protocol, the service that keeps machine clocks in step) can sit minutes or hours out, quietly reordering your story. Third, absence bias. A quiet stretch may mean nothing happened, or it may mean logs rotated, an agent got killed, or no parser exists for that artifact. Treat every gap as a question, never as an answer.
--output-time-zone UTC in psort, explicit datetime() values in KQL. Any source still emitting zoneless local time is a defect to file and fix, not a quirk to remember.Close the loop
A finished timeline is the richest input detection engineering will ever get, because it is ground truth: these techniques, in this order, in *your* environment. The post-incident review walks it row by row and asks one question of every row. Would we have detected this step while it was happening? Every "no" is a concrete work item somebody can pick up: a log source nobody collects, a Sigma rule to write, a correlation to build, a threshold to tune. Keep the cost honest too. Parsing a full disk image burns hours of compute and a pile of storage, so save super timelines for the incidents that earn them and let SIEM micro-timelines carry the rest. That feedback loop is why detection and DFIR (digital forensics and incident response) are one discipline wearing two names.
The timeline hands you one more asset besides detections: a map of where the attacker actually walked in your environment. That map is a targeting package for you. Rather than only watching those paths, you can seed them with bait no legitimate user would ever touch: a credential that unlocks nothing, a storage bucket with a tempting name and nothing real inside it. That is deception engineering, and honeytokens are where we go next.
Try this
Build a three-row timeline by hand from lab artifacts: one logon, one process creation, one cloud API call. One of the three comes off a host that writes local time with no zone marker on it. Sort the rows as they stand, watch the order come out wrong, then put every row on UTC and sort again.
$ # Three rows, but the middle one came off a host that logs local$ # time (US Eastern) with no zone marker on it:$ printf '%s\n' \"2026-07-24T08:01:12Z,signin,j.alvarez,interactive,lab-wks" \"2026-07-24T04:01:40,process,j.alvarez,schtasks.exe /create,lab-wks" \"2026-07-24T08:02:05Z,cloudtrail,j.alvarez,CreateAccessKey,123456789012" \> /tmp/mini-timeline.csv$ sort /tmp/mini-timeline.csv2026-07-24T04:01:40,process,j.alvarez,schtasks.exe /create,lab-wks2026-07-24T08:01:12Z,signin,j.alvarez,interactive,lab-wks2026-07-24T08:02:05Z,cloudtrail,j.alvarez,CreateAccessKey,123456789012# The scheduled task now sorts ahead of the logon that created it. Effect before cause.$ # 04:01:40 Eastern is 08:01:40 UTC in July. Put that row on UTC, then sort again:$ sed -i 's/^2026-07-24T04:01:40,/2026-07-24T08:01:40Z,/' /tmp/mini-timeline.csv$ sort /tmp/mini-timeline.csv2026-07-24T08:01:12Z,signin,j.alvarez,interactive,lab-wks2026-07-24T08:01:40Z,process,j.alvarez,schtasks.exe /create,lab-wks2026-07-24T08:02:05Z,cloudtrail,j.alvarez,CreateAccessKey,123456789012# Pivots: logon → persistence → credential API. Each pivot → detection ticket.
Takeaway
One clock, one schema, one story. Fix the skew before you read the rows, mark your pivots, and turn each pivot into a detection you have actually tested. An investigation that ends without a new detection cost you a week and bought you nothing.
Next step: pull up your notes from the last incident you worked, rebuild only the five pivot events as a mini-timeline, and file one detection pull request from the earliest pivot you were slow to see.