Last updated on

Badass Intelligence Part 1: An API in PowerShell


Windows logs are rich-but they leave frustrating gaps. One of my favorite examples: tracking meaningful changes to Group Policy Objects (GPOs). Event ID 5136 (Directory Object Change) tells you that something changed, but not the friendly name of the GPO. You often just get a GUID like {6AC1786C-016F-11D2-945F-00C04FB984F9}. Great for a robot; useless for a human.

This series shows how to build a ridiculously fast, dependency‑light PowerShell API to enrich those events (GUID → Display Name + metadata), then pipe it into Elastic / Logstash.


The Problem

You see changes to Active Directory objects-especially GPOs-but the native event data omits the GPO display name. Without enrichment you can’t immediately answer:

  • Which GPO changed?
  • When was it created / last modified?
  • Is the change normal or suspicious?

Out-of-the-box suggestions (e.g. Get-GPO, Get-ADObject) are slow (1–2s per lookup) and require modules. At scale (hundreds or thousands of events), latency kills.

Goal

Translate a GUID found in security logs into human context (DisplayName, Created, LastModified) instantly-fast enough to sit inline in a Logstash HTTP enrichment pipeline.

Approach Overview

Leverage .NET type accelerators-especially [adsisearcher]-for raw LDAP speed without importing heavyweight modules.

Key idea:

  1. Build an LDAP filter that scopes to groupPolicyContainer objects.
  2. Query by name={GUID} (that attribute stores the raw GUID for GPOs).
  3. Extract properties directly: displayName, whenCreated, whenChanged.

Warm‑Up: ADSI Searching Yourself

Try on a domain‑joined box:

([adsisearcher]("sAMAccountName=$env:USERNAME")).FindOne().Properties

Wrap the filter in a variable for reuse:

$adsiQueryStr = "sAMAccountName=$env:USERNAME"
([adsisearcher]("$adsiQueryStr")).FindOne().Properties

Properties with {} are returned as collections-so handle array indexing if needed later.

Converting a GUID to a GPO Name

LDAP filter structure:

(&(objectCategory=groupPolicyContainer)(name={GUID_HERE}))

Example script (single lookup):

$Guid = '6AC1786C-016F-11D2-945F-00C04FB984F9'
$filter = "(&(objectCategory=groupPolicyContainer)(name={$Guid}))"
$obj = ([adsisearcher]$filter).FindOne().Properties

if ($obj) {
    $response = [ordered]@{
        DisplayName   = ($obj.displayname).Trim('{}')
        Created       = $obj.whencreated
        LastModified  = $obj.whenchanged
        Guid          = $Guid
    }
    $response | ConvertTo-Json -Compress
} else {
    Write-Warning "GPO GUID not found: $Guid"
}

Output (example):

{"DisplayName":"Default Domain Policy","Created":"2008-11-03T14:27:16","LastModified":"2020-04-26T15:04:13","Guid":"6AC1786C-016F-11D2-945F-00C04FB984F9"}

Why Not The Usual Cmdlets?

OptionDrawback
Get-GPORequires module + slower
Get-ADObjectRequires RSAT / AD module
VBScriptNo thanks
Raw ADSIFast, native, zero extra deps ✅

Towards an API

This lookup will become the primitive our API exposes (Part 2 builds the HTTP layer). For now, save a working snippet:

# snippet_GUIDtoName.ps1
param(
    [Parameter(Mandatory)][string]$Guid
)

$filter = "(&(objectCategory=groupPolicyContainer)(name={$Guid}))"
$obj = ([adsisearcher]$filter).FindOne().Properties

if ($obj) {
    [ordered]@{
        DisplayName  = ($obj.displayname).Trim('{}')
        Created      = $obj.whencreated
        LastModified = $obj.whenchanged
        Guid         = $Guid
    } | ConvertTo-Json -Compress
    exit 0
} else {
    [ordered]@{ Error = "NotFound"; Guid = $Guid } | ConvertTo-Json -Compress
    exit 1
}

Run:

./snippet_GUIDtoName.ps1 -Guid '6AC1786C-016F-11D2-945F-00C04FB984F9'

Next

In Part 2 we wrap this logic in a minimal PowerShell web framework to serve GUID lookups over HTTP.


Tags: elastic · PowerShell · Threat Hunting · Windows Events