218 lines
12 KiB
PowerShell
218 lines
12 KiB
PowerShell
#requires -modules ActiveDirectory
|
|
Add-Type -AssemblyName PresentationFramework, PresentationCore, WindowsBase, System.Xaml, System.Windows.Forms
|
|
|
|
# AD 모듈 로드
|
|
try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Host "[오류] AD 모듈 로드 실패"; exit 1 }
|
|
|
|
# AD 함수 정의
|
|
function Get-LockedADAccounts {
|
|
try {
|
|
$pdc = (Get-ADDomain).PDCEmulator
|
|
$adAccounts = @(Search-ADAccount -LockedOut -Server $pdc | Get-ADUser -Properties Name, SamAccountName, LockedOut, LastLogonDate, BadPwdCount, DisplayName -Server $pdc)
|
|
$list = @()
|
|
foreach ($account in $adAccounts) {
|
|
$lastLogon = if ($account.LastLogonDate -and $account.LastLogonDate.Year -ne 1601) { $account.LastLogonDate } else { $null }
|
|
$list += [PSCustomObject]@{
|
|
Name = if ([string]::IsNullOrWhiteSpace($account.DisplayName)) { $account.Name } else { $account.DisplayName }
|
|
SamAccountName = $account.SamAccountName
|
|
LockedOut = $account.LockedOut
|
|
LastLogonDate = $lastLogon
|
|
BadPwdCount = $account.BadPwdCount
|
|
}
|
|
}
|
|
return ,$list
|
|
} catch { Write-Log "AD 조회 오류: $($_.Exception.Message)" "Red"; return $null }
|
|
}
|
|
|
|
function Unlock-ADUserAccount { param([string]$SamAccountName)
|
|
try {
|
|
$pdc = (Get-ADDomain).PDCEmulator
|
|
Unlock-ADAccount -Identity $SamAccountName -Server $pdc
|
|
return $true
|
|
} catch { Write-Log "잠금 해제 실패: $SamAccountName - $($_.Exception.Message)" "Red"; return $false }
|
|
}
|
|
|
|
function Get-ADLockoutEvent { param([string]$SamAccountName)
|
|
$dcs = (Get-ADDomainController -Filter *).Name
|
|
$latest = $null
|
|
foreach ($dc in $dcs) {
|
|
try {
|
|
$filter = @{ LogName = 'Security'; ID = 4740; StartTime = (Get-Date).AddDays(-1) }
|
|
$event = Get-WinEvent -ComputerName $dc -FilterHashtable $filter -ErrorAction SilentlyContinue |
|
|
Where-Object { $_.Properties[0].Value -eq $SamAccountName } |
|
|
Sort-Object TimeCreated -Descending | Select-Object -First 1
|
|
if ($event -and ($null -eq $latest -or $event.TimeCreated -gt $latest.TimeCreated)) { $latest = $event }
|
|
} catch {}
|
|
}
|
|
if (-not $latest) { return $null }
|
|
|
|
$lockoutTime = $latest.TimeCreated
|
|
$sourceDC = $latest.MachineName
|
|
$caller = $latest.Properties[3].Value
|
|
|
|
if ([string]::IsNullOrWhiteSpace($caller) -or $caller -eq 'S-1-5-18' -or $caller -eq $sourceDC) {
|
|
try {
|
|
$filter4625 = @{ LogName='Security'; ID=4625; StartTime=$lockoutTime.AddMinutes(-5); EndTime=$lockoutTime }
|
|
$fail = Get-WinEvent -ComputerName $sourceDC -FilterHashtable $filter4625 -ErrorAction SilentlyContinue |
|
|
Where-Object { ($_.Properties[5].Value -like "*$SamAccountName*") -and ($_.Properties[10].Value -eq '3' -or $_.Properties[10].Value -eq '10') } |
|
|
Sort-Object TimeCreated -Descending | Select-Object -First 1
|
|
if ($fail) {
|
|
$ws = $fail.Properties[13].Value
|
|
$ip = $fail.Properties[19].Value
|
|
$caller = if ($ws -and $ws -ne '-') { "$ws (IP: $ip)" } else { "IP: $ip" }
|
|
} else { $caller = "$sourceDC (자체 발생 - 4625 없음)" }
|
|
} catch {}
|
|
}
|
|
|
|
return [PSCustomObject]@{ TimeCreated=$lockoutTime; CallerComputer=$caller; DC=$sourceDC }
|
|
}
|
|
|
|
# 로그 출력
|
|
function Write-Log { param([string]$Message,[string]$Color="White")
|
|
try { $mainForm.Dispatcher.Invoke({$para=New-Object Windows.Documents.Paragraph;$run=New-Object Windows.Documents.Run($Message);$run.Foreground=(New-Object Windows.Media.BrushConverter).ConvertFromString($Color);$para.Inlines.Add($run);$LogBox.Document.Blocks.Add($para);$LogBox.ScrollToEnd()}) | Out-Null } catch { Write-Host $Message }
|
|
}
|
|
|
|
# XAML GUI 정의 (Dark, DataGrid, 버튼, 로그)
|
|
[xml]$xaml=@"
|
|
<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
|
|
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|
Title='AD 잠금 해제 툴' Height='640' Width='960' WindowStartupLocation='CenterScreen' Background='#1E1E1E'>
|
|
<Grid Margin='10'>
|
|
<Grid.RowDefinitions>
|
|
<RowDefinition Height='Auto'/><RowDefinition Height='*'/><RowDefinition Height='Auto'/><RowDefinition Height='160'/>
|
|
</Grid.RowDefinitions>
|
|
|
|
<StackPanel Orientation='Horizontal' Grid.Row='0' Margin='0,0,0,10'>
|
|
<Grid Width='260' Height='28' Margin='0,0,10,0'>
|
|
<TextBox x:Name='SearchBox' Background='#2D2D30' Foreground='White' FontFamily='Segoe UI' FontSize='13' BorderThickness='0'/>
|
|
<TextBlock Text='사용자 검색...' Margin='6,0,0,0' VerticalAlignment='Center' Foreground='Gray' IsHitTestVisible='False'>
|
|
<TextBlock.Style>
|
|
<Style TargetType='TextBlock'>
|
|
<Setter Property='Visibility' Value='Collapsed'/>
|
|
<Style.Triggers>
|
|
<DataTrigger Binding='{Binding Text, ElementName=SearchBox}' Value=''><Setter Property='Visibility' Value='Visible'/></DataTrigger>
|
|
</Style.Triggers>
|
|
</Style>
|
|
</TextBlock.Style>
|
|
</TextBlock>
|
|
</Grid>
|
|
<Button x:Name='SearchButton' Content='검색' Width='80' Height='28' Margin='0,0,10,0'/>
|
|
<Button x:Name='UnlockButton' Content='계정 잠금 해제' Width='130' Height='28' Margin='0,0,10,0'/>
|
|
<Button x:Name='CheckSourceButton' Content='잠금 출처 확인' Width='130' Height='28'/>
|
|
</StackPanel>
|
|
|
|
<DataGrid x:Name='LockedAccountsDataGrid' Grid.Row='1' AutoGenerateColumns='False' CanUserAddRows='False'
|
|
IsReadOnly='True' SelectionMode='Single' AlternatingRowBackground='#2D2D30' Background='#1E1E1E' Foreground='White'
|
|
RowBackground='#2D2D30' HorizontalGridLinesBrush='#3E3E42' VerticalGridLinesBrush='#3E3E42' ColumnHeaderHeight='30' RowHeaderWidth='0'>
|
|
<DataGrid.Resources>
|
|
<Style TargetType='DataGridColumnHeader'>
|
|
<Setter Property='Background' Value='#3E3E42'/><Setter Property='Foreground' Value='#DCDCDC'/><Setter Property='FontWeight' Value='Bold'/>
|
|
</Style>
|
|
<Style TargetType='DataGridCell'>
|
|
<Style.Triggers>
|
|
<DataTrigger Binding='{Binding LockedOut}' Value='True'>
|
|
<Setter Property='Foreground' Value='#FF6A00'/><Setter Property='FontWeight' Value='Bold'/>
|
|
</DataTrigger>
|
|
</Style.Triggers>
|
|
</Style>
|
|
</DataGrid.Resources>
|
|
<DataGrid.Columns>
|
|
<DataGridTextColumn Header='표시 이름' Binding='{Binding Name}' Width='200'/>
|
|
<DataGridTextColumn Header='로그온 ID' Binding='{Binding SamAccountName}' Width='150'/>
|
|
<DataGridTextColumn Header='잠김 여부' Binding='{Binding LockedOut}' Width='100'/>
|
|
<DataGridTextColumn Header='마지막 로그인' Binding='{Binding LastLogonDate, StringFormat=yyyy-MM-dd HH:mm:ss}' Width='200'/>
|
|
<DataGridTextColumn Header='암호 오류 횟수' Binding='{Binding BadPwdCount}' Width='120'/>
|
|
</DataGrid.Columns>
|
|
</DataGrid>
|
|
|
|
<Button x:Name='RefreshButton' Content='새로고침' Grid.Row='2' Width='110' Height='28' Margin='0,10,0,10' HorizontalAlignment='Left'/>
|
|
<RichTextBox x:Name='LogBox' Grid.Row='3' Background='Black' Foreground='White' IsReadOnly='True' VerticalScrollBarVisibility='Auto'/>
|
|
</Grid>
|
|
</Window>
|
|
"@
|
|
|
|
$reader = New-Object System.Xml.XmlNodeReader $xaml
|
|
$mainForm = [Windows.Markup.XamlReader]::Load($reader)
|
|
$reader.Close()
|
|
|
|
$SearchBox = $mainForm.FindName('SearchBox')
|
|
$SearchButton = $mainForm.FindName('SearchButton')
|
|
$UnlockButton = $mainForm.FindName('UnlockButton')
|
|
$CheckSourceButton = $mainForm.FindName('CheckSourceButton')
|
|
$LockedAccountsDataGrid = $mainForm.FindName('LockedAccountsDataGrid')
|
|
$RefreshButton = $mainForm.FindName('RefreshButton')
|
|
$LogBox = $mainForm.FindName('LogBox')
|
|
|
|
# 데이터 그리드 채우기
|
|
function Populate-DataGrid {
|
|
Write-Log "잠긴 계정 목록 가져오는 중..."
|
|
$data = Get-LockedADAccounts
|
|
if ($data) { $LockedAccountsDataGrid.ItemsSource = $data; Write-Log "잠김 계정 목록 새로고침 완료 (총 $($data.Count)개)" "Cyan" }
|
|
else { $LockedAccountsDataGrid.ItemsSource = $null; Write-Log "잠김 계정 없음!" "Red" }
|
|
}
|
|
|
|
$RefreshButton.Add_Click({ Populate-DataGrid })
|
|
|
|
$SearchButton.Add_Click({
|
|
$q=$SearchBox.Text.Trim()
|
|
if ([string]::IsNullOrWhiteSpace($q)) { Populate-DataGrid; return }
|
|
try {
|
|
$pdc=(Get-ADDomain).PDCEmulator
|
|
$result=Get-ADUser -Filter "SamAccountName -like '*$q*' -or Name -like '*$q*' -or DisplayName -like '*$q*'" -Server $pdc -Properties LockedOut, LastLogonDate, BadPwdCount, DisplayName |
|
|
Select-Object @{n='Name';e={ if ([string]::IsNullOrWhiteSpace($_.DisplayName)){ $_.Name } else { $_.DisplayName } }}, SamAccountName, LockedOut,
|
|
@{n='LastLogonDate';e={ $_.LastLogonDate }}, BadPwdCount
|
|
$LockedAccountsDataGrid.ItemsSource=@($result)
|
|
Write-Log "검색 완료: '$q' ($(@($result).Count)건)" "Cyan"
|
|
} catch { Write-Log "검색 실패: $($_.Exception.Message)" "Red" }
|
|
})
|
|
|
|
$UnlockButton.Add_Click({
|
|
if ($LockedAccountsDataGrid.SelectedItem) {
|
|
$sel=$LockedAccountsDataGrid.SelectedItem
|
|
Write-Log "잠금 해제 요청: $($sel.Name) [$($sel.SamAccountName)]" "Yellow"
|
|
if (Unlock-ADUserAccount -SamAccountName $sel.SamAccountName) { Write-Log "성공: '$($sel.Name)' 잠금 해제 완료" "Green"; Populate-DataGrid }
|
|
} else { Write-Log "계정을 선택하세요." "Orange" }
|
|
})
|
|
|
|
$CheckSourceButton.Add_Click({
|
|
if ($LockedAccountsDataGrid.SelectedItem) {
|
|
$sel=$LockedAccountsDataGrid.SelectedItem
|
|
Write-Log "잠금 출처 조회 중... ($($sel.SamAccountName))" "Yellow"
|
|
$info = Get-ADLockoutEvent -SamAccountName $sel.SamAccountName
|
|
if ($info) {
|
|
$details=@"
|
|
표시 이름: $($sel.Name)
|
|
로그온 ID: $($sel.SamAccountName)
|
|
잠긴 시간: $($info.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))
|
|
잠금 발생 위치: $($info.CallerComputer)
|
|
로그 발견 DC: $($info.DC)
|
|
"@
|
|
|
|
$popup=New-Object Windows.Window
|
|
$popup.Title="잠금 출처 정보"; $popup.Width=520; $popup.Height=320; $popup.WindowStartupLocation="CenterOwner"; $popup.Owner=$mainForm; $popup.Background=[Windows.Media.Brushes]::Black
|
|
|
|
$grid=New-Object Windows.Controls.Grid
|
|
$grid.Margin=10
|
|
$grid.RowDefinitions.Add((New-Object Windows.Controls.RowDefinition))
|
|
$grid.RowDefinitions.Add((New-Object Windows.Controls.RowDefinition -Property @{Height=35}))
|
|
|
|
$tb=New-Object Windows.Controls.TextBox
|
|
$tb.Text=$details; $tb.FontFamily='Consolas'; $tb.FontSize=13; $tb.Foreground=[Windows.Media.Brushes]::White
|
|
$tb.Background=[Windows.Media.Brushes]::Black; $tb.TextWrapping='Wrap'; $tb.IsReadOnly=$true
|
|
$tb.VerticalScrollBarVisibility='Auto'; $tb.HorizontalScrollBarVisibility='Auto'
|
|
|
|
$btn=New-Object Windows.Controls.Button
|
|
$btn.Content="복사"; $btn.Height=28; $btn.Width=80; $btn.HorizontalAlignment='Right'; $btn.Margin='0,5,0,0'
|
|
$btn.Add_Click({ [Windows.Clipboard]::SetText($tb.Text); Write-Log "잠금 출처 정보 복사됨" "Cyan" })
|
|
|
|
$grid.Children.Add($tb); [Windows.Controls.Grid]::SetRow($tb,0)
|
|
$grid.Children.Add($btn); [Windows.Controls.Grid]::SetRow($btn,1)
|
|
|
|
$popup.Content=$grid; $popup.ShowDialog() | Out-Null
|
|
} else { Write-Log "잠금 이벤트 없음" "Orange" }
|
|
} else { Write-Log "계정을 선택하세요." "Orange" }
|
|
})
|
|
|
|
Populate-DataGrid
|
|
$mainForm.ShowDialog() | Out-Null
|