EventMonitor/TaskManagement.ps1

# ── Task Management & Event Collection Orchestration ──────────────────────────
# Public functions for managing the Windows Scheduled Task lifecycle and the
# main orchestrator that coordinates event collection for all users.

# ── Event Collection Orchestrator ─────────────────────────────────────────────

<#
.SYNOPSIS
    Collects logon/logoff events and active session data for a single user.
.DESCRIPTION
    Orchestrates the full event collection pipeline for one user:
    1. Reads logon-indicator events (Connect)
    2. Reads logoff-indicator events (Disconnect)
    3. Checks for active quser sessions
    Results are forwarded to all registered telemetry sinks and logged locally.
.PARAMETER sessionId
    Correlation identifier for this monitoring session.
.PARAMETER timeRangeForEventsBefore
    Only process events created after this timestamp.
.PARAMETER user
    The Windows username to collect events for.
.EXAMPLE
    Get-WindowsEventsAndSessions -sessionId $sid -timeRangeForEventsBefore (Get-Date).AddMinutes(-5) -user 'jdoe'
#>

function Get-WindowsEventsAndSessions {
    [CmdletBinding()]
    param(
        [string]$sessionId,

        [Parameter(Mandatory)]
        [DateTime]$timeRangeForEventsBefore,

        [Parameter(Mandatory)]
        [string]$user
    )

    try {
        $commonParams = @{
            sessionId             = $sessionId
            StartTime             = $timeRangeForEventsBefore
            User                  = $user
        }

        $enabled = $script:MonitoringConfig.EnabledGroups

        # Per-user event processors — only run enabled groups
        if ('Logon'          -in $enabled) { Get-LogonEvents        @commonParams }
        if ('Logoff'         -in $enabled) { Get-LogoffEvents       @commonParams }
        if ('SSH'            -in $enabled) { Get-SSHEvents          @commonParams }
        if ('PrivilegeUse'   -in $enabled) { Get-PrivilegeEvents    @commonParams }
        if ('ProcessTracking'-in $enabled) { Get-ProcessEvents      @commonParams }
        if ('NetworkShare'   -in $enabled) { Get-NetworkShareEvents @commonParams }

        $hasActiveSession = Get-ActiveUsersByQUsers -sessionId $sessionId `
            -UserName $user

        Write-EMLog -Message "User '$user' has active session: $hasActiveSession"
    }
    catch {
        Write-EMLog -Message "Get-WindowsEventsAndSessions failed for '$user': $($_.Exception.Message)" -Level Error
        $errorProps = [System.Collections.Generic.Dictionary[string, string]]::new()
        $errorProps['SessionId'] = $sessionId
        $errorProps['Function']  = 'Get-WindowsEventsAndSessions'
        $errorProps['User']      = $user
        TrackException -ErrorRecord $_ -Properties $errorProps
    }
}

# ── Scheduled Task Registration ───────────────────────────────────────────────

<#
.SYNOPSIS
    Creates, registers, and starts a Windows Scheduled Task for event-driven monitoring.
.DESCRIPTION
    Registers a scheduled task that runs Start-EventMonitorService.ps1 under the SYSTEM
    account as a long-running, event-driven process. The task is configured to:
    - Start at boot (AtStartup trigger)
    - Auto-restart on failure (RestartInterval + RestartCount)
    - Never stop on idle
    - Run whether user is logged on or not
 
    The service uses EventLogWatcher for instant event detection (zero polling)
    and a watchdog timer for self-healing and telemetry flushing.
 
    Compatible with Windows 10, Windows 11, and Windows Server 2016+.
.PARAMETER logAnalyticsConString
    Application Insights connection string. Stored securely in a file for the service to read.
    Only used if AppInsights is your telemetry sink.
.PARAMETER sessionId
    Unique identifier for this monitoring session. Defaults to a new GUID.
.PARAMETER watchdogIntervalMin
    How often (in minutes) the watchdog checks health and flushes telemetry. Default: 30.
.PARAMETER scheduledTaskName
    Display name for the scheduled task. Default: 'WinEventMonitor'.
.EXAMPLE
    Register-EventMonitor
    # Local-only mode — events saved to journal, no cloud telemetry
.EXAMPLE
    Register-EventMonitor -logAnalyticsConString 'InstrumentationKey=...'
    # Send events to App Insights + local journal
.EXAMPLE
    Register-EventMonitor -scheduledTaskName 'MyMonitor' -watchdogIntervalMin 15
#>

function Register-EventMonitor {
    [CmdletBinding()]
    param(
        [ValidateNotNullOrEmpty()]
        [string]$logAnalyticsConString,

        [string]$sessionId = [guid]::NewGuid().Guid,

        [ValidateRange(5, 1440)]
        [int]$watchdogIntervalMin = 30,

        [ValidateNotNullOrEmpty()]
        [string]$scheduledTaskName = 'WinEventMonitor'
    )

    Write-EMLog -Message "Registering scheduled task '$scheduledTaskName' (event-driven mode, session: $sessionId)"

    # Check if task already exists — stop and remove it first
    $existingTask = Get-ScheduledTask -TaskName $scheduledTaskName -ErrorAction SilentlyContinue
    if ($existingTask) {
        Write-EMLog -Message "Task '$scheduledTaskName' already exists — updating." -Level Warning
        try {
            if ($existingTask.State -ne 'Disabled') {
                Stop-ScheduledTask -TaskName $scheduledTaskName -ErrorAction SilentlyContinue
            }
            Unregister-ScheduledTask -TaskName $scheduledTaskName -Confirm:$false
        }
        catch {
            Write-EMLog -Message "Failed to remove existing task: $($_.Exception.Message)" -Level Error
        }
    }

    $taskScriptPath = Join-Path $PSScriptRoot 'Start-EventMonitorService.ps1'
    if (-not (Test-Path $taskScriptPath)) {
        throw "Entry script not found at '$taskScriptPath'. Module installation may be corrupt."
    }

    # Store connection string if provided (for AppInsights sink)
    if ($logAnalyticsConString) {
        $conStringPath = Join-Path $script:SecretsDir 'ConnectionString.txt'
        Set-Content -Path $conStringPath -Value $logAnalyticsConString -Force

        # Restrict ACL to SYSTEM + Administrators only
        try {
            $acl = Get-Acl -Path $conStringPath
            $acl.SetAccessRuleProtection($true, $false)
            $acl.Access | ForEach-Object { $acl.RemoveAccessRule($_) } | Out-Null
            $systemRule = [System.Security.AccessControl.FileSystemAccessRule]::new(
                'NT AUTHORITY\SYSTEM', 'FullControl', 'Allow')
            $adminRule = [System.Security.AccessControl.FileSystemAccessRule]::new(
                'BUILTIN\Administrators', 'FullControl', 'Allow')
            $acl.AddAccessRule($systemRule)
            $acl.AddAccessRule($adminRule)
            Set-Acl -Path $conStringPath -AclObject $acl
        }
        catch {
            Write-EMLog -Message "Could not restrict ACL on connection string file: $($_.Exception.Message)" -Level Warning
        }

        Write-EMLog -Message "Connection string stored securely at: $conStringPath" -Level Warning
    }

    try {
        # Build the task action — long-running service, only non-secret params on command line
        $taskArgument = "-NoProfile -File `"$taskScriptPath`" " +
            "-sessionId `"$sessionId`" " +
            "-watchdogIntervalMin $watchdogIntervalMin"

        $taskAction = New-ScheduledTaskAction -Execute 'pwsh.exe' -Argument $taskArgument

        $taskPrincipal = New-ScheduledTaskPrincipal `
            -UserId 'NT AUTHORITY\SYSTEM' `
            -RunLevel Highest `
            -LogonType ServiceAccount

        # Settings optimized for a long-running event-driven service:
        # - RestartInterval: auto-restart after 1 minute if the process crashes
        # - RestartCount: up to 3 restarts before giving up
        # - ExecutionTimeLimit: no time limit (runs indefinitely)
        # - StopIfGoingOnBatteries/DisallowStartIfOnBatteries: false (always run)
        # - DontStopOnIdleEnd: true (never stop on idle)
        # - MultipleInstances: IgnoreNew (prevent duplicate instances)
        $taskSettings = New-ScheduledTaskSettingsSet `
            -Priority 4 `
            -RestartInterval (New-TimeSpan -Minutes 1) `
            -RestartCount 3 `
            -ExecutionTimeLimit (New-TimeSpan -Days 0) `
            -DontStopOnIdleEnd `
            -AllowStartIfOnBatteries `
            -DontStopIfGoingOnBatteries `
            -MultipleInstances IgnoreNew

        # Single trigger: start at system boot
        $startupTrigger = New-ScheduledTaskTrigger -AtStartup

        $task = Register-ScheduledTask $scheduledTaskName `
            -Action $taskAction `
            -Principal $taskPrincipal `
            -Settings $taskSettings `
            -Trigger $startupTrigger

        $task | Start-ScheduledTask
        Write-EMLog -Message "Scheduled task '$scheduledTaskName' registered and started (event-driven mode)."
    }
    catch {
        Write-EMLog -Message "Failed to register task '$scheduledTaskName': $($_.Exception.Message)" -Level Error
        throw "Failed to register event monitor task: $_"
    }
}

