03 / INCIDENT TRIAGE WORKFLOWS · LESSON 12

Build an evidence timeline

Combine two export formats into an ordered, traceable timeline for one host and time window.

12 min · learn at your pace

One view, two sources

Authentication and process exports have different schemas. Create a small common projection before combining them: source label, record ID, computer, and timestamp. Keeping Source distinguishes records from different datasets even when their identifiers happen to overlap.

The task focuses on WS-01 during a specified UTC window. Apply the same host and time conditions to both sources. Use ConvertFrom-Json -DateKind String to preserve the process export's original timestamp strings until you explicitly parse them.

Sort instants, retain references

Sort-Object accepts a script block as a calculated sort key. Parse timestamps for chronological ordering instead of sorting the original text. Add source and record ID as tie-breakers so equal timestamps have a repeatable order.

Import-Csv '/home/user/auth.csv' |
    Sort-Object { [DateTimeOffset]::Parse($_.Time) } |
    Select-Object -First 3 RecordId, Time

A timeline shows temporal relationships, not causation. A successful logon after failures may deserve review, but the order alone does not prove that those events share an actor or purpose. Clock accuracy and gaps in collection also limit what the sequence can tell you.

This output keeps enough source identity to return to each original observation. The next module will package similar transformations into functions that can be tested on more than one dataset.

Try an example.ps1
Import-Csv '/home/user/auth.csv' | Sort-Object { [DateTimeOffset]::Parse($_.Time) } | Select-Object -First 3 RecordId, Time
YOUR TURN

Put it into practice.

Combine auth.csv and processes.json into a WS-01 timeline in [09:00,09:15) UTC on 2026-10-12. Emit Source (Auth or Process), RecordId, Computer, and UtcTime as a UTC ISO string. Sort chronologically, then Source and RecordId for ties. Include both successful and failed authentication events.

See an explained solution
One way to solve it.ps1
$start = [DateTimeOffset]::Parse('2026-10-12T09:00:00Z')
$end = [DateTimeOffset]::Parse('2026-10-12T09:15:00Z')
$events = @(
    Import-Csv '/home/user/auth.csv' | ForEach-Object { [pscustomobject]@{ Source='Auth'; RecordId=$_.RecordId; Computer=$_.Computer; Time=$_.Time } }
    Get-Content '/home/user/processes.json' -Raw | ConvertFrom-Json -DateKind String | ForEach-Object { [pscustomobject]@{ Source='Process'; RecordId=$_.RecordId; Computer=$_.Computer; Time=$_.Time } }
)
$events | Where-Object { $_.Computer -eq 'WS-01' -and [DateTimeOffset]::Parse($_.Time) -ge $start -and [DateTimeOffset]::Parse($_.Time) -lt $end } |
    Sort-Object { [DateTimeOffset]::Parse($_.Time) }, Source, RecordId |
    ForEach-Object { [pscustomobject]@{ Source=$_.Source; RecordId=$_.RecordId; Computer=$_.Computer; UtcTime=[DateTimeOffset]::Parse($_.Time).ToUniversalTime().ToString('o') } }

A common schema makes the sources composable. Parsing time before sorting places the +11:00 timestamp correctly, and source labels preserve where each observation came from.