Last updated on

Badass Intelligence Part 2: Building the PowerShell API


In Part 1 we proved we can resolve a GPO GUID to a friendly name instantly using [adsisearcher]. Now we turn that lookup into an HTTP API endpoint that Logstash (or any collector) can call inline for enrichment.


Requirements & Design

Constraints:

  • Very low latency (sub‑50ms typical for local queries)
  • No heavy external modules
  • JSON output
  • Resilient to missing GUIDs (returns structured error)

Endpoint Contract:

MethodPathQueryResponse (200)
GET/gpolookupguid=<GUID>{DisplayName, Created, LastModified, Guid}

404 / 400 style errors can be simplified to a 200 with an Error property to keep Logstash filter logic straightforward.

Minimal Framework Choice

We can use lightweight listeners instead of a full web server. Two common PowerShell approaches:

  • Start-Job + HttpListener
  • Polaris (if allowed-adds a dependency but still lean)

We’ll demonstrate a pure .NET HttpListener for zero dependencies.

Core Lookup Function

function Resolve-GpoGuid {
    [CmdletBinding()]
    param([Parameter(Mandatory)][string]$Guid)

    $filter = "(&(objectCategory=groupPolicyContainer)(name={$Guid}))"
    $obj = ([adsisearcher]$filter).FindOne().Properties
    if ($obj) {
        return [ordered]@{
            DisplayName  = ($obj.displayname).Trim('{}')
            Created      = $obj.whencreated
            LastModified = $obj.whenchanged
            Guid         = $Guid
        }
    }
    return [ordered]@{ Error = 'NotFound'; Guid = $Guid }
}

HTTP Listener Script

# gpo-api.ps1
param(
    [int]$Port = 8080
)

Add-Type -AssemblyName System.Net
$listener = [System.Net.HttpListener]::new()
$prefix = "http://*:${Port}/"
$listener.Prefixes.Add($prefix)
$listener.Start()
Write-Host "[+] GPO API listening on $prefix" -ForegroundColor Green

function Write-Json($context, $obj, [int]$status=200) {
    $json = ($obj | ConvertTo-Json -Compress)
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($json)
    $context.Response.StatusCode = $status
    $context.Response.ContentType = 'application/json'
    $context.Response.OutputStream.Write($bytes,0,$bytes.Length)
    $context.Response.Close()
}

while ($listener.IsListening) {
    $context = $listener.GetContext()
    $req = $context.Request
    try {
        if ($req.Url.AbsolutePath -eq '/gpolookup') {
            $guid = $req.QueryString['guid']
            if (-not $guid) { Write-Json $context ([ordered]@{ Error='MissingGuid'}) 200; continue }
            $data = Resolve-GpoGuid -Guid $guid
            Write-Json $context $data 200
        } else {
            Write-Json $context ([ordered]@{ Error='NotFound'; Path=$req.Url.AbsolutePath }) 404
        }
    } catch {
        Write-Json $context ([ordered]@{ Error='Exception'; Message=$_.Exception.Message }) 500
    }
}

Run:

./gpo-api.ps1 -Port 8080

Test:

Invoke-RestMethod "http://localhost:8080/gpolookup?guid=6AC1786C-016F-11D2-945F-00C04FB984F9"

Performance Notes

  • Cache layer (optional): store recent GUID → result in [System.Collections.Concurrent.ConcurrentDictionary].
  • Batch Mode (future): accept comma‑separated GUIDs for bulk enrichment.

Next

Part 3 wires this endpoint into Logstash for inline enrichment during ingestion.


Tags: elastic · PowerShell · Polaris · Threat Hunting · Windows Events