# ── Scheduled Task Lifecycle Functions ────────────────────────────────────────

<#
.SYNOPSIS
    Removes the event monitor scheduled task.
#>

function Unregister-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor'
    )

    try {
        Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
        Write-EMLog -Message "Unregistered event monitor task '$TaskName'."
    }
    catch {
        Write-EMLog -Message "Failed to unregister task '$TaskName': $($_.Exception.Message)" -Level Error
        throw
    }
}

<#
.SYNOPSIS
    Stops monitoring and removes the scheduled task. Keeps all data (logs, journal, config).
.DESCRIPTION
    Stops the event monitor service, removes the scheduled task, but preserves all
    captured data in C:\ProgramData\WindowsEventMonitor\. Use this when you want to
    stop monitoring but keep your event history.
 
    To also delete all data, use: Uninstall-EventMonitor -DeleteData
    To re-deploy later: Register-EventMonitor
.PARAMETER TaskName
    The scheduled task name. Default: 'WinEventMonitor'.
.PARAMETER DeleteData
    Also delete all data (logs, journal, config, secrets) from ProgramData.
    WARNING: This cannot be undone.
.EXAMPLE
    Uninstall-EventMonitor
    # Stops service, keeps data
.EXAMPLE
    Uninstall-EventMonitor -DeleteData
    # Stops service AND deletes all data
