Build an evidence timeline
Combine two export formats into an ordered, traceable timeline for one host and time window.
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.
Import-Csv '/home/user/auth.csv' | Sort-Object { [DateTimeOffset]::Parse($_.Time) } | Select-Object -First 3 RecordId, Time
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
$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.