Public/Measure-TripwireConfidence.ps1

<#
.SYNOPSIS
    Score one decoy touch and return a trip.

.DESCRIPTION
    The false-positive suppression engine. It answers one question -- how confident are we
    that this touch is an adversary rather than the tenant tripping over its own furniture
    -- and it answers it in a way a human can audit: every signal reports its raw strength,
    its weight, and why.

    Order of operations, and it matters:

      1. Hard suppression. Known-benign causes (the allowlist, provisioning self-noise)
         short-circuit to a zero-confidence 'Suppressed' trip. A suppressed touch is still
         RETURNED, with its reason, because a suppression rule that hides evidence is
         indistinguishable from a bug that loses it.
      2. Four weighted signals, each of which may declare itself unavailable.
      3. Confidence = sum of weighted signals x 100, banded against the policy thresholds.

    A trip whose signals were not all available is marked Degraded. Its confidence is a
    FLOOR: withheld signals contribute zero and the weights are never renormalised, so a
    degraded score can only be lower than the truth, never higher. Invoke-TripwireContainment
    refuses to act on a degraded trip unless explicitly told otherwise.

.PARAMETER Touch
    One normalised touch, as produced by Watch-TripwireActivity.

.PARAMETER Decoy
    The decoy record the touch landed on.

.PARAMETER Baseline
    The tenant baseline from Get-TripwireBaseline. Optional: without it the origin and
    off-hours signals are withheld and the trip is marked degraded.

.PARAMETER DistinctDecoyCount
    How many distinct decoys this actor touched inside the correlation window. Supplied by
    Invoke-TripwireAnalysis, which sees the whole set; defaults to 1 when scoring a single
    touch in isolation.

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

.EXAMPLE
    $trips = Watch-TripwireActivity -Demo
    $trips[0].Signals | Format-Table Name, Raw, Weight, Available, Rationale
#>

function Measure-TripwireConfidence {
    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Mandatory)] $Touch,
        [AllowNull()] $Decoy,
        [AllowNull()] $Baseline,
        [int] $DistinctDecoyCount = 1,
        $Policy
    )

    if (-not $Policy) { $Policy = Get-TripwirePolicy }

    $decoyName = if ($null -ne $Decoy) { [string]$Decoy.displayName } else { "unknown decoy ($($Touch.decoyId))" }
    $actorLabel = if (-not [string]::IsNullOrWhiteSpace([string]$Touch.actor.displayName)) {
        [string]$Touch.actor.displayName
    }
    elseif (-not [string]::IsNullOrWhiteSpace([string]$Touch.actor.upn)) { [string]$Touch.actor.upn }
    else { [string]$Touch.actor.id }

    $suppression = Test-TripwireSuppression -Touch $Touch -Decoy $Decoy -Baseline $Baseline -Policy $Policy
    if ($suppression) {
        return [pscustomobject]@{
            TouchId           = [string]$Touch.id
            DecoyId           = [string]$Touch.decoyId
            DecoyName         = $decoyName
            ActorLabel        = $actorLabel
            ActorKey          = Get-TripwireActorKey -Touch $Touch
            Touch             = $Touch
            Confidence        = 0.0
            Band              = 'Suppressed'
            Degraded          = $false
            Withheld          = @()
            Signals           = @()
            SuppressionReason = $suppression
            DistinctDecoys    = $DistinctDecoyCount
        }
    }

    $signals = @(
        Measure-TripwireDecoyPuritySignal -Decoy $Decoy -Policy $Policy
        Measure-TripwireOriginSignal -Touch $Touch -Baseline $Baseline -Policy $Policy
        Measure-TripwireFanOutSignal -DistinctDecoyCount $DistinctDecoyCount -Policy $Policy
        Measure-TripwireOffHoursSignal -Touch $Touch -Baseline $Baseline -Policy $Policy
    )

    $total = 0.0
    foreach ($s in $signals) { $total += $s.Weighted }
    $confidence = [Math]::Round($total * 100, 1)

    $withheld = @($signals | Where-Object { -not $_.Available } | ForEach-Object { $_.Name })

    $band = if ($confidence -ge $Policy.Threshold.High) { 'High' }
    elseif ($confidence -ge $Policy.Threshold.Medium) { 'Medium' }
    else { 'Low' }

    [pscustomobject]@{
        TouchId           = [string]$Touch.id
        DecoyId           = [string]$Touch.decoyId
        DecoyName         = $decoyName
        ActorLabel        = $actorLabel
        ActorKey          = Get-TripwireActorKey -Touch $Touch
        Touch             = $Touch
        Confidence        = $confidence
        Band              = $band
        Degraded          = ($withheld.Count -gt 0)
        Withheld          = $withheld
        Signals           = $signals
        SuppressionReason = $null
        DistinctDecoys    = $DistinctDecoyCount
    }
}