#>

function Uninstall-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor',

        [switch]$DeleteData
    )

    # Stop and remove the scheduled task
    try {
        $task = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
        if ($task) {
            if ($task.State -ne 'Disabled') {
                Stop-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
            }
            Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
            Write-EMLog -Message "Scheduled task '$TaskName' removed." -Level Warning
        }
        else {
            Write-EMLog -Message "Scheduled task '$TaskName' not found — already removed." -Level Warning
        }
    }
    catch {
        Write-EMLog -Message "Failed to remove task: $($_.Exception.Message)" -Level Error
    }

    if ($DeleteData) {
        $dataRoot = Join-Path $env:ProgramData 'WindowsEventMonitor'
        if (Test-Path $dataRoot) {
            Remove-Item -Path $dataRoot -Recurse -Force -ErrorAction Stop
            Write-Warning "All data deleted from $dataRoot"
        }
    }
    else {
        Write-Host ""
        Write-Host " Monitoring stopped. Data preserved at:" -ForegroundColor Green
        Write-Host " $script:DataRoot" -ForegroundColor Gray
        Write-Host ""
        Write-Host " To re-deploy: Register-EventMonitor" -ForegroundColor Gray
        Write-Host " To delete data: Uninstall-EventMonitor -DeleteData" -ForegroundColor Gray
        Write-Host ""
    }
}

<#
.SYNOPSIS
    Starts a previously registered event monitor scheduled task.
#>

