04 / DEFENSIVE AUTOMATION AND REPORTING · LESSON 13

Turn a rule into a reusable detector

Separate input data and thresholds from detection logic so the same function works on new cases.

15 min · learn at your pace

Make the rule portable

A useful detection function should accept data, not secretly read one particular case file. Give Find-FailedLogonBurst an Events parameter and an inclusive Threshold. The caller owns import, time-window selection, and output location; the function owns the grouping rule.

Keep the behaviour from the earlier lesson: count only 4625 records, group on computer plus account plus source address, and exclude missing or - addresses. The complete input collection is the observation window for this function.

Return findings as objects

A function writes objects to the success pipeline. Avoid status strings such as “Checking events…” on that same stream; callers should receive only findings. Use Write-Verbose for optional diagnostics in a more developed tool.

function Get-EventCount {
    param([object[]]$Events)
    @($Events).Count
}
Get-EventCount -Events (Import-Csv '/home/user/auth.csv')

Use @(...) at the call site when you need a predictable collection for zero, one, or many results. No qualifying group should yield no findings, not a fabricated zero-count row.

The automatic checks call your function with a second dataset, exact-threshold cases, and empty input. This verifies the rule beyond the example output. A short pipeline and an explicit loop are equally acceptable if they preserve the contract.

Try an example.ps1
Import-Csv '/home/user/auth.csv' | Where-Object EventId -EQ 4625 | Measure-Object
YOUR TURN

Put it into practice.

Define Find-FailedLogonBurst -Events <objects> -Threshold <integer> (default 3). Return Computer, TargetUser, SourceIp, FailureCount for groups with at least Threshold failed logons (4625). Group on all three identity fields and exclude empty/“-” addresses. Accept empty input and arbitrary supplied events. You may call it on auth.csv to try it.

See an explained solution
One way to solve it.ps1
function Find-FailedLogonBurst {
    param([object[]]$Events, [ValidateRange(1,10000)][int]$Threshold = 3)
    $Events | Where-Object { $_.EventId -eq 4625 -and $_.SourceIp -and $_.SourceIp -ne '-' } |
        Group-Object Computer, TargetUser, SourceIp |
        Where-Object Count -GE $Threshold | ForEach-Object {
            $first = $_.Group[0]
            [pscustomobject]@{ Computer=$first.Computer; TargetUser=$first.TargetUser; SourceIp=$first.SourceIp; FailureCount=$_.Count }
        }
}
Find-FailedLogonBurst -Events (Import-Csv '/home/user/auth.csv') -Threshold 3

The function accepts its evidence and threshold from the caller, returns only findings, and handles an empty result naturally. Tests cover the exact threshold so an off-by-one rule does not slip through.