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.
693 lines
26 KiB
PowerShell
693 lines
26 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Monitors RDP user sessions and exports metrics for Prometheus windows_exporter.
|
|
|
|
.DESCRIPTION
|
|
This script monitors the number of active RDP user sessions and creates Prometheus-formatted metrics.
|
|
The metrics are written to a text file that can be consumed by the windows_exporter.
|
|
It can also run periodically.
|
|
|
|
.PARAMETER MetricsPath
|
|
The path where the Prometheus metrics file will be written.
|
|
|
|
.PARAMETER IntervalSeconds
|
|
The interval in seconds for the scheduled task. Default is 60 seconds.
|
|
|
|
.Parameter RunOnce
|
|
Switch to run the script once and exit instead of creating a scheduled task.
|
|
|
|
.PARAMETER Debug
|
|
Switch to run the script in debug mode.
|
|
|
|
.PARAMETER RunOnce
|
|
Switch to run the script once and exit instead of creating a scheduled task.
|
|
|
|
.PARAMETER DryRun
|
|
Switch to output metrics to console instead of writing to file.
|
|
|
|
.PARAMETER Verbose
|
|
Switch to enable verbose debug output.
|
|
|
|
.PARAMETER Quiet
|
|
Switch to suppress non-error output.
|
|
|
|
.PARAMETER NoSchedule
|
|
Switch to skip scheduled task creation.
|
|
|
|
.PARAMETER Version
|
|
Switch to display script version and exit.
|
|
|
|
.NOTES
|
|
Version: 1.1.2-20251002
|
|
Author: Phil Connor contact@mylinux.work
|
|
|
|
Features:
|
|
- Monitors active RDP user sessions using quser command
|
|
- Captures username, session name, session ID, state (Active/Disconnected), idle time, and logon time
|
|
- Attempts to correlate session IDs with client IP addresses using qwinsta
|
|
- Writes metrics to a text file for consumption by windows_exporter.
|
|
- Reads last 10 PowerShell commands from each user's PSReadline history file.
|
|
#>
|
|
|
|
param(
|
|
[ValidateScript({
|
|
if ($_ -and -not (Test-Path (Split-Path $_ -Parent))) {
|
|
throw "Directory for metrics path does not exist: $(Split-Path $_ -Parent)"
|
|
}
|
|
return $true
|
|
})]
|
|
[string]$MetricsPath = "C:\Program Files\windows_exporter\textfile_inputs\users_logged_in.prom",
|
|
[int]$IntervalSeconds = 60,
|
|
[switch]$RunOnce,
|
|
[switch]$Debug,
|
|
[switch]$DryRun,
|
|
[switch]$Verbose,
|
|
[switch]$Quiet,
|
|
[switch]$NoSchedule,
|
|
[switch]$Version
|
|
)
|
|
|
|
# Handle version display
|
|
if ($Version) {
|
|
Write-Host "Windows RDP User Monitor PowerShell Script"
|
|
Write-Host "Version: 1.1.0-20250915"
|
|
Write-Host "Author: Phil Connor contact@mylinux.work"
|
|
exit 0
|
|
}
|
|
|
|
# Set up logging preferences based on Verbose/Quiet flags
|
|
if ($Verbose) {
|
|
$VerbosePreference = 'Continue'
|
|
$InformationPreference = 'Continue'
|
|
}
|
|
if ($Quiet) {
|
|
$VerbosePreference = 'SilentlyContinue'
|
|
$InformationPreference = 'SilentlyContinue'
|
|
$WarningPreference = 'SilentlyContinue'
|
|
}
|
|
|
|
# Enhanced logging functions
|
|
function Write-InfoLog {
|
|
param([string]$Message)
|
|
if (-not $Quiet) {
|
|
Write-Host "[INFO] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Green
|
|
}
|
|
}
|
|
|
|
function Write-VerboseLog {
|
|
param([string]$Message)
|
|
if ($Verbose) {
|
|
Write-Host "[VERBOSE] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Cyan
|
|
}
|
|
}
|
|
|
|
# Configuration constants for the script
|
|
$script:Config = @{
|
|
RDP_SESSION_PATTERN = "rdp-tcp#\d+|console" # Regex pattern to match RDP session names
|
|
METRIC_NAME = "windows_rdp_users_logged_in" # Primary Prometheus metric name
|
|
QWINSTA_IP_REGEX = '^\s*(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+\.\d+\.\d+\.\d+)' # Pattern for IP extraction
|
|
QUSER_HEADER_REGEX = "USERNAME.*SESSIONNAME.*ID.*STATE" # Expected quser output header format
|
|
COLUMNS = @{ # Column positions in quser output
|
|
USERNAME = 0; SESSION = 1; ID = 2; STATE = 3; IDLE = 4; LOGON_START = 5
|
|
}
|
|
}
|
|
|
|
# Sanitize string values for use as Prometheus metric labels
|
|
# Removes or replaces characters that would break Prometheus metric format
|
|
function ConvertTo-MetricLabel {
|
|
param([AllowEmptyString()][string]$Value)
|
|
if ([string]::IsNullOrEmpty($Value)) { return "" }
|
|
|
|
# Replace problematic characters with underscores to prevent metric parsing issues
|
|
$sanitized = $Value -replace '["\\\n\r\t>]', '_'
|
|
|
|
# Limit length to prevent overly long metric labels (Prometheus best practice)
|
|
if ($sanitized.Length -gt 200) {
|
|
$sanitized = $sanitized.Substring(0, 200) + "..."
|
|
}
|
|
return $sanitized
|
|
}
|
|
|
|
# Format metric data into Prometheus text format
|
|
function Write-PrometheusMetric {
|
|
param(
|
|
[ValidateNotNullOrEmpty()][string]$Name,
|
|
[ValidateNotNullOrEmpty()][string]$Help,
|
|
[ValidateNotNullOrEmpty()][string]$Type,
|
|
[ValidateNotNull()][array]$Metrics
|
|
)
|
|
|
|
try {
|
|
@(
|
|
# Write Prometheus metric header with help text and type
|
|
"# HELP $Name $Help"
|
|
"# TYPE $Name $Type"
|
|
# Format each metric with its labels and value
|
|
$Metrics | ForEach-Object {
|
|
if ($null -eq $_ -or $null -eq $_.Labels -or $null -eq $_.Value) {
|
|
throw "Invalid metric data"
|
|
}
|
|
"$Name$($_.Labels) $($_.Value)"
|
|
}
|
|
)
|
|
}
|
|
catch {
|
|
Write-Error "Failed to write metric: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# Execute quser command and validate output format
|
|
# Returns raw quser command output after basic validation
|
|
function Get-QUserData {
|
|
try {
|
|
# Run quser command and suppress stderr to avoid noise
|
|
$output = quser 2>$null
|
|
|
|
# Validate that we got some output
|
|
if (-not $output -or $output.Count -eq 0) {
|
|
throw "No user sessions found or quser command failed"
|
|
}
|
|
|
|
# Ensure output has expected header format
|
|
if ($output.Count -lt 2 -or $output[0] -notmatch $script:Config.QUSER_HEADER_REGEX) {
|
|
throw "Unexpected quser output format"
|
|
}
|
|
return $output
|
|
}
|
|
catch [System.Management.Automation.CommandNotFoundException] {
|
|
throw "quser command not found. This script requires Windows with Terminal Services."
|
|
}
|
|
}
|
|
|
|
# Get IP addresses for RDP sessions using qwinsta command
|
|
# Attempts to correlate session IDs with client IP addresses for remote sessions
|
|
function Get-SessionIPAddresses {
|
|
try {
|
|
$sessionIPs = @{}
|
|
# Run qwinsta to get session information including IP addresses
|
|
$qwinstaOutput = qwinsta 2>$null
|
|
|
|
if ($qwinstaOutput) {
|
|
Write-Verbose "Raw qwinsta output:"
|
|
$qwinstaOutput | ForEach-Object { Write-Verbose " $_" }
|
|
|
|
foreach ($line in $qwinstaOutput) {
|
|
# Skip header lines and empty lines
|
|
if ([string]::IsNullOrWhiteSpace($line) -or $line -match '^\s*SESSIONNAME') {
|
|
continue
|
|
}
|
|
|
|
Write-Verbose "Processing qwinsta line: '$line'"
|
|
|
|
# Look for any IP address in the line and try to correlate with session ID
|
|
if ($line -match '(\d+\.\d+\.\d+\.\d+)') {
|
|
$ipAddress = $matches[1]
|
|
|
|
# Try different patterns to find session ID that corresponds to this IP
|
|
$sessionId = $null
|
|
|
|
# Pattern 1: Standard format with session ID as 3rd column
|
|
if ($line -match '^\s*(\S+)\s+(\S+)?\s+(\d+)\s+') {
|
|
$sessionId = $matches[3]
|
|
}
|
|
# Pattern 2: RDP session format
|
|
elseif ($line -match 'rdp-tcp#\d+.*?\s(\d+)\s+') {
|
|
$sessionId = $matches[1]
|
|
}
|
|
# Pattern 3: Any number that looks like a session ID (between spaces)
|
|
elseif ($line -match '\s(\d+)\s+\w+') {
|
|
$sessionId = $matches[1]
|
|
}
|
|
|
|
# Store the mapping if we found a valid session ID
|
|
if ($sessionId) {
|
|
$sessionIPs[$sessionId] = $ipAddress
|
|
Write-Verbose "Mapped session ID $sessionId to IP $ipAddress"
|
|
}
|
|
else {
|
|
Write-Verbose "Found IP $ipAddress but could not determine session ID"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Verbose "Final session IP mapping: $($sessionIPs | ConvertTo-Json -Compress)"
|
|
return $sessionIPs
|
|
}
|
|
catch {
|
|
# Don't fail the entire script if IP detection fails
|
|
Write-Warning "Failed to get session IP addresses: $($_.Exception.Message)"
|
|
return @{}
|
|
}
|
|
}
|
|
|
|
# Parses a single line of quser output into a structured object
|
|
# Converts space-separated quser output into a PowerShell object with named properties
|
|
function ConvertFrom-QUserLine {
|
|
param(
|
|
[ValidateNotNullOrEmpty()][string]$Line,
|
|
[hashtable]$SessionIPs = @{}
|
|
)
|
|
|
|
# Split the line into fields, normalizing whitespace
|
|
$fields = $Line.Trim() -Replace '\s+', ' ' -Split '\s'
|
|
|
|
# Validate minimum expected field count
|
|
if ($fields.Length -lt 6) { return $null }
|
|
|
|
$cols = $script:Config.COLUMNS
|
|
$sessionId = $fields[$cols.ID]
|
|
|
|
# Look up IP address for this session if available
|
|
$ipAddress = if ($SessionIPs.ContainsKey($sessionId)) { $SessionIPs[$sessionId] } else { "unknown" }
|
|
|
|
# Extract logon time from remaining fields (may span multiple columns)
|
|
$logonTime = if ($fields.Length -gt $cols.LOGON_START) {
|
|
$endIndex = if ($fields.Length -gt 6) { $fields.Length - 2 } else { $fields.Length - 1 }
|
|
$fields[$cols.LOGON_START..$endIndex] -join ' '
|
|
}
|
|
else { "Unknown" }
|
|
|
|
# Clean username by removing leading ">" character if present (indicates active session)
|
|
$cleanUserName = $fields[$cols.USERNAME] -replace '^>', ''
|
|
|
|
# Create structured object with all session information
|
|
return [PSCustomObject]@{
|
|
UserName = $cleanUserName
|
|
SessionName = $fields[$cols.SESSION]
|
|
ID = $sessionId
|
|
State = $fields[$cols.STATE]
|
|
IdleTime = $fields[$cols.IDLE]
|
|
LogonTime = $logonTime
|
|
ClientLocation = if ($fields.Length -gt 6) { $fields[-1] } else { "local" }
|
|
IPAddress = $ipAddress
|
|
}
|
|
}
|
|
|
|
# Get command history for a specific user session
|
|
# Retrieves recent PowerShell commands from the user's PSReadline history file
|
|
function Get-UserCommandHistory {
|
|
param(
|
|
[string]$UserName,
|
|
[string]$SessionId,
|
|
[int]$MaxCommands = 10
|
|
)
|
|
|
|
try {
|
|
# Sanitize username to remove invalid file path characters
|
|
$sanitizedUserName = $UserName -replace '[<>:"|?*]', '_'
|
|
|
|
# Try to get PowerShell history from the user's profile
|
|
$historyPath = "C:\Users\$sanitizedUserName\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt"
|
|
$commands = @()
|
|
|
|
# Check if PowerShell history file exists
|
|
if (Test-Path $historyPath) {
|
|
# Read the last N commands from the history file
|
|
$historyContent = Get-Content $historyPath -Tail $MaxCommands -ErrorAction SilentlyContinue
|
|
if ($historyContent) {
|
|
# Clean up the commands by trimming whitespace and removing empty lines
|
|
$commands = $historyContent | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
|
|
}
|
|
}
|
|
|
|
# If no PowerShell history, try to get CMD history using doskey
|
|
if ($commands.Count -eq 0) {
|
|
try {
|
|
# Use query session to check if user is active, then try to get command history
|
|
$sessionInfo = query session $SessionId 2>$null
|
|
if ($sessionInfo) {
|
|
# This is a simplified approach - in practice, CMD history is harder to access remotely
|
|
$commands = @("No recent command history available")
|
|
}
|
|
}
|
|
catch {
|
|
$commands = @("Unable to retrieve command history")
|
|
}
|
|
}
|
|
|
|
# Return the most recent commands up to the specified limit
|
|
return $commands | Select-Object -First $MaxCommands
|
|
}
|
|
catch {
|
|
Write-Verbose "Failed to get command history for user $UserName (Session $SessionId): $($_.Exception.Message)"
|
|
return @("Command history unavailable")
|
|
}
|
|
}
|
|
|
|
# Get all active RDP user sessions with detailed information
|
|
# Combines quser and qwinsta data to create comprehensive user session objects
|
|
function Get-RDPUsers {
|
|
try {
|
|
# Get raw user session data and IP address mappings
|
|
$qUserOutput = Get-QUserData
|
|
$sessionIPs = Get-SessionIPAddresses
|
|
Write-Verbose "Found $($qUserOutput.Count) total user sessions"
|
|
Write-Verbose "Found $($sessionIPs.Count) session IP addresses"
|
|
|
|
# Process each user session line (skip header line)
|
|
$allUsers = $qUserOutput | Select-Object -Skip 1 | ForEach-Object {
|
|
# Parse the quser output line into a structured object
|
|
$user = ConvertFrom-QUserLine $_ $sessionIPs
|
|
if ($null -eq $user) {
|
|
Write-Warning "Skipping malformed quser output: $_"
|
|
return
|
|
}
|
|
|
|
# Add command history to user object
|
|
$commandHistory = Get-UserCommandHistory -UserName $user.UserName -SessionId $user.ID
|
|
$user | Add-Member -NotePropertyName "CommandHistory" -NotePropertyValue $commandHistory
|
|
|
|
$user
|
|
} | Where-Object { $_ }
|
|
|
|
# Filter to only RDP sessions (excluding services and other non-user sessions)
|
|
$rdpUsers = $allUsers | Where-Object {
|
|
$_.SessionName -match $script:Config.RDP_SESSION_PATTERN -and
|
|
![string]::IsNullOrEmpty($_.UserName) -and
|
|
![string]::IsNullOrEmpty($_.SessionName) -and
|
|
![string]::IsNullOrEmpty($_.State)
|
|
}
|
|
|
|
Write-Verbose "Processed $($allUsers.Count) valid user sessions"
|
|
Write-Verbose "Filtered to $($rdpUsers.Count) RDP sessions"
|
|
|
|
return $rdpUsers
|
|
}
|
|
catch {
|
|
throw "Failed to collect user data: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# Creates Prometheus metrics from user session data
|
|
# Transforms user session objects into Prometheus-formatted metric data
|
|
function New-UserMetrics {
|
|
param([array]$Users)
|
|
|
|
if (-not $Users) { return @() }
|
|
|
|
# Initialize counters and collections for metric generation
|
|
$stateCount = @{ Active = 0; Disc = 0 }
|
|
$usernames = @()
|
|
$userMetrics = @()
|
|
$commandMetrics = @()
|
|
|
|
# Process each user to create individual metrics
|
|
foreach ($user in $Users) {
|
|
if ($null -eq $user) {
|
|
Write-Warning "Found null user in collection"
|
|
continue
|
|
}
|
|
|
|
# Track state counts for summary metrics
|
|
$stateCount[$user.State]++
|
|
$usernames += $user.UserName
|
|
|
|
# Create individual user session metric
|
|
$userMetrics += @{
|
|
Labels = "{username=`"$(ConvertTo-MetricLabel $user.UserName)`",session=`"$(ConvertTo-MetricLabel $user.SessionName)`",state=`"$($user.State)`",location=`"$(ConvertTo-MetricLabel $user.ClientLocation)`",ip=`"$(ConvertTo-MetricLabel $user.IPAddress)`"}"
|
|
Value = 1
|
|
}
|
|
|
|
# Add command history metrics for each user
|
|
if ($user.CommandHistory -and $user.CommandHistory.Count -gt 0) {
|
|
for ($i = 0; $i -lt $user.CommandHistory.Count; $i++) {
|
|
$command = ConvertTo-MetricLabel $user.CommandHistory[$i]
|
|
$commandMetrics += @{
|
|
Labels = "{username=`"$(ConvertTo-MetricLabel $user.UserName)`",session=`"$(ConvertTo-MetricLabel $user.SessionName)`",command_index=`"$($i + 1)`",command=`"$command`"}"
|
|
Value = 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Create summary metrics with totals and user list
|
|
$summaryMetrics = @(
|
|
@{ Labels = '{metric="total"}'; Value = $Users.Count }
|
|
@{ Labels = '{metric="active"}'; Value = $stateCount.Active }
|
|
@{ Labels = '{metric="disconnected"}'; Value = $stateCount.Disc }
|
|
@{ Labels = '{metric="users_list",users="' + $(ConvertTo-MetricLabel (($usernames | Sort-Object) -join ',')) + '"}'; Value = 1 }
|
|
)
|
|
|
|
# Combine all metric types into a single collection
|
|
return $summaryMetrics + $userMetrics + $commandMetrics
|
|
}
|
|
|
|
# Write metrics content to file using atomic write operation
|
|
function Write-MetricsFile {
|
|
param(
|
|
[ValidateNotNull()]$Content,
|
|
[string]$Path
|
|
)
|
|
|
|
if (-not $Path) {
|
|
return $Content
|
|
}
|
|
|
|
# Ensure the directory exists
|
|
$directory = Split-Path $Path -Parent
|
|
if ($directory -and -not (Test-Path $directory)) {
|
|
try {
|
|
New-Item -Path $directory -ItemType Directory -Force | Out-Null
|
|
Write-Verbose "Created directory: $directory"
|
|
}
|
|
catch {
|
|
Write-Error "Failed to create directory '$directory': $($_.Exception.Message)"
|
|
return
|
|
}
|
|
}
|
|
|
|
$tempPath = "$Path.tmp"
|
|
try {
|
|
if ($Content -is [array]) {
|
|
$Content -join "`n" | Out-File -FilePath $tempPath -Encoding UTF8
|
|
} else {
|
|
$Content | Out-File -FilePath $tempPath -Encoding UTF8
|
|
}
|
|
Move-Item -Path $tempPath -Destination $Path -Force -ErrorAction Stop
|
|
}
|
|
catch {
|
|
Write-Error "Failed to write metrics file: $($_.Exception.Message)"
|
|
if (Test-Path $tempPath) { Remove-Item $tempPath -Force }
|
|
}
|
|
}
|
|
|
|
# Main function that orchestrates the complete metrics collection process
|
|
# Coordinates all data collection, processing, and output generation
|
|
function Invoke-MetricsCollection {
|
|
$startTime = Get-Date
|
|
|
|
# Add dry-run header if applicable
|
|
if ($DryRun) {
|
|
Write-Host "=== DRY RUN MODE - Metrics that would be written to $MetricsPath ===" -ForegroundColor Yellow
|
|
}
|
|
|
|
try {
|
|
# Collect RDP user session data
|
|
Write-VerboseLog "Collecting RDP user session data..."
|
|
$rdpUsers = Get-RDPUsers
|
|
if ($null -eq $rdpUsers) {
|
|
throw "Get-RDPUsers returned null"
|
|
}
|
|
|
|
Write-VerboseLog "Found $($rdpUsers.Count) RDP users"
|
|
|
|
# Convert user data to Prometheus metrics
|
|
$metrics = New-UserMetrics -Users $rdpUsers
|
|
if ($null -eq $metrics) {
|
|
throw "New-UserMetrics returned null"
|
|
}
|
|
|
|
# Collect failed login attempts
|
|
Write-VerboseLog "Collecting failed login data..."
|
|
$failedLoginMetrics = Get-FailedLogins
|
|
|
|
# Calculate script execution time for performance monitoring
|
|
$endTime = Get-Date
|
|
$executionTimeMs = [math]::Round(($endTime - $startTime).TotalMilliseconds, 2)
|
|
|
|
# Add execution time metric for monitoring script performance
|
|
$executionMetric = @{
|
|
Labels = '{metric="execution_time_ms"}'
|
|
Value = $executionTimeMs
|
|
}
|
|
$metrics += $executionMetric
|
|
|
|
# Split metrics into different types
|
|
$userMetrics = $metrics | Where-Object { $_.Labels -notmatch 'command=' }
|
|
$commandMetrics = $metrics | Where-Object { $_.Labels -match 'command=' }
|
|
|
|
# Generate Prometheus-formatted output
|
|
$output = @()
|
|
$output += Write-PrometheusMetric -Name $script:Config.METRIC_NAME -Help "Number of RDP users currently logged in" -Type "gauge" -Metrics $userMetrics
|
|
|
|
# Add command history metrics as a separate metric family
|
|
if ($commandMetrics.Count -gt 0) {
|
|
$output += Write-PrometheusMetric -Name "windows_rdp_user_command_history" -Help "Recent command history for RDP users" -Type "gauge" -Metrics $commandMetrics
|
|
}
|
|
|
|
# Add failed login metrics
|
|
if ($failedLoginMetrics.Count -gt 0) {
|
|
$output += Write-PrometheusMetric -Name "windows_user_failed_logins" -Help "Failed login attempts from Windows Event Log" -Type "counter" -Metrics $failedLoginMetrics
|
|
}
|
|
|
|
if ($null -eq $output) {
|
|
throw "Write-PrometheusMetric returned null"
|
|
}
|
|
|
|
Write-VerboseLog "Metrics collection completed (execution time: ${executionTimeMs}ms)"
|
|
|
|
# Output to console and/or file based on mode
|
|
if ($DryRun) {
|
|
Write-Host $output
|
|
Write-Host "=== END DRY RUN OUTPUT ===" -ForegroundColor Yellow
|
|
} else {
|
|
Write-Output $output
|
|
Write-MetricsFile -Content $output -Path $MetricsPath
|
|
}
|
|
}
|
|
catch {
|
|
Write-Error "Failed to collect metrics: $($_.Exception.Message)"
|
|
# Attempt to write partial results if available
|
|
if ($MetricsPath -and $output -and -not $DryRun) {
|
|
$output | Out-File -FilePath $MetricsPath -Encoding UTF8
|
|
}
|
|
}
|
|
}
|
|
|
|
# Register cleanup handler for graceful shutdown
|
|
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
|
|
Write-Host "Shutting down gracefully..."
|
|
}
|
|
|
|
# Create scheduled task for periodic execution
|
|
function New-MetricsScheduledTask {
|
|
param(
|
|
[int]$IntervalSeconds = 60,
|
|
[string]$TaskName = "PrometheusRDPMetrics"
|
|
)
|
|
|
|
try {
|
|
# Check if scheduled task already exists
|
|
if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
|
|
Write-InfoLog "Scheduled task '$TaskName' already exists. Skipping creation."
|
|
return
|
|
}
|
|
|
|
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
|
|
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -MetricsPath `"$MetricsPath`" -RunOnce"
|
|
$trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Seconds $IntervalSeconds)
|
|
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
|
|
|
Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
|
Write-InfoLog "Scheduled task '$TaskName' created successfully with $IntervalSeconds second interval"
|
|
}
|
|
catch {
|
|
Write-Error "Failed to create scheduled task: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# Debug function to test qwinsta parsing
|
|
function Test-QwinstaOutput {
|
|
Write-Host "=== Testing qwinsta output parsing ===" -ForegroundColor Cyan
|
|
|
|
try {
|
|
$qwinstaOutput = qwinsta 2>$null
|
|
Write-Host "Raw qwinsta output:" -ForegroundColor Yellow
|
|
$qwinstaOutput | ForEach-Object { Write-Host " $_" }
|
|
|
|
Write-Host "`nTesting IP address extraction:" -ForegroundColor Yellow
|
|
$sessionIPs = Get-SessionIPAddresses
|
|
$sessionIPs.GetEnumerator() | ForEach-Object {
|
|
Write-Host " Session ID $($_.Key) -> IP $($_.Value)" -ForegroundColor Green
|
|
}
|
|
|
|
Write-Host "`nTesting quser output:" -ForegroundColor Yellow
|
|
$quserOutput = quser 2>$null
|
|
$quserOutput | ForEach-Object { Write-Host " $_" }
|
|
|
|
}
|
|
catch {
|
|
Write-Error "Test failed: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
|
|
# Get failed login attempts from Windows Event Log
|
|
function Get-FailedLogins {
|
|
try {
|
|
$failedLogins = @()
|
|
$24HoursAgo = (Get-Date).AddHours(-24)
|
|
|
|
# Query Windows Security Event Log for failed logon attempts (Event ID 4625)
|
|
$failedLogonEvents = Get-WinEvent -FilterHashtable @{
|
|
LogName = 'Security'
|
|
Id = 4625 # Failed logon attempts
|
|
StartTime = $24HoursAgo
|
|
} -ErrorAction SilentlyContinue | Select-Object -First 50
|
|
|
|
if ($failedLogonEvents) {
|
|
foreach ($event in $failedLogonEvents) {
|
|
try {
|
|
$eventXml = [xml]$event.ToXml()
|
|
$eventData = $eventXml.Event.EventData.Data
|
|
|
|
# Extract relevant information from event data
|
|
$targetUserName = ($eventData | Where-Object {$_.Name -eq 'TargetUserName'}).'#text'
|
|
$workstationName = ($eventData | Where-Object {$_.Name -eq 'WorkstationName'}).'#text'
|
|
$sourceNetworkAddress = ($eventData | Where-Object {$_.Name -eq 'IpAddress'}).'#text'
|
|
$failureReason = ($eventData | Where-Object {$_.Name -eq 'SubStatus'}).'#text'
|
|
|
|
# Clean up values
|
|
if ([string]::IsNullOrWhiteSpace($targetUserName)) { $targetUserName = "unknown" }
|
|
if ([string]::IsNullOrWhiteSpace($sourceNetworkAddress) -or $sourceNetworkAddress -eq '-') { $sourceNetworkAddress = "local" }
|
|
if ([string]::IsNullOrWhiteSpace($workstationName)) { $workstationName = "unknown" }
|
|
|
|
# Determine failure type based on sub status
|
|
$failureType = switch ($failureReason) {
|
|
"0xC0000064" { "invalid_user" }
|
|
"0xC000006A" { "wrong_password" }
|
|
"0xC0000234" { "account_locked" }
|
|
"0xC0000072" { "account_disabled" }
|
|
"0xC000006F" { "logon_time_restriction" }
|
|
"0xC0000070" { "workstation_restriction" }
|
|
default { "other_failure" }
|
|
}
|
|
|
|
$failedLogins += @{
|
|
Labels = "{username=`"$targetUserName`",source_ip=`"$sourceNetworkAddress`",workstation=`"$workstationName`",failure_type=`"$failureType`"}"
|
|
Value = 1
|
|
}
|
|
} catch {
|
|
Write-VerboseLog "Failed to parse event: $($_.Exception.Message)"
|
|
}
|
|
}
|
|
}
|
|
|
|
return $failedLogins
|
|
} catch {
|
|
Write-Warning "Failed to get failed login events: $($_.Exception.Message)"
|
|
return @()
|
|
}
|
|
}
|
|
|
|
# Main execution logic - determines script behavior based on parameters
|
|
if ($Debug) {
|
|
# Debug mode: test qwinsta and quser output parsing
|
|
Test-QwinstaOutput
|
|
}
|
|
elseif ($RunOnce -or $DryRun) {
|
|
# Single execution mode: collect metrics once and exit
|
|
Invoke-MetricsCollection
|
|
}
|
|
else {
|
|
# Scheduled mode: create scheduled task (unless NoSchedule) and run immediately
|
|
if (-not $NoSchedule) {
|
|
New-MetricsScheduledTask -IntervalSeconds $IntervalSeconds
|
|
} else {
|
|
Write-InfoLog "Skipping scheduled task creation (-NoSchedule specified)"
|
|
}
|
|
|
|
# Run metrics collection immediately
|
|
Invoke-MetricsCollection
|
|
}
|