Public/Watch-TripwireActivity.ps1

<#
.SYNOPSIS
    Collect every touch on a decoy in a time window and return scored trips.

.DESCRIPTION
    Two log sources, because the two interesting behaviours look different:

      Sign-in logs somebody is USING a decoy credential. The principal that signed
                          in is itself a decoy.
      Directory audits somebody READ, listed or modified a decoy object. Enumeration
                          precedes use, and this is where it shows up first.

    Records are normalised into touches, correlated per actor, and scored. Output is trips
    sorted by confidence -- including suppressed ones, which stay visible with their reason
    attached. A suppression rule that hides evidence is indistinguishable from a bug that
    loses it.

    -Demo runs the whole path against synthetic fixtures that ship inside the module: no
    tenant, no credentials, no network. It is the intended first five minutes with this tool.

.PARAMETER Demo
    Score the shipped fixtures instead of a tenant.

.PARAMETER Days
    How far back to read. Entra directory audit retention is typically 30 days.

.PARAMETER Decoy
    Decoy inventory. Defaults to the profile inventory (or the demo one under -Demo).

.PARAMETER Baseline
    Tenant baseline. Defaults to the profile baseline (or the demo one under -Demo).

.PARAMETER Policy
    Scoring policy. Defaults to Get-TripwirePolicy.

.EXAMPLE
    Watch-TripwireActivity -Demo | Format-Table Confidence, Band, DecoyName, ActorLabel, DistinctDecoys

.EXAMPLE
    Watch-TripwireActivity -Days 1 | Where-Object Band -eq 'High'
#>

function Watch-TripwireActivity {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [switch] $Demo,
        [int] $Days = 1,
        [object[]] $Decoy,
        [AllowNull()] $Baseline,
        $Policy
    )

    if (-not $Policy) { $Policy = Get-TripwirePolicy }
    if (-not $Decoy) { $Decoy = @(Get-TripwireDecoy -Demo:$Demo) }
    if (-not $PSBoundParameters.ContainsKey('Baseline')) { $Baseline = Get-TripwireBaseline -Demo:$Demo }

    if (@($Decoy).Count -eq 0) {
        Write-TripwireLog -Level 'Warn' -Message 'The decoy inventory is empty, so no touch can ever be detected. This is a blind run, not a quiet tenant. Plant decoys with New-TripwireDecoy.'
        return
    }

    $index = Get-TripwireDecoyIndex -Decoy $Decoy

    if ($Demo) {
        $signIns = @(Read-TripwireJsonFile -Path (Get-TripwireDemoPath -Name 'signins.json'))
        $audits = @(Read-TripwireJsonFile -Path (Get-TripwireDemoPath -Name 'directoryaudits.json'))
    }
    else {
        $since = [DateTime]::UtcNow.AddDays(-$Days).ToString('yyyy-MM-ddTHH:mm:ssZ')
        $signIns = @(Invoke-TripwireGraphRequest -Uri "/v1.0/auditLogs/signIns?`$top=999&`$filter=createdDateTime ge $since" -MaxPages 200)
        # Directory audits are fetched by time window and matched client-side. Graph's
        # $filter on targetResources is too limited to express "any of these N objects",
        # and building a server-side filter that silently drops a decoy would be worse
        # than reading a few more pages.
        $audits = @(Invoke-TripwireGraphRequest -Uri "/v1.0/auditLogs/directoryAudits?`$top=999&`$filter=activityDateTime ge $since" -MaxPages 200)
    }

    $touches = [System.Collections.Generic.List[object]]::new()
    foreach ($r in $signIns) {
        if ($null -eq $r) { continue }
        $t = ConvertTo-TripwireTouchFromSignIn -Record $r -DecoyIndex $index
        if ($null -ne $t) { $touches.Add($t) }
    }
    foreach ($r in $audits) {
        if ($null -eq $r) { continue }
        $t = ConvertTo-TripwireTouchFromAudit -Record $r -DecoyIndex $index
        if ($null -ne $t) { $touches.Add($t) }
    }

    Write-TripwireLog -Message "$($signIns.Count) sign-in and $($audits.Count) audit records read; $($touches.Count) touched a decoy."

    if ($touches.Count -eq 0) { return }

    Invoke-TripwireAnalysis -Touch $touches.ToArray() -Decoy $Decoy -Baseline $Baseline -Policy $Policy |
        Sort-Object -Property Confidence -Descending
}