function Start-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor'
    )

    try {
        Start-ScheduledTask -TaskName $TaskName
        Write-EMLog -Message "Started event monitor task '$TaskName'."
    }
    catch {
        Write-EMLog -Message "Failed to start task '$TaskName': $($_.Exception.Message)" -Level Error
        throw
    }
}

<#
.SYNOPSIS
    Stops a running event monitor scheduled task.
#>

function Stop-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor'
    )

    try {
        Stop-ScheduledTask -TaskName $TaskName
        Write-EMLog -Message "Stopped event monitor task '$TaskName'."
    }
    catch {
        Write-EMLog -Message "Failed to stop task '$TaskName': $($_.Exception.Message)" -Level Error
        throw
    }
}

<#
.SYNOPSIS
    Gets the status of the event monitor scheduled task.
#>

function Get-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor'
    )

    try {
        $task = Get-ScheduledTask -TaskName $TaskName
        Write-EMLog -Message "Retrieved status for task '$TaskName'."
        return $task
    }
    catch {
        Write-EMLog -Message "Failed to get task '$TaskName': $($_.Exception.Message)" -Level Error
        throw
    }
}

<#
.SYNOPSIS
    Disables the event monitor scheduled task without removing it.
#>

function Disable-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor'
    )

    try {
        Disable-ScheduledTask -TaskName $TaskName
        Write-EMLog -Message "Disabled event monitor task '$TaskName'."
    }
    catch {
        Write-EMLog -Message "Failed to disable task '$TaskName': $($_.Exception.Message)" -Level Error
        throw
    }
}

<#
.SYNOPSIS
    Re-enables a previously disabled event monitor scheduled task.
#>

function Enable-EventMonitor {
    [CmdletBinding()]
    param(
        [string]$TaskName = 'WinEventMonitor'
    )

    try {
        Enable-ScheduledTask -TaskName $TaskName
        Write-EMLog -Message "Enabled event monitor task '$TaskName'."
    }
    catch {
        Write-EMLog -Message "Failed to enable task '$TaskName': $($_.Exception.Message)" -Level Error
        throw
    }
}

# ── Diagnostic Scan Function ─────────────────────────────────────────────────

<#
.SYNOPSIS
    Runs a one-shot diagnostic scan of recent Windows security events.
.DESCRIPTION
    Scans the specified time window for all monitored events and dispatches
    them to registered telemetry sinks. Useful for:
    - Testing that the module works before deploying the service
    - Manually scanning a specific time range
    - Debugging and troubleshooting
 
    For continuous monitoring, use Register-EventMonitor to deploy the
    event-driven service.
.PARAMETER LookBackMinutes
    How far back (in minutes) to read events. Default: 60. Max: 10080 (7 days).
.PARAMETER SessionId
    Correlation identifier for this scan. Defaults to a new GUID.
.EXAMPLE
    Invoke-EventMonitor
.EXAMPLE
    Invoke-EventMonitor -LookBackMinutes 30
.EXAMPLE
    Invoke-EventMonitor -LookBackMinutes 1440 # scan last 24 hours
#>

