bin/Public/Get-sqmSysadminAccounts.ps1

<#
.SYNOPSIS
    Ermittelt alle Logins mit Sysadmin-Rechten auf einer SQL Server-Instanz.
 
.DESCRIPTION
    Fragt sys.server_principals und sys.server_role_members ab und liefert
    alle direkten Mitglieder der sysadmin-Serverrolle.
 
    Pro Login werden folgende Informationen ermittelt:
    - Loginname und Logintyp (SQL, Windows-User, Windows-Gruppe, etc.)
    - Aktiviert / deaktiviert
    - Ist SA (SID 0x01) oder nicht
    - Erstellungsdatum
    - Ob der Login explizit ausgeschlossen wurde (-ExcludeLogin)
 
    Mit -ExcludeLogin koennen bekannte/erwartete Konten aus dem Bericht
    gefiltert werden (sie werden als 'Excluded' markiert).
 
    Mit -ExcludeSysAccounts werden bekannte SQL Server-System- und
    Dienstkonten automatisch als 'Excluded' markiert.
 
    BUILTIN\Administrators erhaelt einen eigenen Status 'BuiltinAdmins'
    und wird NICHT automatisch ausgeschlossen - Sicherheitspruefung erforderlich.
 
    Ausgabe:
        SysadminAccounts_<instanz>_<datum>.txt - Lesbarer Bericht
        SysadminAccounts_<instanz>_<datum>.csv - Maschinenlesbar
 
.PARAMETER SqlInstance
    SQL Server-Instanz(en). Pipeline-faehig. Standard: aktueller Computername.
 
.PARAMETER SqlCredential
    Optionales PSCredential fuer die Verbindung.
 
.PARAMETER ExcludeLogin
    Logins die als 'Excluded' markiert werden (Wildcards erlaubt).
 
.PARAMETER ExcludeSysAccounts
    Wenn gesetzt, werden bekannte Systemkonten automatisch ausgeschlossen.
 
.PARAMETER IncludeDisabled
    Wenn $true (Standard), werden auch deaktivierte sysadmin-Logins einbezogen.
 
.PARAMETER OutputPath
    Ausgabeverzeichnis fuer die Berichtsdateien. Standard: C:\System\WinSrvLog\MSSQL
 
.PARAMETER ContinueOnError
    Bei Fehler auf einer Instanz fortfahren.
 
.PARAMETER EnableException
    Ausnahmen sofort ausloesen (ueberschreibt ContinueOnError).
 
.PARAMETER Confirm
    Fordert vor dem Schreiben der Dateien eine Bestaetigung an.
 
.PARAMETER WhatIf
    Zeigt, welche Dateien erstellt wuerden, ohne sie zu schreiben.
 
.EXAMPLE
    Get-sqmSysadminAccounts
 
.EXAMPLE
    Get-sqmSysadminAccounts -SqlInstance "SQL01" -ExcludeSysAccounts
#>

