TurnToMenu.ps1 — PowerShell Menu System - Snowboundport37/champlain GitHub Wiki

TurnToMenu.ps1 — PowerShell Menu System

Author: Andrei Gorlitsky
Date: October 7, 2025
Course: SEC-350


Overview

A PowerShell menu system that integrates security and system administration functions with input validation and error handling.


The Script

<# TurnToMenu.ps1 - Robust version with Security-log fallback to exported EVTX file #>

# ---------- Helper functions ----------
function Pause-Enter { Read-Host "`nPress Enter to continue" | Out-Null }

function Read-SecurityEvents {
    param(
        [int]$Days = 7,
        [int]$Max = 100,
        [string]$EvtxPath  # optional: path to an exported .evtx file
    )

    $since = (Get-Date).AddDays(-$Days)

    if ($EvtxPath) {
        if (-not (Test-Path -LiteralPath $EvtxPath)) {
            throw "EVTX file not found: $EvtxPath"
        }
        # Read from the exported file
        return Get-WinEvent -Path $EvtxPath -FilterXPath "*[System[EventID=4625 or EventID=4624] and System/TimeCreated/@SystemTime >= '$($since.ToUniversalTime().ToString("o"))']" -ErrorAction Stop -MaxEvents $Max
    }

    # Try reading live Security log first (may be blocked by policy)
    try {
        return Get-WinEvent -FilterHashtable @{LogName='Security'; StartTime=$since; Id=4625,4624} -ErrorAction Stop -MaxEvents $Max
    } catch {
        throw $_
    }
}

# Helper to parse a failed 4625 event for username (best-effort)
function Parse-FailedLogin {
    param($evt)
    $user = $null
    try {
        # try properties (fast)
        if ($evt.Properties.Count -gt 5) { $user = $evt.Properties[5].Value }
    } catch {}
    if (-not $user) {
        # fallback: regex on Message
        $m = [regex]::Match($evt.Message, '(?ms)Account For Which Logon Failed:.*?Account Name:\s*(?<acct>[^\r\n]+)')
        if ($m.Success) { $user = $m.Groups['acct'].Value.Trim() }
    }
    return $user
}

# ---------- Menu actions ----------
function Show-ApacheLogs {
    $default = 'C:\xampp\apache\logs\access.log'
    $path = Read-Host "Enter Apache access log path (or press Enter for default [$default])"
    if ([string]::IsNullOrWhiteSpace($path)) { $path = $default }

    if (Test-Path -LiteralPath $path) {
        Write-Host "`nLast 10 lines of Apache access log:`n"
        Get-Content -LiteralPath $path -Tail 10
    } else {
        Write-Host "File not found: $path"
    }
}

function Show-LastFailedLogins {
    # tries live Security log; if permission denied, asks for an exported EVTX
    $days = Read-Host "How many days back to search? (default 7)"
    if (-not [int]::TryParse($days, [ref]0)) { $days = 7 }

    try {
        $events = Read-SecurityEvents -Days $days -Max 50
    } catch {
        Write-Host "`n⚠️  Cannot read live Security log: $($_.Exception.Message)"
        Write-Host "If you can't change the policy, export the Security log via Event Viewer (Action → Save All Events As...)"
        $evtx = Read-Host "Enter full path to the exported Security .evtx file (or press Enter to cancel)"
        if ([string]::IsNullOrWhiteSpace($evtx)) { Write-Host "Canceled."; return }
        try {
            $events = Read-SecurityEvents -Days $days -EvtxPath $evtx -Max 50
        } catch {
            Write-Host "Failed to read exported EVTX: $($_.Exception.Message)"
            return
        }
    }

    # Filter failed logons (4625) and show last 10 with Time/User/Machine/IP
    $failed = $events | Where-Object { $_.Id -eq 4625 } | Sort-Object TimeCreated -Descending | Select-Object -First 10
    if (-not $failed) { Write-Host "No failed login events found in the period."; return }

    $out = foreach ($e in $failed) {
        $user = Parse-FailedLogin -evt $e
        $ip = try { ([regex]::Match($e.Message, '(?im)Source Network Address:\s*(?<ip>[^\r\n]+)')).Groups['ip'].Value } catch { $null }
        [pscustomobject]@{
            TimeCreated = $e.TimeCreated
            User        = $user
            Machine     = $e.MachineName
            IpAddress   = $ip
        }
    }
    $out | Format-Table -AutoSize
}

function Show-AtRiskUsers {
    $days = Read-Host "Count failed logins within how many days? (default 7)"
    if (-not [int]::TryParse($days, [ref]0)) { $days = 7 }
    $threshold = Read-Host "Threshold (failed attempts) to flag a user as 'at risk' (default 10)"
    if (-not [int]::TryParse($threshold, [ref]0)) { $threshold = 10 }

    try {
        $events = Read-SecurityEvents -Days $days -Max 1000
    } catch {
        Write-Host "`n⚠️  Cannot read live Security log: $($_.Exception.Message)"
        Write-Host "If you can't change the policy, export the Security log via Event Viewer (Action → Save All Events As...)"
        $evtx = Read-Host "Enter full path to the exported Security .evtx file (or press Enter to cancel)"
        if ([string]::IsNullOrWhiteSpace($evtx)) { Write-Host "Canceled."; return }
        try {
            $events = Read-SecurityEvents -Days $days -EvtxPath $evtx -Max 1000
        } catch {
            Write-Host "Failed to read exported EVTX: $($_.Exception.Message)"
            return
        }
    }

    $failed = $events | Where-Object { $_.Id -eq 4625 }
    if (-not $failed) { Write-Host "No failed login events found in the period."; return }

    $groups = $failed | Group-Object -Property { Parse-FailedLogin -evt $_ } -NoElement
    $atRisk = $groups | Where-Object { $_.Count -ge $threshold } | Sort-Object Count -Descending

    if (-not $atRisk) { Write-Host "No at-risk users found (threshold $threshold)." ; return }

    $atRisk | Select-Object @{n='User';e={$_.Name}}, @{n='FailedCount';e={$_.Count}} | Format-Table -AutoSize
}

function Start-Champlain {
    $proc = Get-Process -Name chrome -ErrorAction SilentlyContinue
    if ($null -eq $proc) {
        Start-Process "chrome.exe" "https://www.champlain.edu"
        Write-Host "Chrome started to champlain.edu"
    } else {
        Write-Host "Chrome is already running"
    }
}

# ---------- Main menu loop ----------
do {
    Clear-Host
    Write-Host "==== Turn To Menu ===="
    Write-Host "1) Display last 10 Apache logs"
    Write-Host "2) Display last 10 failed logins for all users"
    Write-Host "3) Display at risk users"
    Write-Host "4) Start Chrome to champlain.edu (if not running)"
    Write-Host "5) Exit"
    Write-Host ""

    $choice = Read-Host "Choose 1-5"
    if (-not [int]::TryParse($choice, [ref]0)) {
        Write-Host "Please enter a number between 1 and 5."
        Pause-Enter
        continue
    }

    switch ([int]$choice) {
        1 { Show-ApacheLogs; Pause-Enter }
        2 { Show-LastFailedLogins; Pause-Enter }
        3 { Show-AtRiskUsers; Pause-Enter }
        4 { Start-Champlain; Pause-Enter }
        5 { break }
        Default { Write-Host "Please choose a number between 1 and 5."; Pause-Enter }
    }
} while ($choice -ne 5)

Write-Host "`nGoodbye!"
image
⚠️ **GitHub.com Fallback** ⚠️