a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
275 lines
11 KiB
PowerShell
275 lines
11 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Windows LAPS Prometheus Metrics Exporter
|
|
.DESCRIPTION
|
|
Prometheus exporter for Windows Local Administrator Password Solution (LAPS).
|
|
Monitors password expiration, rotation status, coverage across OUs, and
|
|
stale passwords. Exports metrics for windows_exporter textfile collector
|
|
or standalone HTTP listener.
|
|
.PARAMETER TextFile
|
|
Write to windows_exporter textfile directory
|
|
.PARAMETER OutFile
|
|
Write metrics to a specific file path
|
|
.PARAMETER Install
|
|
Create a scheduled task for automatic collection
|
|
.PARAMETER Listen
|
|
Start HTTP listener on specified address:port
|
|
.PARAMETER Interval
|
|
Collection interval in seconds for scheduled task (default: 300)
|
|
.NOTES
|
|
Author: Phil Connor
|
|
Contact: contact@mylinux.work
|
|
Website: https://mylinux.work
|
|
License: MIT
|
|
Version: 1.0
|
|
#>
|
|
|
|
param(
|
|
[switch]$TextFile,
|
|
[string]$OutFile = "",
|
|
[switch]$Install,
|
|
[string]$Listen = "",
|
|
[int]$Interval = 300
|
|
)
|
|
|
|
$ErrorActionPreference = "SilentlyContinue"
|
|
$Version = "1.0"
|
|
$TextfileDir = "C:\ProgramData\node_exporter"
|
|
$MetricPrefix = "windows_laps"
|
|
|
|
# ============================================================================
|
|
# HELPER FUNCTIONS
|
|
# ============================================================================
|
|
|
|
function Get-PrometheusEscape {
|
|
param([string]$Value)
|
|
$Value -replace '\\', '\\' -replace '"', '\"' -replace "`n", ''
|
|
}
|
|
|
|
function Write-MetricHeader {
|
|
param([string]$Name, [string]$Help, [string]$Type)
|
|
"# HELP $Name $Help"
|
|
"# TYPE $Name $Type"
|
|
}
|
|
|
|
# ============================================================================
|
|
# METRIC GENERATION
|
|
# ============================================================================
|
|
|
|
function Get-LapsMetrics {
|
|
$startTime = Get-Date
|
|
$metrics = [System.Collections.ArrayList]::new()
|
|
|
|
# Check if LAPS module is available
|
|
$lapsAvailable = $false
|
|
if (Get-Module -ListAvailable -Name "LAPS" -ErrorAction SilentlyContinue) {
|
|
Import-Module LAPS -ErrorAction SilentlyContinue
|
|
$lapsAvailable = $true
|
|
}
|
|
# Also check for Windows LAPS (new in Server 2022+)
|
|
$windowsLaps = $false
|
|
if (Get-Command "Get-LapsAADPassword" -ErrorAction SilentlyContinue) {
|
|
$windowsLaps = $true
|
|
$lapsAvailable = $true
|
|
}
|
|
|
|
$up = if ($lapsAvailable) { 1 } else { 0 }
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_up" "LAPS exporter status (1=up, 0=down)" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_up $up")
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_info" "Exporter version information" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_exporter_info{version=`"$Version`"} 1")
|
|
[void]$metrics.Add("")
|
|
|
|
if ($up -eq 0) {
|
|
$endTime = Get-Date
|
|
$duration = [math]::Round(($endTime - $startTime).TotalSeconds, 2)
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_duration_seconds" "Script execution time" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_exporter_duration_seconds $duration")
|
|
return ($metrics -join "`n")
|
|
}
|
|
|
|
# ========================================================================
|
|
# COMPUTER ACCOUNT LAPS STATUS
|
|
# ========================================================================
|
|
|
|
try {
|
|
$computers = Get-ADComputer -Filter {Enabled -eq $true} -Properties `
|
|
"ms-Mcs-AdmPwdExpirationTime", "ms-Mcs-AdmPwd", "DistinguishedName", "OperatingSystem" `
|
|
-ErrorAction Stop
|
|
|
|
$totalComputers = @($computers).Count
|
|
$lapsManaged = 0
|
|
$lapsUnmanaged = 0
|
|
$passwordExpired = 0
|
|
$passwordExpiringSoon = 0
|
|
$now = Get-Date
|
|
$soonThreshold = $now.AddDays(7)
|
|
|
|
foreach ($computer in $computers) {
|
|
$expTime = $computer."ms-Mcs-AdmPwdExpirationTime"
|
|
|
|
if ($expTime -and $expTime -gt 0) {
|
|
$lapsManaged++
|
|
$expDate = [DateTime]::FromFileTime($expTime)
|
|
|
|
if ($expDate -lt $now) {
|
|
$passwordExpired++
|
|
} elseif ($expDate -lt $soonThreshold) {
|
|
$passwordExpiringSoon++
|
|
}
|
|
} else {
|
|
$lapsUnmanaged++
|
|
}
|
|
}
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_computers_total" "Total enabled computer accounts" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_computers_total $totalComputers")
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_computers_managed" "Computers with LAPS password set" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_computers_managed $lapsManaged")
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_computers_unmanaged" "Computers without LAPS password" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_computers_unmanaged $lapsUnmanaged")
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_passwords_expired" "Computers with expired LAPS passwords" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_passwords_expired $passwordExpired")
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_passwords_expiring_soon" "Computers with LAPS passwords expiring within 7 days" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_passwords_expiring_soon $passwordExpiringSoon")
|
|
[void]$metrics.Add("")
|
|
|
|
# Coverage percentage
|
|
$coverage = if ($totalComputers -gt 0) { [math]::Round(($lapsManaged / $totalComputers) * 100, 1) } else { 0 }
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_coverage_percent" "LAPS coverage percentage" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_coverage_percent $coverage")
|
|
[void]$metrics.Add("")
|
|
|
|
# ====================================================================
|
|
# PER-OU BREAKDOWN
|
|
# ====================================================================
|
|
|
|
$ouStats = @{}
|
|
foreach ($computer in $computers) {
|
|
$dn = $computer.DistinguishedName
|
|
$ouParts = ($dn -split ",") | Where-Object { $_ -match "^OU=" }
|
|
$ou = if ($ouParts.Count -gt 0) { ($ouParts[0] -replace "^OU=", "") } else { "Default" }
|
|
|
|
if (-not $ouStats.ContainsKey($ou)) {
|
|
$ouStats[$ou] = @{ Total = 0; Managed = 0 }
|
|
}
|
|
$ouStats[$ou].Total++
|
|
|
|
$expTime = $computer."ms-Mcs-AdmPwdExpirationTime"
|
|
if ($expTime -and $expTime -gt 0) {
|
|
$ouStats[$ou].Managed++
|
|
}
|
|
}
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_ou_computers_total" "Computers per OU" "gauge"))
|
|
foreach ($ou in $ouStats.Keys) {
|
|
$ouName = Get-PrometheusEscape $ou
|
|
[void]$metrics.Add("${MetricPrefix}_ou_computers_total{ou=`"$ouName`"} $($ouStats[$ou].Total)")
|
|
}
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_ou_computers_managed" "LAPS-managed computers per OU" "gauge"))
|
|
foreach ($ou in $ouStats.Keys) {
|
|
$ouName = Get-PrometheusEscape $ou
|
|
[void]$metrics.Add("${MetricPrefix}_ou_computers_managed{ou=`"$ouName`"} $($ouStats[$ou].Managed)")
|
|
}
|
|
[void]$metrics.Add("")
|
|
|
|
} catch {
|
|
[void]$metrics.Add("# ERROR: Failed to query AD computers: $_")
|
|
[void]$metrics.Add("")
|
|
}
|
|
|
|
# ========================================================================
|
|
# LAPS GPO STATUS
|
|
# ========================================================================
|
|
|
|
try {
|
|
$gpos = Get-GPO -All -ErrorAction Stop | Where-Object {
|
|
$_.DisplayName -match "LAPS|Local Administrator Password"
|
|
}
|
|
|
|
$lapsGpos = @($gpos).Count
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_gpo_count" "LAPS-related GPOs" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_gpo_count $lapsGpos")
|
|
[void]$metrics.Add("")
|
|
} catch {
|
|
# GPO query may not be available on all systems
|
|
}
|
|
|
|
# ========================================================================
|
|
# EXPORTER RUNTIME
|
|
# ========================================================================
|
|
|
|
$endTime = Get-Date
|
|
$duration = [math]::Round(($endTime - $startTime).TotalSeconds, 2)
|
|
$timestamp = [math]::Round((Get-Date -UFormat %s), 0)
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_duration_seconds" "Script execution time" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_exporter_duration_seconds $duration")
|
|
[void]$metrics.Add("")
|
|
|
|
[void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_last_run_timestamp" "Unix timestamp of last run" "gauge"))
|
|
[void]$metrics.Add("${MetricPrefix}_exporter_last_run_timestamp $timestamp")
|
|
|
|
return ($metrics -join "`n")
|
|
}
|
|
|
|
# ============================================================================
|
|
# OUTPUT MODES
|
|
# ============================================================================
|
|
|
|
if ($Install) {
|
|
$scriptPath = $MyInvocation.MyCommand.Path
|
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
|
|
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -TextFile"
|
|
$trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Seconds $Interval) -Once -At (Get-Date)
|
|
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
Register-ScheduledTask -TaskName "LAPS Metrics Exporter" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
|
Write-Host "Scheduled task 'LAPS Metrics Exporter' created (interval: ${Interval}s)"
|
|
exit 0
|
|
}
|
|
|
|
if ($Listen -ne "") {
|
|
$port = $Listen -replace '.*:', ''
|
|
if (-not $port) { $port = "9199" }
|
|
$listener = [System.Net.HttpListener]::new()
|
|
$listener.Prefixes.Add("http://+:$port/")
|
|
$listener.Start()
|
|
Write-Host "LAPS exporter listening on port $port..."
|
|
|
|
while ($listener.IsListening) {
|
|
$context = $listener.GetContext()
|
|
$response = $context.Response
|
|
$output = Get-LapsMetrics
|
|
$buffer = [System.Text.Encoding]::UTF8.GetBytes($output)
|
|
$response.ContentType = "text/plain; version=0.0.4"
|
|
$response.ContentLength64 = $buffer.Length
|
|
$response.OutputStream.Write($buffer, 0, $buffer.Length)
|
|
$response.Close()
|
|
}
|
|
} elseif ($TextFile -or $OutFile -ne "") {
|
|
$outputPath = if ($OutFile -ne "") { $OutFile } else { Join-Path $TextfileDir "laps.prom" }
|
|
$outputDir = Split-Path $outputPath -Parent
|
|
if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir -Force | Out-Null }
|
|
$tempFile = Join-Path $outputDir ".laps-metrics.tmp"
|
|
$metricsOutput = Get-LapsMetrics
|
|
$metricsOutput | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline
|
|
Move-Item -Path $tempFile -Destination $outputPath -Force
|
|
Write-Host "Metrics written to $outputPath"
|
|
} else {
|
|
Get-LapsMetrics
|
|
}
|