Building a Linux Super Timeline with Plaso and mactime
Build a Linux forensic super timeline with TSK mactime and Plaso: log2timeline, psort and pinfo, Linux parsers, time slicing, UTC and Timesketch.
A single artifact rarely tells the whole story. A sshd login in the journal, a file dropped in /tmp, a new cron entry and a package installation are four separate facts until you put them on one axis. A super timeline merges file system metadata and parsed log content into one chronologically sorted list, so you can start from one confirmed event and read what happened immediately before and after it. See the super timeline glossary entry for the concept.
This guide covers two approaches: a fast file system timeline with The Sleuth Kit, and a full super timeline with Plaso. Both work on a forensic image or read-only mount (see acquiring Linux evidence), never on the live disk.
Why timelines matter
- Context. An event is interpreted by its neighbours. A
wgetis benign on its own; five seconds after an unexpected login it is not. - Scoping. The timeline shows first and last attacker activity, which bounds the investigation window and the data exposure question.
- Contradictions. Timestamps from independent sources should agree. When a file's modification time predates the login that created it, suspect timestomping (see ext4 and XFS timestamps).
What a timeline does not prove: a timestamp is the last recorded change, not a history. Overwritten metadata, noatime/relatime mount options and rotated logs all leave holes.
Quick file system timeline with The Sleuth Kit
fls walks the file system and writes a body file; mactime sorts it.
mmls /cases/web-prod-03.raw # find the partition offset (in sectors)
fls -r -m / -o 2048 /cases/web-prod-03.raw > body.txt
mactime -b body.txt -d -y -z UTC 2026-09-19..2026-09-21 > fs-timeline.csv
-m / prefixes paths with a mount point, -o is the sector offset, -d writes comma-separated output and -y prints ISO 8601 dates, always in UTC. mactime ignores -z for the output in that mode (without -y, -z picks the zone used to render dates); here it only makes the date range on the command line UTC as well. Each body file line holds MD5|name|inode|mode|UID|GID|size|atime|mtime|ctime|crtime, and mactime collapses these into the familiar macb flags per row. On ext4, TSK reports the creation time (see ext4 crtime); support for other file systems such as XFS and Btrfs depends on your TSK version, so check the output rather than assuming.
This is fast and precise for file activity, but blind to everything inside logs.
Plaso: log2timeline, psort and pinfo
Plaso is a framework built around these command-line tools:
| Tool | Role | Typical output |
|---|---|---|
log2timeline.py | Extracts events from a source into a storage file | timeline.plaso (SQLite-based) |
pinfo.py | Reports what was processed, parser counts, warnings | text summary |
psort.py | Filters, sorts, deduplicates and exports events | CSV, JSON lines, OpenSearch |
psteal.py | Runs extraction and output in one step | CSV |
The simplest reproducible way to run it is the official Docker image:
docker run --rm -v /cases:/data log2timeline/plaso \
log2timeline.py --storage-file /data/web-prod-03.plaso \
--parsers linux --timezone Europe/Paris \
--partitions all --volumes all \
/data/web-prod-03.raw
--timezone tells Plaso which zone to assume for sources that do not record one, chiefly traditional syslog files. Recover it from /etc/localtime in the image first. It does not affect sources that store UTC, such as the journal.
Linux parsers
The linux preset selects the parsers and plugins relevant to a Linux host. Useful ones include:
| Parser / plugin | Artifact |
|---|---|
text/syslog, text/syslog_traditional | /var/log/syslog, messages, auth.log, secure |
systemd_journal | binary journal files under /var/log/journal |
utmp | wtmp, btmp, utmp login records |
text/bash_history | .bash_history with #epoch timestamp lines |
text/zsh_extended_history | zsh history in extended format |
text/dpkg, text/apt_history | /var/log/dpkg.log, /var/log/apt/history.log |
text/selinux | SELinux and auditd audit.log format |
jsonl/docker_container_config, jsonl/docker_container_log, jsonl/docker_layer_config | Docker container config, JSON log and layer files |
filestat | file system timestamps for every file |
In recent Plaso releases many text formats are plugins of a single text parser (and JSON-lines formats of jsonl), so a custom --parsers expression must name them with the prefix, such as text/bash_history, and exact names can change between versions. List what your build supports with log2timeline.py --info before writing a custom --parsers expression. Note that bash_history only yields timestamps if HISTTIMEFORMAT was set on the host; otherwise commands have no individual times (see shell history and user activity).
Traditional syslog lines have no year. Plaso infers it from context, and you can supply --preferred_year when inference fails, typically for logs that span a year boundary.
Checking the extraction
pinfo.py /cases/web-prod-03.plaso
Review the event counts per parser and the extraction warnings. A journal parser count of zero on a systemd host means either a volatile journal or a path Plaso could not reach, and both are findings worth recording.
Exporting and filtering
psort.py -o dynamic --output_time_zone UTC \
-w /cases/web-prod-03-full.csv /cases/web-prod-03.plaso
psort.py -o l2tcsv --output_time_zone UTC -w /cases/window.csv /cases/web-prod-03.plaso \
"date > '2026-09-20 02:30:00' AND date < '2026-09-20 05:00:00'"
psort.py -o json_line --output_time_zone UTC -w /cases/slice.jsonl \
--slice "2026-09-20T03:14:07" --slice_size 30 /cases/web-prod-03.plaso
dynamic is the default column layout, l2tcsv is the legacy log2timeline CSV that many spreadsheets and scripts expect, and json_line writes one JSON object per event for jq or ingestion into other tools. The filter expression selects a time range; --slice with --slice_size (in minutes) extracts a window centred on a timestamp, which is the fastest way to pivot around an anchor event.
Timesketch for collaborative analysis
Large timelines are painful in a spreadsheet. Timesketch, the companion web platform, ingests .plaso files directly (upload through the web interface or the importer client) and adds search, tagging, saved views, comments and analyzers. Use it when several analysts share a case or when the timeline has millions of rows.
Worked example (fictional)
Illustrative scenario: host web-prod-03, account deploy, attacker IP 203.0.113.50. The anchor is an Accepted password for deploy from 203.0.113.50 line in auth.log, confirmed by a systemd_journal event and a wtmp record. Slicing 30 minutes around it with UTC output gives, simplified:
2026-09-20T03:12:46Z syslog sshd: Failed password for invalid user admin from 203.0.113.50
2026-09-20T03:14:07Z syslog sshd: Accepted password for deploy from 203.0.113.50 port 40318
2026-09-20T03:14:07Z utmp login deploy pts/0 from 203.0.113.50
2026-09-20T03:15:30Z syslog sudo: deploy : USER=root ; COMMAND=/usr/bin/bash
2026-09-20T03:16:02Z filestat ..cb /tmp/.cache/x.sh (inode 393251)
2026-09-20T03:16:40Z dpkg status installed netcat-openbsd
2026-09-20T03:17:15Z filestat m.c. /etc/cron.d/sysupdate
2026-09-20T03:21:48Z utmp logout deploy pts/0
Reading it: a password spray precedes a successful login, the account escalates with sudo, a script appears in a hidden directory under /tmp, a networking tool is installed and a cron file is written, which points you to hunting persistence. Every row is then verified at the source artifact before it goes into the report, and each new indicator (the script path, the cron file) becomes the next pivot.
Pitfalls
- Mixed time zones. Always export with
--output_time_zone UTCand state it in the report. - Local syslog time. A wrong
--timezoneshifts every syslog event by hours relative to the journal. Cross-check one event present in both. - Volume. A full
filestatpass produces millions of rows. Filter by time slice or path before reviewing. - Clock drift. The host clock may have been wrong or changed. Look for time synchronisation messages in the journal.
- Absence of events. An empty window may mean log deletion, rotation or a volatile journal, not inactivity.
Key takeaways
- Use
fls+mactimefor a quick file system view, Plaso for the full super timeline. - Set the source
--timezonefor syslog and always export in UTC. - Run
pinfo.pyto confirm which parsers produced events before trusting gaps. - Pivot from a confirmed anchor event with
--sliceand verify each row at its source. - Move large or shared timelines into Timesketch.