TurnToMenu.ps1 — PowerShell Menu System - Snowboundport37/champlain GitHub Wiki
Author: Andrei Gorlitsky
Date: October 7, 2025
Course: SEC-350
A PowerShell menu system that integrates security and system administration functions with input validation and error handling.
<# 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!"