function Invoke-EventMonitor {
    [CmdletBinding()]
    param(
        [ValidateRange(1, 10080)]
        [int]$LookBackMinutes = 60,

        [string]$SessionId = [guid]::NewGuid().Guid
    )

    Write-EMLog -Message "=== Diagnostic scan started (session: $SessionId, lookback: ${LookBackMinutes}min) ===" -Level Warning
    Write-Verbose "Diagnostic scan: session=$SessionId, lookback=${LookBackMinutes}min"

    # Auto-register App Insights sink if not already registered
    if (-not ($script:TelemetrySinks.Contains('AppInsights'))) {
        $cs = Resolve-AppInsightsConnectionString
        if ($cs) {
            try {
                Register-AppInsightsSink -ConnectionString $cs
                Write-EMLog -Message "AppInsights sink registered ($($cs.Length) char connection string)" -Level Warning
            }
            catch {
                Write-EMLog -Message "Failed to register AppInsights sink: $($_.Exception.Message)" -Level Error
            }
        }
        else {
            Write-EMLog -Message 'No App Insights connection string found. Set EventMonitorAppInsightsConString env var or use Register-TelemetrySink for custom sinks.' -Level Warning
        }
    }
    else {
        Write-EMLog -Message 'AppInsights sink already registered.' -Level Warning
    }

    $startTime = (Get-Date).AddMinutes(-$LookBackMinutes)
    Write-EMLog -Message "Reading events from $startTime onwards." -Level Warning

    # Per-user event collection
    try {
        $windowsUsers = Get-WindowsUsers -sessionId $SessionId
        if ($null -eq $windowsUsers -or $windowsUsers.Count -eq 0) {
            Write-EMLog -Message 'No user profiles found. Skipping per-user event collection.' -Level Warning
        }
        else {
            Write-EMLog -Message "Found $($windowsUsers.Count) user profile(s) to scan." -Level Warning
            foreach ($user in $windowsUsers) {
                Write-EMLog -Message "Scanning events for user: $($user.UserName)" -Level Warning
                Get-WindowsEventsAndSessions `
                    -sessionId $SessionId `
                    -timeRangeForEventsBefore $startTime `
                    -user $user.UserName
            }
        }
    }
    catch {
        Write-EMLog -Message "User event collection failed: $($_.Exception.Message)" -Level Error
    }

    # Active SSH detection
    try {
        $hasSSH = Get-ActiveSSHDConnectionByNetStat -sessionId $SessionId
        Write-EMLog -Message "Active SSH connections detected: $hasSSH"
    }
    catch {
        Write-EMLog -Message "SSH detection failed: $($_.Exception.Message)" -Level Error
    }

    # Machine-wide event processors — only run enabled groups
    $machineParams = @{
        sessionId = $SessionId
        StartTime = $startTime
    }
    $enabled = $script:MonitoringConfig.EnabledGroups

    if ('AccountManagement' -in $enabled) { try { Get-AccountEvents      @machineParams } catch { Write-EMLog -Message "AccountEvents: $($_.Exception.Message)"      -Level Error } }
    if ('GroupManagement'   -in $enabled) { try { Get-GroupEvents        @machineParams } catch { Write-EMLog -Message "GroupEvents: $($_.Exception.Message)"        -Level Error } }
    if ('Persistence' -in $enabled -or 'PersistenceSystem' -in $enabled) { try { Get-PersistenceEvents  @machineParams } catch { Write-EMLog -Message "PersistenceEvents: $($_.Exception.Message)"  -Level Error } }
    if ('AuditTampering'    -in $enabled) { try { Get-AuditEvents        @machineParams } catch { Write-EMLog -Message "AuditEvents: $($_.Exception.Message)"        -Level Error } }
    if ('PowerShell'        -in $enabled) { try { Get-PowerShellEvents   @machineParams } catch { Write-EMLog -Message "PowerShellEvents: $($_.Exception.Message)"   -Level Error } }
    if ('SystemHealth'      -in $enabled) { try { Get-SystemHealthEvents @machineParams } catch { Write-EMLog -Message "SystemHealthEvents: $($_.Exception.Message)" -Level Error } }
    if ('NetworkFirewall'   -in $enabled) { try { Get-NetworkEvents      @machineParams } catch { Write-EMLog -Message "NetworkEvents: $($_.Exception.Message)"      -Level Error } }
    if ('RDP'               -in $enabled) { try { Get-RDPEvents          @machineParams } catch { Write-EMLog -Message "RDPEvents: $($_.Exception.Message)"          -Level Error } }
    if ('WinRM'             -in $enabled) { try { Get-WinRMEvents        @machineParams } catch { Write-EMLog -Message "WinRMEvents: $($_.Exception.Message)"        -Level Error } }
    if ('Defender'          -in $enabled) { try { Get-DefenderEvents     @machineParams } catch { Write-EMLog -Message "DefenderEvents: $($_.Exception.Message)"     -Level Error } }

    # Flush
    try {
        Flush-Telemetry
        Write-EMLog -Message 'Telemetry flushed.'
    }
    catch {
        Write-EMLog -Message "Flush failed: $($_.Exception.Message)" -Level Error
    }

    Write-EMLog -Message "=== Diagnostic scan completed ===" -Level Warning
}