function Get-sqmSysadminAccounts
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'None')]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false, ValueFromPipeline = $true)]
        [string[]]$SqlInstance = @($env:COMPUTERNAME),
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $false)]
        [string[]]$ExcludeLogin = @(),
        [Parameter(Mandatory = $false)]
        [switch]$ExcludeSysAccounts,
        [Parameter(Mandatory = $false)]
        [bool]$IncludeDisabled = $true,
        [Parameter(Mandatory = $false)]
        [string]$OutputPath = 'C:\System\WinSrvLog\MSSQL',
        [Parameter(Mandatory = $false)]
        [switch]$ContinueOnError,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )
    
    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $allInstanceResults = [System.Collections.Generic.List[PSCustomObject]]::new()
        
        if (-not (Get-Module -ListAvailable -Name dbatools))
        {
            $errMsg = "dbatools-Modul nicht gefunden."
            Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
            throw $errMsg
        }
        
        Invoke-sqmLogging -Message "Starte $functionName mit OutputPath: $OutputPath" -FunctionName $functionName -Level "INFO"
        
        # Systemkonten-Muster fuer -ExcludeSysAccounts
        $sysAccountPatterns = @(
            'NT SERVICE\*',
            'NT AUTHORITY\SYSTEM',
            'NT AUTHORITY\NETWORK SERVICE',
            'NT AUTHORITY\LOCAL SERVICE',
            'NT AUTHORITY\*',
            '##MS_*##'
        )
        
        if ($ExcludeSysAccounts)
        {
            $ExcludeLogin = @($ExcludeLogin) + $sysAccountPatterns | Sort-Object -Unique
            Invoke-sqmLogging -Message "ExcludeSysAccounts: $($sysAccountPatterns.Count) Systemmuster hinzugefuegt." -FunctionName $functionName -Level "DEBUG"
        }
        
        # Hilfsfunktion fuer Ausschlusspruefung
        function _IsExcluded
        {
            param ([string]$Name,
                [string[]]$Patterns)
            if (-not $Patterns) { return $false }
            foreach ($p in $Patterns)
            {
                if ($Name -like $p) { return $true }
            }
            return $false
        }
    }
    
    process
    {
        foreach ($instance in $SqlInstance)
        {
            $connParams = @{ SqlInstance = $instance }
            if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential }
            
            $detailRows = [System.Collections.Generic.List[PSCustomObject]]::new()
            
            try
            {
                Invoke-sqmLogging -Message "[$instance] Starte Sysadmin-Audit ..." -FunctionName $functionName -Level "INFO"
                
                $disabledFilter = if ($IncludeDisabled) { '' }
                else { 'AND sp.is_disabled = 0' }
                
                # Achtung: password_last_set_time und last_login_date wurden entfernt,
                # da sie in aelteren SQL Server-Versionen nicht existieren.
                $query = @"
SELECT
    sp.name AS LoginName,
    sp.type_desc AS LoginType,
    sp.is_disabled AS IsDisabled,
    CASE WHEN sp.sid = 0x01 THEN 1 ELSE 0 END AS IsSa,
    sp.create_date AS CreateDate,
    sp.modify_date AS ModifyDate,
    NULL AS LastPasswordChange,
    NULL AS LastLogin,
    sp.default_database_name AS DefaultDatabase
FROM sys.server_principals sp
JOIN sys.server_role_members rm ON rm.member_principal_id = sp.principal_id
JOIN sys.server_principals sr ON sr.principal_id = rm.role_principal_id
WHERE sr.name = 'sysadmin'
  AND sp.type IN ('S','U','G','R')
  AND sp.principal_id > 1
  $disabledFilter
ORDER BY sp.type_desc, sp.name;
"@

                $rows = Invoke-DbaQuery @connParams -Query $query -EnableException:$EnableException
                
                if (-not $rows)
                {
                    $msg = "Keine sysadmin-Logins auf '$instance' gefunden (unerwartet)."
                    Invoke-sqmLogging -Message $msg -FunctionName $functionName -Level "WARNING"
                    $detailRows.Add([PSCustomObject]@{
                            SqlInstance           = $instance
                            LoginName           = '(keine)'
                            LoginType           = 'n/a'
                            IsEnabled           = $null
                            IsSa               = $false
                            LastPasswordChange = $null
                            LastLogin           = $null
                            CreateDate           = $null
                            Status               = 'Error'
                            Message               = $msg
                        })
                }
                else
                {
                    Invoke-sqmLogging -Message "[$instance] $($rows.Count) sysadmin-Login(s) gefunden." -FunctionName $functionName -Level "INFO"
                    
                    foreach ($row in $rows)
                    {
                        $loginName = $row.LoginName
                        $isSa = [bool]$row.IsSa
                        $isEnabled = -not [bool]$row.IsDisabled
                        $excluded = _IsExcluded $loginName $ExcludeLogin
                        $isBuiltinAdmins = ($loginName -eq 'BUILTIN\Administrators')
                        
                        $status = if ($isSa) { 'SA' }
                        elseif ($isBuiltinAdmins) { 'BuiltinAdmins' }
                        elseif ($excluded) { 'Excluded' }
                        elseif (-not $isEnabled) { 'Disabled' }
                        else { 'Unexpected' }
                        
                        $msg = switch ($status)
                        {
                            'SA'            { 'SA-Konto (SID 0x01).' }
                            'BuiltinAdmins' { 'BUILTIN\Administrators hat Sysadmin-Rechte - SICHERHEITSPRueFUNG ERFORDERLICH.' }
                            'Excluded'      { 'Ausgeschlossen via -ExcludeLogin.' }
                            'Disabled'      { 'Login hat sysadmin-Rechte, ist aber deaktiviert.' }
                            'Unexpected'    { 'Sysadmin-Login - kein Ausschluss definiert.' }
                        }
                        
                        $createDate = if ($row.CreateDate) { $row.CreateDate.ToString('yyyy-MM-dd') }
                        else { $null }
                        
                        $detailRows.Add([PSCustomObject]@{
                                SqlInstance = $instance
                                LoginName   = $loginName
                                LoginType   = $row.LoginType
                                IsEnabled   = $isEnabled
                                IsSa        = $isSa
                                LastPasswordChange = $null # Nicht verfuegbar in aelteren Versionen
                                LastLogin   = $null # Nicht verfuegbar in aelteren Versionen
                                CreateDate  = $createDate
                                Status        = $status
                                Message        = $msg
                            })
                    }
                }
                
                # Statistik
                $cntSa = ($detailRows | Where-Object Status -eq 'SA').Count
                $cntExcluded = ($detailRows | Where-Object Status -eq 'Excluded').Count
                $cntDisabled = ($detailRows | Where-Object Status -eq 'Disabled').Count
                $cntUnexpected = ($detailRows | Where-Object Status -eq 'Unexpected').Count
                $cntBuiltinAdmins = ($detailRows | Where-Object Status -eq 'BuiltinAdmins').Count
                
                Invoke-sqmLogging -Message ("[$instance] Gesamt: $($detailRows.Count) | SA: $cntSa | Ausgeschlossen: $cntExcluded | " +
                    "Deaktiviert: $cntDisabled | Unerwartet: $cntUnexpected | BUILTIN\\Admins: $cntBuiltinAdmins") -FunctionName $functionName -Level "INFO"
                
                # Dateien schreiben
                $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
                $datestamp = Get-Date -Format 'yyyy-MM-dd'
                $safeInst = $instance -replace '[\\/:*?"<>|]', '_'
                $txtFile = Join-Path $OutputPath "SysadminAccounts_${safeInst}_${datestamp}.txt"
                $csvFile = Join-Path $OutputPath "SysadminAccounts_${safeInst}_${datestamp}.csv"
                
                if ($PSCmdlet.ShouldProcess($instance, "Erstelle Sysadmin-Bericht in $OutputPath"))
                {
                    if (-not (Test-Path $OutputPath))
                    {
                        New-Item -ItemType Directory -Path $OutputPath -Force -ErrorAction Stop | Out-Null
                        Invoke-sqmLogging -Message "Verzeichnis $OutputPath wurde erstellt." -FunctionName $functionName -Level "INFO"
                    }
                    
                    # TXT-Bericht (identisch zum vorherigen, daher hier ausgelassen - bitte aus Original uebernehmen)
                    # ... (der Code fuer die TXT-Erstellung bleibt unveraendert)
                    $lines = [System.Collections.Generic.List[string]]::new()
                    $lines.Add("# ================================================================")
                    $lines.Add("# MSSQLTools - Sysadmin-Konten Bericht")
                    $lines.Add("# Instanz : $instance")
                    $lines.Add("# Erstellt : $timestamp")
                    $lines.Add("# Gesamt : $($detailRows.Count) Logins")
                    $lines.Add("# SA : $cntSa")
                    $lines.Add("# Ausgesch. : $cntExcluded")
                    $lines.Add("# Deaktiv. : $cntDisabled")
                    $lines.Add("# Unerwartet: $cntUnexpected ? PRueFEN")
                    $lines.Add("# BUILTIN\\Adm: $cntBuiltinAdmins ? SICHERHEITSPRueFUNG")
                    $lines.Add("# SysExclude: $(if ($ExcludeSysAccounts) { 'Ja (NT SERVICE\*, NT AUTHORITY\*, ##MS_*##)' }
                            else { 'Nein (manuell via -ExcludeLogin)' })"
)
                    $lines.Add("# ================================================================")
                    
                    # BUILTIN\Administrators
                    $builtinEntries = $detailRows | Where-Object { $_.Status -eq 'BuiltinAdmins' }
                    $lines.Add(""); $lines.Add("# ================================================================")
                    $lines.Add("# BUILTIN\Administrators - SICHERHEITSPRueFUNG ERFORDERLICH ($cntBuiltinAdmins)")
                    $lines.Add("# ================================================================")
                    if ($builtinEntries)
                    {
                        foreach ($e in $builtinEntries)
                        {
                            $lines.Add((" Name : {0}" -f $e.LoginName))
                            $lines.Add((" Typ : {0} | Aktiv: {1} | Erstellt: {2}" -f $e.LoginType, $e.IsEnabled, $e.CreateDate))
                            $lines.Add(" ? Empfehlung: Pruefen ob BUILTIN\Administrators Sysadmin-Rechte")
                            $lines.Add(" gemaess Sicherheitsrichtlinie zulaessig sind. Ggf. entfernen:")
                            $lines.Add(" EXEC sp_dropsrvrolemember 'BUILTIN\Administrators','sysadmin';")
                        }
                    }
                    else { $lines.Add(" (nicht vorhanden - kein Befund)") }
                    
                    # Unerwartete Konten
                    $unexpected = $detailRows | Where-Object { $_.Status -eq 'Unexpected' }
                    $lines.Add(""); $lines.Add("# ----------------------------------------------------------------")
                    $lines.Add("# UNERWARTETE SYSADMIN-KONTEN ($cntUnexpected) ? PRueFEN")
                    $lines.Add("# ----------------------------------------------------------------")
                    if ($unexpected)
                    {
                        foreach ($e in ($unexpected | Sort-Object LoginType, LoginName))
                        {
                            $lines.Add((" {0,-40} {1,-20} Enabled:{2,-5} Erstellt:{3}" -f $e.LoginName, $e.LoginType, $e.IsEnabled, $e.CreateDate))
                        }
                    }
                    else { $lines.Add(" (keine)") }
                    
                    # Deaktivierte Konten
                    $disabledEntries = $detailRows | Where-Object { $_.Status -eq 'Disabled' }
                    $lines.Add(""); $lines.Add("# ----------------------------------------------------------------")
                    $lines.Add("# DEAKTIVIERTE SYSADMIN-KONTEN ($cntDisabled)")
                    $lines.Add("# ----------------------------------------------------------------")
                    if ($disabledEntries)
                    {
                        foreach ($e in ($disabledEntries | Sort-Object LoginName))
                        {
                            $lines.Add(" $($e.LoginName) [$($e.LoginType)] Erstellt: $($e.CreateDate)")
                        }
                    }
                    else { $lines.Add(" (keine)") }
                    
                    # SA-Konto
                    $saEntry = $detailRows | Where-Object { $_.Status -eq 'SA' }
                    $lines.Add(""); $lines.Add("# ----------------------------------------------------------------")
                    $lines.Add("# SA-KONTO (SID 0x01)")
                    $lines.Add("# ----------------------------------------------------------------")
                    if ($saEntry)
                    {
                        foreach ($e in $saEntry)
                        {
                            $lines.Add((" Name: {0,-40} Enabled: {1}" -f $e.LoginName, $e.IsEnabled))
                        }
                    }
                    else { $lines.Add(" (nicht gefunden)") }
                    
                    # Ausgeschlossene Konten
                    $excludedEntries = $detailRows | Where-Object { $_.Status -eq 'Excluded' }
                    $lines.Add(""); $lines.Add("# ----------------------------------------------------------------")
                    $lines.Add("# AUSGESCHLOSSENE KONTEN ($cntExcluded)")
                    $lines.Add("# ----------------------------------------------------------------")
                    if ($excludedEntries)
                    {
                        foreach ($e in ($excludedEntries | Sort-Object LoginType, LoginName))
                        {
                            $lines.Add((" {0,-40} {1,-20} Enabled:{2}" -f $e.LoginName, $e.LoginType, $e.IsEnabled))
                        }
                    }
                    else { $lines.Add(" (keine)") }
                    
                    $lines | Out-File -FilePath $txtFile -Encoding UTF8 -Force
                    $detailRows | Export-Csv -Path $csvFile -Encoding UTF8 -NoTypeInformation -Force
                    
                    Invoke-sqmLogging -Message "[$instance] Bericht erstellt: $txtFile" -FunctionName $functionName -Level "INFO"
                }
                else
                {
                    Invoke-sqmLogging -Message "[$instance] WhatIf: Berichtsdateien wuerden erstellt werden." -FunctionName $functionName -Level "VERBOSE"
                    $txtFile = $null
                    $csvFile = $null
                }
                
                if ($cntBuiltinAdmins -gt 0)
                {
                    Invoke-sqmLogging -Message ("[$instance] BUILTIN\Administrators hat Sysadmin-Rechte - Sicherheitspruefung erforderlich!") -FunctionName $functionName -Level "WARNING"
                }
                if ($cntUnexpected -gt 0)
                {
                    Invoke-sqmLogging -Message ("[$instance] $cntUnexpected unerwartete(s) sysadmin-Konto(en) gefunden.") -FunctionName $functionName -Level "WARNING"
                }
                
                $instanceResult = [PSCustomObject]@{
                    SqlInstance                                 = $instance
                    Timestamp                                 = $timestamp
                    DetailRows                                 = $detailRows
                    TxtFile                                     = $txtFile
                    CsvFile                                     = $csvFile
                    Status                                     = if ($cntUnexpected -gt 0 -or $cntBuiltinAdmins -gt 0) { 'Warning' } else { 'OK' }
                }
                $allInstanceResults.Add($instanceResult)
            }
            catch
            {
                $errMsg = "Fehler auf '$instance': $($_.Exception.Message)"
                Invoke-sqmLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                $allInstanceResults.Add([PSCustomObject]@{
                        SqlInstance = $instance
                        Status        = 'Error'
                        Message        = $errMsg
                        DetailRows  = $null
                        TxtFile        = $null
                        CsvFile        = $null
                    })
                if ($EnableException) { throw }
                if (-not $ContinueOnError) { throw $_ }
            }
        }
    }
    
    end
    {
        Invoke-sqmLogging -Message "$functionName abgeschlossen. $($allInstanceResults.Count) Instanzen verarbeitet." -FunctionName $functionName -Level "INFO"
        return $allInstanceResults
    }
}