Artikler / PowerShell

Automatisk oprydning af computerobjekter i Active Directory

Ved at benytte dettet script kan du nemt og sikkert fjerne ubrugte computerobjekter i dit Active Directory. Det gøres ved hjælp af en Scheduled Task.

Her beskrives det hvordan man opsætter en automatiseret oprydning af computerobjekter, der ikke der ikke har været i kontakt med domain controllere i et Active Directory (AD) miljø. Løsningen består af et script, som bliver kørt af et planlagt job i kontekst af en servicebruger, der har rettigheder til at deaktivere og flytte computerobjekter til en specifik organization unit (OU).

Scriptet logger til en .txt fil som er formateret til at kunne læses af CMTrace. CMTrace bliver installeret som en del af Configuration Manager Tools, og ligger i “C:\Windows\CCM” hvis man har SCCM agenten installeret. Configuration Manager Tools kan hentes her.

Med løsningen får man et bedre overblik over hvilke computere der er i brug og samtidig får en forøget sikkerhed.

Som sikkerhedsansvarlig bestræber man sige efter at reducere systemets såkaldte angrebsoverflade. Ved at reducere antallet af computerobjekter, reducere man de potentielle vektorer og dermed overfladen.

Hvad kræver opsætningen

  • En servicebruger, som har rettigheder til at deaktivere og flytte computerobjekter i AD.
  • Servicebrugeren er i “log on as a batch job” gruppen lokalt på serveren der afvikler scriptet.
  • RSAT tools er installeret på serveren.

Scriptet i 3 dele (Variabeler, funktioner og hoveddelen)

Scriptet er inddelt i 3 sektioner for at gøre det mere overskueligt. Vi koncentrerer os mest om sektionen med variabler, da det er dem der bestemmer indstillingerne for vores kørsel. Det er derfor ikke nødvendigt at læse funktioner og hoveddelen i denne blog – men det er anbefalet – for at få den fulde forståelse af hvordan scriptet fungerer.

Hent scriptet fra Zwables repository og gem det. I eksemplet gemmer vi det til filen “Disable-Computers.ps1”.

Variabler:

##*=============================================
##* VARIABLES LISTINGS
##*=============================================
#Log variables
$LogLocation = "$PSScriptRoot\Logs"
$LogName = "Disable-Computers"

#The domain info
$DomainName = Get-ADDomain

#OU's to search
$SearchOUs = @("OU=AD Computers,DC=mroenborg,DC=dk","CN=Computers,DC=mroenborg,DC=dk")

#OU to move disabled computers to
$MoveToOU =  "OU=Disabled Computers,DC=mroenborg,DC=dk"

#Number of days from today since the last logon. This will impact on when to disable and move the object.
$Days = -120

##*=============================================
##* END VARIABLES LISTINGS
##*=============================================

Åben scriptet og gå til linie 22-36 hvor du vil finde ovenstående sektion. Her ændrer vi variablerne ud fra vores behov:

  • $LogLocation er stien til mappen for hvor logfilen skal placeres. Som standard er den sat til mappen “Logs” i mappen hvor scriptet bliver kørt fra. Hvis mappen ikke eksistere, bliver den oprettet.
  • $LogName er navnet på logfilen. Som standard er den “Disable-Computers”.
  • $DomainName indeholder oplysninger omkring domænet hvor scriptet bliver afviklet i. Som udgangspunkt skal det ikke ændres, medmindre man vil benytte en specifik AD server.
  • $SearchOUs indeholder arrayet med de OU’er scriptet skal søge i. I eksemplet er der specificeret to lokationer. Man finder lokationerne på “distinguishedName” attributten på OU’en.
  • $MoveToOu indeholder navnet på OU’en, hvor computerobjekterne vil blive flyttet til. Man finder lokationerne på “distinguishedName” attributten på OU’en.
  • $Days er antallet af dage siden sidste kommunikation med domain controllerne. Hvis et objekt overstiger dette, vil det blive deaktiveret og flyttet. Dagene er angivet i en negativ heltal (se hvorfor i $DateThreshold definitionen).

Funktioner:

##*=============================================
##* FUNCTION LISTINGS
##*=============================================
#region Function Write-Log
Function Write-Log
{
    param 
    (
        [Parameter(Mandatory=$true, HelpMessage="Provide a message")][string]$LogOutput,
        [Parameter(Mandatory=$true, HelpMessage="Provide the function name")][string]$FunctionName,
        [Parameter(Mandatory=$false, HelpMessage="Provide the scriptlinenumber")][string]$ScriptLine,
        [Parameter(Mandatory=$false, HelpMessage="Provide path, default is .\Logs")][string]$Path = "$PSScriptRoot\Logs",
        [Parameter(Mandatory=$false, HelpMessage="Provide name for the log")][string]$Name,
        [Parameter(Mandatory=$false, HelpMessage="Provide level, 1 = default, 2 = warning 3 = error")][ValidateSet(1, 2, 3)][int]$LogLevel = 1
    )

    #If the scriptline is not defined then use from the invocation
    If(!($ScriptLine)){
        $ScriptLine = $($MyInvocation.ScriptLineNumber)
    }

    if($LogOutput){

        #Date for the lognaming
        $FullLogName = ($Path + "\" + $Name + ".log")
        $FullSecodaryLogName = ($FullLogName).Replace(".log",".lo_")

        #If the log has reached over xx mb then rename it
        if(Test-Path $FullLogName){
            if((Get-Item $FullLogName).Length -gt 5000kb){
                if(Test-Path $FullSecodaryLogName){
                    Remove-Item -Path $FullSecodaryLogName -force
                }
                Rename-Item -Path $FullLogName -NewName $FullSecodaryLogName
            }
        }

        #First check if folder/logfile exists, if not then create it
        if(!(test-path $Path)){
            New-Item -ItemType Directory -Force -Path $Path -ErrorAction SilentlyContinue
        }

        #Get current date and time to write to log
        $TimeGenerated = "$(Get-Date -Format HH:mm:ss).$((Get-Date).Millisecond)+000"

        #Construct the logline format
        $Line = '<![LOG[{0}]LOG]!><time="{1}" date="{2}" component="{3}" context="" type="{4}" thread="" file="">'

        #Define line
        $LineFormat = $logOutput, $TimeGenerated, (Get-Date -Format MM-dd-yyyy), "$($FunctionName):$($Scriptline)", $LogLevel

        #Append line
        $Line = $Line -f $LineFormat

        #Write log
        try {
            Write-Host ("[$($FunctionName):$($Scriptline)]" + $logOutput)
            $Line | Out-File -FilePath ($Path + "\" + $Name + ".log") -Append -NoClobber -Force -Encoding 'UTF8' -ErrorAction 'Stop'
        }
        catch {
            Write-Host "$_"
        }
    }
}
#endregion
##*=============================================
##* END FUNCTION LISTINGS
##*=============================================

På linie 44-105 finder vi funktionerne, der som udgangspunkt ikke skal ændres. I denne opsætning er der kun én funktion. Funtionen hedder Write-Log og den bruges til at skrive til $LogName referencen. Den benyttes i sidste sektion, nemlig hoveddelen.

Hoveddelen:

##*=============================================
##* MAIN START
##*=============================================

#Start write log
Write-Log -LogOutput ("*********************************************** SCRIPT START ***********************************************") -FunctionName $($MyInvocation.MyCommand) -Path $LogLocation -Name $LogName | Out-Null
Write-Log -LogOutput ("Fetching information from objects in AD and moving computer objects to '$($MoveToOU)' for computer objects that have lastLogon '$($Days)' days ago..") -FunctionName $($MyInvocation.MyCommand) -Path $LogLocation -Name $LogName | Out-Null

#Get the date between now and the $Days.
$DateThreshold = (Get-Date).AddDays($Days)

#Catch, log and stop if anything goes wrong
try{

    #Ensure that the variable is defined as an array
    $Computers = @()

    #Add to the array from the defined OUs
    foreach($OU in $SearchOUs){

        #Add all computer objects that is enabled, and last logon is XXX days.
        $Computers += Get-ADComputer -Property Name,lastLogonDate -Filter {lastLogonDate -lt $DateThreshold -AND Enabled -eq $true} -SearchBase $OU

        #Add all computers that have been created but have never logged on, and is created more than xx days ago
        $Computers += Get-ADComputer -Property Name,lastLogonDate,whenCreated -Filter  {lastlogondate -notlike "*" -AND whenCreated -lt $DateThreshold -AND Enabled -eq $true} -SearchBase $OU
    }

    #Disable the computer objects.
    Write-Log -LogOutput  ("Number og computers to disable is '$($Computers.Count)'. Computers:`n$($Computers.Name -join "`n")") -FunctionName $($MyInvocation.MyCommand) -Path $LogLocation -Name $LogName | Out-Null
    $Computers | Set-ADComputer -Server $DomainName -Enabled $false -Description ("Disabled (Script) - " + (Get-Date -UFormat "%d-%m-%Y %H:%M:%S")) 

    #Move all the computers to the disabled OU.
    $Computers | Move-ADObject -Server $DomainName -TargetPath $MoveToOU
}
catch {
    
    #Write error to log
    Write-Log -LogOutput ("$_") -FunctionName $($MyInvocation.MyCommand) -Path $LogLocation -Name $LogName -LogLevel 3 | Out-Null
}

#End script
Write-Log -LogOutput ("*********************************************** SCRIPT END ***********************************************") -FunctionName $($MyInvocation.MyCommand) -Path $LogLocation -Name $LogName | Out-Null
##*=============================================
##* END MAIN
##*=============================================

Vi finder hoveddelen på linie 112-150 – som udgangspunkt ikke skal ændres – og her bliver det rigtigt sjovt, fordi det er her scriptet udfører sin egentlige opgave – nemlig at søge, sortere, flytte og deaktiverer computerobjekterne.

    #Add to the array from the defined OUs
    foreach($OU in $SearchOUs){

        #Add all computer objects that is enabled, and last logon is XXX days.
        $Computers += Get-ADComputer -Property Name,lastLogonDate -Filter {lastLogonDate -lt $DateThreshold -AND Enabled -eq $true} -SearchBase $OU

        #Add all computers that have been created but have never logged on, and is created more than xx days ago
        $Computers += Get-ADComputer -Property Name,lastLogonDate,whenCreated -Filter  {lastlogondate -notlike "*" -AND whenCreated -lt $DateThreshold -AND Enabled -eq $true} -SearchBase $OU
    }

Lad os kigge nærmere på hoveddelen. Herover finder vi et udsnit af scriptet, hvor der tilføjes til den variable($Computers) der indeholder de objekter, som skal behandles. Det er indsat i en foreach-løkke for at søge og tilføje ud fra hver af de OU’er vi har angivet i $SearchOUs. For hver OU finder scriptet de objekter der ikke er logget på siden de angivende antal dage. Scriptet tilføjer også de objekter der er oprettet før de angivet antal dage, og som aldrig er logget på.
Dem der aldrig er logget på kunne være computere der er oprettet via en Microsoft Deployment Toolkit (MDT) eller System Center Configuration Manager(SCCM) Task Sequence og er fejlet, eller som blev afbrudt under processen.

    #Disable the computer objects.
    Write-Log -LogOutput  ("Number og computers to disable is '$($Computers.Count)'. Computers:`n$($Computers.Name -join "`n")") -FunctionName $($MyInvocation.MyCommand) -Path $LogLocation -Name $LogName | Out-Null
    $Computers | Set-ADComputer -Server $DomainName -Enabled $false -Description ("Disabled (Script) - " + (Get-Date -UFormat "%d-%m-%Y %H:%M:%S")) 

    #Move all the computers to the disabled OU.
    $Computers | Move-ADObject -Server $DomainName -TargetPath $MoveToOU

Herover ser vi delen hvor objekterne bliver deaktiveret og flyttet. De får også tilføjet en beskrivelse med datoen for hvornår de blev deaktiveret.

Overfør scriptet og test

Nu hvor variablerne er ændret efter ens behov og miljø, skal scriptet gemmes og overføres til serveren i en ønsket mappe, her overfører vi den til “C:\Maintenance\DisableComputers\Disable-Computers.ps1”. Når scriptet er overført og servicekontoens rettigheder er på plads, kan det planlagte job oprettes og eksekveres af kontoen. Læs vejledningen “opret et planlagt job” for at læse hvordan man opnår det. I vores eksempel har vi oprettet jobbet til at starte et program 1 gang i døgnet med disse informationer:
Program/script:
Powershell.exe

Add arguments(optional):
-ExecutionPolicy Bypass -WindowStyle Hidden -File “C:\Maintenance\DisableComputers\Disable-Computers.ps1”

Automatisk oprydning af computerobjekter i Active Directory

Herover ses løsningen i aktion.

Afsluttende ord

Hos Zwable har vi mange års erfaring med drift, både On-Premise og i Azure AD. Med automatiseringer som denne, ved vi at man opnår tid til mere relevante arbejdsopgaver samtidigt med at ubudne gæster ikke kan udnytte disse sikkerhedshuller.