Public/Get-TripwireSummary.ps1
|
<# .SYNOPSIS Reduce a set of scored trips to one verdict and the next thing to do. .DESCRIPTION A list of trips is evidence. It is not an answer. This command answers the two questions an administrator actually has after a run -- is anything wrong, and what do I type next -- from the trips themselves, so the answer cannot drift from the data. Three rules it keeps: * It never upgrades the verdict. The bands come from the scoring policy and nothing here re-reads them. * It never calls a quiet result a clean one. Zero trips with an empty inventory is a blind run, and the next steps say so instead of congratulating anybody. * It reports withheld signals as withheld. A degraded trip's confidence is a floor, and the next steps name the usual cause -- no baseline -- separately from the unavoidable one: a directory audit record carries no origin at all. .PARAMETER Trip Scored trips from Watch-TripwireActivity or Invoke-TripwireAnalysis. An empty set is valid and produces the blind-run verdict. .PARAMETER Demo Shape the suggested commands for a demo run, so they can be pasted as printed. .EXAMPLE Watch-TripwireActivity -Demo | Get-TripwireSummary -Demo .EXAMPLE (Watch-TripwireActivity -Demo | Get-TripwireSummary).NextSteps #> function Get-TripwireSummary { [CmdletBinding()] [OutputType([pscustomobject])] param( [Parameter(ValueFromPipeline)] [AllowNull()] $Trip, [switch] $Demo ) begin { $collected = [System.Collections.Generic.List[object]]::new() } process { if ($null -ne $Trip) { $collected.Add($Trip) } } end { $trips = @($collected) $high = @($trips | Where-Object { $_.Band -eq 'High' }) $medium = @($trips | Where-Object { $_.Band -eq 'Medium' }) $low = @($trips | Where-Object { $_.Band -eq 'Low' }) $suppressed = @($trips | Where-Object { $_.Band -eq 'Suppressed' }) $degraded = @($trips | Where-Object { $_.Degraded }) $eligible = @($high | Where-Object { -not $_.Degraded }) # Degraded splits in two, and conflating them would send the operator to build a # baseline that cannot fix the second kind. A sign-in record carries an origin; a # directory audit record does not, so an audit-sourced touch is degraded however # good the baseline is. $degradedAudit = @($degraded | Where-Object { $_.Touch.source -ne 'SignInLog' }).Count $degradedSignIn = $degraded.Count - $degradedAudit $suffix = if ($Demo) { ' -Demo' } else { '' } $watch = "Watch-TripwireActivity$suffix" $top = 0.0 if ($trips.Count -gt 0) { $top = @($trips | Sort-Object -Property Confidence -Descending)[0].Confidence } $verdict = if ($trips.Count -eq 0) { 'No touch on a decoy was recorded. That is good news only if decoys are planted and the window covered them -- otherwise it is a blind run reading like a quiet tenant.' } elseif ($high.Count -gt 0) { "$($high.Count) trip(s) reached the High band, the only band containment is eligible in. Review them before anything else." } elseif ($medium.Count -gt 0) { "Nothing reached the High band. $($medium.Count) trip(s) are Medium: evidence worth a human look, never an automatic action." } elseif ($low.Count -gt 0) { "$($low.Count) trip(s) scored Low. Decoys were touched, but not in a pattern this engine calls an intrusion." } else { "Every one of the $($suppressed.Count) touch(es) in this run was suppressed by a known-benign rule. Each is listed with its reason rather than hidden." } $steps = [System.Collections.Generic.List[object]]::new() function Add-Step { param([Parameter(Mandatory)] [string] $Action, [string] $Command = '') $steps.Add([pscustomobject]@{ PSTypeName = 'EntraTripwire.NextStep' Step = $steps.Count + 1 Action = $Action Command = $Command }) } if ($trips.Count -eq 0) { Add-Step -Action 'Confirm decoys are planted. An empty inventory detects nothing and says nothing.' -Command 'Get-TripwireDecoy' Add-Step -Action 'Widen the window. Entra keeps directory audits for about 30 days.' -Command "$watch -Days 7" if (-not $Demo) { Add-Step -Action 'See the whole path end to end against the shipped fixtures, with no tenant.' -Command 'Start-EntraTripwire -Demo' } } else { if ($high.Count -gt 0) { Add-Step -Action 'Read the signals behind the highest-scoring trip. Every one carries its raw strength, its weight and a rationale.' ` -Command "($watch | Sort-Object Confidence -Descending)[0].Signals | Format-Table Name, Raw, Weight, Available, Rationale" Add-Step -Action 'Plan containment. The default is a dry run: it returns the exact Graph call for every action and touches nothing.' ` -Command "$watch | Where-Object Band -eq 'High' | Invoke-TripwireContainment" } if ($medium.Count -gt 0 -and $high.Count -eq 0) { Add-Step -Action 'Look at the Medium trips by hand. Containment will not act on them, by design.' ` -Command "$watch | Where-Object Band -eq 'Medium' | Format-List DecoyName, ActorLabel, Confidence, Withheld" } if ($degradedSignIn -gt 0 -and -not $Demo) { Add-Step -Action "$degradedSignIn sign-in trip(s) were scored with a signal withheld, so their confidence is a floor. A baseline is what the origin and off-hours signals compare against." ` -Command 'Get-TripwireBaseline -Build -Days 30 -Save' } if ($degradedAudit -gt 0) { Add-Step -Action "$degradedAudit trip(s) came from the directory audit log, which carries no origin at all. They stay degraded whatever the baseline says; weigh them on purity and fan-out." } if ($suppressed.Count -gt 0) { Add-Step -Action "$($suppressed.Count) touch(es) were suppressed. If one of them should have alerted, the allowlist is the first place to look." ` -Command "(Get-TripwireBaseline$suffix).suppressedActors" } Add-Step -Action 'Keep the evidence. The HTML report is one self-contained file that opens with no network.' ` -Command "$watch | Export-TripwireReport -Path ./tripwire-report.html$suffix" } [pscustomobject]@{ PSTypeName = 'EntraTripwire.Summary' Total = $trips.Count High = $high.Count Medium = $medium.Count Low = $low.Count Suppressed = $suppressed.Count Degraded = $degraded.Count ContainmentEligible = $eligible.Count TopConfidence = $top Verdict = $verdict NextSteps = $steps.ToArray() } } } # Eight properties, so PowerShell renders this as a list rather than a table -- which is # what a verdict wants. NextSteps is left off the default set because it is a collection: # it prints on its own, or through Start-EntraTripwire, which lays it out. Update-TypeData -TypeName 'EntraTripwire.Summary' ` -DefaultDisplayPropertySet 'Total', 'High', 'Medium', 'Low', 'Suppressed', 'Degraded', 'ContainmentEligible', 'Verdict' -Force Update-TypeData -TypeName 'EntraTripwire.NextStep' -DefaultDisplayPropertySet 'Step', 'Action', 'Command' -Force |