

Kod: Tümünü seç
<#
.SYNOPSIS
Audits local Windows user accounts for security risk indicators and
optionally disables risky accounts with user confirmation.
.DESCRIPTION
This script replicates the manual investigation workflow used to analyze
the VUSR_NOTEBOOK-PC (Visual Studio Analyzer) local account:
1. Enumerates all local user accounts.
2. Checks group memberships (Administrators, Users, Remote Desktop Users).
3. Exports and parses the local User Rights Assignment policy
(SeInteractiveLogonRight, SeRemoteInteractiveLogonRight,
SeNetworkLogonRight, SeBatchLogonRight, SeServiceLogonRight).
4. Checks for services running under each account.
5. Computes a three-tier risk level (LOW / MEDIUM / HIGH) per account.
6. Optionally disables accounts, only after explicit user confirmation
(unless -AutoApprove or -DryRun is specified).
.PARAMETER DryRun
Runs the full audit and prints what WOULD be done, but never disables
any account and never prompts for confirmation.
.PARAMETER AutoApprove
Skips interactive confirmation and automatically disables accounts
flagged as HIGH risk. MEDIUM risk accounts are still reported only.
Ignored if -DryRun is specified.
.PARAMETER Silent
Suppresses console color output and the on-screen table; only the
CSV report is written. Confirmation prompts still occur unless
-AutoApprove or -DryRun is also specified.
.PARAMETER LogFolder
Folder where the CSV audit report is saved. Created if it does not exist.
Default: Desktop\SecurityAudit
.EXAMPLE
.\Invoke-LocalAccountSecurityAudit.ps1 -DryRun
.EXAMPLE
.\Invoke-LocalAccountSecurityAudit.ps1 -AutoApprove -LogFolder "C:\Logs"
#>
param(
[switch]$DryRun,
[switch]$AutoApprove,
[switch]$Silent,
[string]$LogFolder = "$env:USERPROFILE\Desktop\SecurityAudit"
)
# ---------------------------------------------------------------------------
# Elevation check
# ---------------------------------------------------------------------------
function Test-IsAdmin {
$currentIdentity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($currentIdentity)
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
if (-not (Test-IsAdmin)) {
Write-Host "This script must be run as Administrator. Exiting." -ForegroundColor Red
Read-Host "`nPress ENTER to exit."
exit 1
}
# ---------------------------------------------------------------------------
# Helper: resolve a raw secedit rights entry (SID or name) to a display name
# ---------------------------------------------------------------------------
function Resolve-RightsEntry {
param([string]$Entry)
if ($Entry -like "*S-1-*") {
$rawSid = $Entry.TrimStart('*')
try {
$sid = New-Object System.Security.Principal.SecurityIdentifier($rawSid)
return $sid.Translate([System.Security.Principal.NTAccount]).Value
} catch {
return $rawSid
}
}
return $Entry
}
# ---------------------------------------------------------------------------
# Export and parse local User Rights Assignment policy via secedit
# ---------------------------------------------------------------------------
function Get-UserRightsMap {
$tempFile = Join-Path $env:TEMP ("secpol_{0}.cfg" -f ([guid]::NewGuid().ToString("N")))
secedit /export /cfg $tempFile /areas USER_RIGHTS | Out-Null
if (-not (Test-Path $tempFile)) {
Write-Host "Warning: secedit export failed. User rights data will be incomplete." -ForegroundColor Yellow
return @{}
}
$lines = Get-Content -Path $tempFile -Encoding Unicode
Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
$rightsMap = @{}
foreach ($line in $lines) {
if ($line -match '^(Se\w+)\s*=\s*(.*)$') {
$rightName = $Matches[1]
$rawList = $Matches[2] -split ','
$resolved = @()
foreach ($rawEntry in $rawList) {
$trimmed = $rawEntry.Trim()
if ($trimmed) {
$resolved += (Resolve-RightsEntry -Entry $trimmed)
}
}
$rightsMap[$rightName] = $resolved
}
}
return $rightsMap
}
# ---------------------------------------------------------------------------
# Build the per-account risk report
# ---------------------------------------------------------------------------
function Get-AccountRiskReport {
param([hashtable]$RightsMap)
$currentUserName = $env:USERNAME
$allUsers = Get-LocalUser
$report = @()
foreach ($user in $allUsers) {
$accountName = $user.Name
# Group memberships
$memberOfGroups = @()
foreach ($group in Get-LocalGroup) {
$members = Get-LocalGroupMember -Group $group.Name -ErrorAction SilentlyContinue
if ($members | Where-Object { $_.Name -like "*\$accountName" -or $_.Name -eq $accountName }) {
$memberOfGroups += $group.Name
}
}
$isAdminMember = $memberOfGroups -contains "Administrators"
$hasInteractive = $RightsMap["SeInteractiveLogonRight"] -contains $accountName
$hasRemoteInteractive = $RightsMap["SeRemoteInteractiveLogonRight"] -contains $accountName
$hasNetwork = $RightsMap["SeNetworkLogonRight"] -contains $accountName
$hasBatch = $RightsMap["SeBatchLogonRight"] -contains $accountName
$hasService = $RightsMap["SeServiceLogonRight"] -contains $accountName
# Services running under this account
$tiedServices = Get-CimInstance Win32_Service -ErrorAction SilentlyContinue |
Where-Object { $_.StartName -like "*$accountName*" } |
Select-Object -ExpandProperty Name
# ---------------------------------------------------------------
# Risk scoring
# ---------------------------------------------------------------
$score = 0
$reasons = @()
if ($user.Enabled -and (-not $user.PasswordRequired)) {
$score += 3
$reasons += "Enabled with no password required"
}
if ($user.Enabled -and $isAdminMember) {
$score += 2
$reasons += "Enabled and member of Administrators"
}
if ($user.Enabled -and ($hasInteractive -or $hasRemoteInteractive)) {
$score += 2
$reasons += "Enabled with interactive or remote logon rights"
}
if ($accountName -eq "Guest" -and $user.Enabled) {
$score += 3
$reasons += "Built-in Guest account is enabled"
}
if ($user.Enabled -and ($hasBatch -or $hasService) -and $tiedServices.Count -eq 0) {
$score += 1
$reasons += "Has batch/service logon right but no tied service found"
}
if ($user.Enabled -and -not $user.LastLogon -and $user.PasswordLastSet -and
((Get-Date) - $user.PasswordLastSet).Days -gt 365) {
$score += 1
$reasons += "Enabled, never logged on, password older than 1 year"
}
$riskLevel = "LOW"
if ($score -ge 5) { $riskLevel = "HIGH" }
elseif ($score -ge 2) { $riskLevel = "MEDIUM" }
$report += [PSCustomObject]@{
Name = $accountName
Enabled = $user.Enabled
PasswordRequired = $user.PasswordRequired
IsCurrentUser = ($accountName -eq $currentUserName)
Groups = ($memberOfGroups -join "; ")
InteractiveLogon = $hasInteractive
RemoteInteractiveLogon= $hasRemoteInteractive
NetworkLogon = $hasNetwork
BatchLogon = $hasBatch
ServiceLogon = $hasService
TiedServices = ($tiedServices -join "; ")
RiskScore = $score
RiskLevel = $riskLevel
RiskReasons = ($reasons -join " | ")
}
}
return $report
}
# ---------------------------------------------------------------------------
# Console output helper (three-tier color convention: GREEN / YELLOW / RED)
# ---------------------------------------------------------------------------
function Write-RiskLine {
param([PSCustomObject]$AccountEntry)
$color = "Green"
if ($AccountEntry.RiskLevel -eq "MEDIUM") { $color = "Yellow" }
if ($AccountEntry.RiskLevel -eq "HIGH") { $color = "Red" }
Write-Host ("[{0}] {1} (Score: {2})" -f $AccountEntry.RiskLevel, $AccountEntry.Name, $AccountEntry.RiskScore) -ForegroundColor $color
if ($AccountEntry.RiskReasons) {
Write-Host (" Reasons: {0}" -f $AccountEntry.RiskReasons) -ForegroundColor $color
}
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
Write-Host "Starting local account security audit..." -ForegroundColor Cyan
Write-Host "Exporting user rights assignment policy..." -ForegroundColor Cyan
$rightsMap = Get-UserRightsMap
$report = Get-AccountRiskReport -RightsMap $rightsMap
if (-not $Silent) {
Write-Host "`n===== Local Account Risk Report =====`n" -ForegroundColor Cyan
$sortedReport = $report | Sort-Object -Property @{Expression = {
switch ($_.RiskLevel) { "HIGH" {0} "MEDIUM" {1} default {2} }
}}, Name
foreach ($entry in $sortedReport) {
Write-RiskLine -AccountEntry $entry
}
Write-Host ""
}
# ---------------------------------------------------------------------------
# Save CSV report
# ---------------------------------------------------------------------------
if (-not (Test-Path $LogFolder)) {
New-Item -Path $LogFolder -ItemType Directory -Force | Out-Null
}
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$csvPath = Join-Path $LogFolder ("LocalAccountAudit_{0}.csv" -f $timestamp)
$report | Export-Csv -Path $csvPath -NoTypeInformation -Encoding UTF8
Write-Host ("Report saved to: {0}" -f $csvPath) -ForegroundColor Cyan
# ---------------------------------------------------------------------------
# Remediation: disable risky accounts
# ---------------------------------------------------------------------------
$actionable = $report | Where-Object { $_.Enabled -and $_.RiskLevel -in @("HIGH", "MEDIUM") -and -not $_.IsCurrentUser }
if ($actionable.Count -eq 0) {
Write-Host "`nNo actionable risky accounts found." -ForegroundColor Green
} elseif ($DryRun) {
Write-Host "`n[DRY RUN] The following accounts would be reviewed for disabling:" -ForegroundColor Yellow
foreach ($entry in $actionable) {
Write-Host (" - {0} ({1})" -f $entry.Name, $entry.RiskLevel) -ForegroundColor Yellow
}
} else {
Write-Host "`n===== Remediation =====`n" -ForegroundColor Cyan
foreach ($entry in $actionable) {
if ($entry.Name -eq "Administrator" -or $entry.Name -eq "Guest") {
if ($entry.Name -eq "Guest" -and $entry.RiskLevel -ne "HIGH") {
continue
}
}
$shouldDisable = $false
if ($AutoApprove -and $entry.RiskLevel -eq "HIGH") {
$shouldDisable = $true
Write-Host ("Auto-approving disable for HIGH risk account: {0}" -f $entry.Name) -ForegroundColor Red
} elseif (-not $AutoApprove) {
Write-Host ("Account: {0} Risk: {1} Reasons: {2}" -f $entry.Name, $entry.RiskLevel, $entry.RiskReasons) -ForegroundColor Yellow
$answer = Read-Host "Disable this account now? (Y/N)"
if ($answer -match '^[Yy]') {
$shouldDisable = $true
}
}
if ($shouldDisable) {
try {
Disable-LocalUser -Name $entry.Name -ErrorAction Stop
Write-Host (" -> Disabled: {0}" -f $entry.Name) -ForegroundColor Green
} catch {
Write-Host (" -> Failed to disable {0}: {1}" -f $entry.Name, $_.Exception.Message) -ForegroundColor Red
}
} else {
Write-Host (" -> Skipped: {0}" -f $entry.Name) -ForegroundColor DarkYellow
}
}
}
Write-Host "`nAudit complete." -ForegroundColor Cyan
Read-Host "`nPress ENTER to exit."

