Skip to content

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.

Published on 6 min read

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 wget is 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:

ToolRoleTypical output
log2timeline.pyExtracts events from a source into a storage filetimeline.plaso (SQLite-based)
pinfo.pyReports what was processed, parser counts, warningstext summary
psort.pyFilters, sorts, deduplicates and exports eventsCSV, JSON lines, OpenSearch
psteal.pyRuns extraction and output in one stepCSV

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 / pluginArtifact
text/syslog, text/syslog_traditional/var/log/syslog, messages, auth.log, secure
systemd_journalbinary journal files under /var/log/journal
utmpwtmp, btmp, utmp login records
text/bash_history.bash_history with #epoch timestamp lines
text/zsh_extended_historyzsh history in extended format
text/dpkg, text/apt_history/var/log/dpkg.log, /var/log/apt/history.log
text/selinuxSELinux and auditd audit.log format
jsonl/docker_container_config, jsonl/docker_container_log, jsonl/docker_layer_configDocker container config, JSON log and layer files
filestatfile 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 UTC and state it in the report.
  • Local syslog time. A wrong --timezone shifts every syslog event by hours relative to the journal. Cross-check one event present in both.
  • Volume. A full filestat pass 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 + mactime for a quick file system view, Plaso for the full super timeline.
  • Set the source --timezone for syslog and always export in UTC.
  • Run pinfo.py to confirm which parsers produced events before trusting gaps.
  • Pivot from a confirmed anchor event with --slice and verify each row at its source.
  • Move large or shared timelines into Timesketch.