AD User Query and Selecting

Für einen regelmässigen Export aus dem Active Directory habe ich mich entschlossen das bestehende VBScript durch ein, von Grund auf neu geschriebenes, Powershell Script zu ersetzten. Active Directory Abfragen, welche früher in VBScript mit AdoDB und dem ADSI Interface gemacht wurden können mit Powershell bequem über die entsprechenden Cmdlets realisiert werden. Dabei gibt es ein paar Unterschiede zwischen dem ADSI Interface und dem Active Directory Web Services, welches von den Powershell Cmdlets genutzt wird, zu berücksichtigen. Ein Beispiel ist der Security Identifier (SID) welcher von ADSI Binär und von AD Web Services als String Repräsentation geliefert wird. Genau dieser wurde in oben erwähntem Export explizit in Binär Form gewünscht, wofür ich eine Konvertierung in das Powershell Script einbauen musste. Falls noch weitere solche Anforderungen auftauchen habe ich ein wieder verwendbares Snippet erstellt was mir ermöglicht auf allen Attributen der Abfrage Modifikationen durchzuführen. Das sieht folgendermassen aus

[object[]]$Attributes = 
"sAMAccountName",
"givenName",
"sn",
"Title",
"Department",
"Company",
@{L="objectSid";E={
  $buf = [Byte[]]::new($_.objectSid.BinaryLength)
  $_.objectSid.GetBinaryForm($buf,0)
  ([System.BitConverter]::ToString($buf)).Replace("-","")
}}

$Properties = ($Attributes | ForEach-Object { @($_, $_.L)[$_.L -ne $null] })
Get-ADUser -LdapFilter "(cn=pri)" -Properties $Properties | Select-Object $Attributes | Format-Table -AutoSize

In Zeile 1-12 wird ein Array aus Objekten erstellt, welches entweder nur den Attribut Namen als String enthält, wenn keine Modifikationen durchgeführt werden sollen oder eine Hashtable mit den Keys L (Label) und E (Expression) welche als Input für das Cmdlet Select-Object verwendet werden kann. Zeile 14 bildet ein String Array welches als Parameter für das -Properties Attribut der RSAT-AD Cmdlets verwendet werden kann.

Das Resultat sieht folgendermassen (hier als Tabelle formatiert) aus.

Powershell Ternary Operator

Ich finden den Ternary Operator (Vereinfachung einer if-then-else Anweisung auf einer Zeile) sehr praktisch. Er ist leserlicher als eine simple if-then-else Anweisung, welche sich über mehrere Zeilen verteilt. Viele Sprachen haben den Ternary Operator eingebaut. Nachfolgend ein Beispiel, wenn die Variable «threshold» grösser als 80 ist wird der String «critical», ansonsten «normal» ausgegeben.

# Konstruktion
value_if_true if condition else value_if_false

# Beispiel
threshold = 79
print('critical' if threshold > 80 else 'normal')
# >> normal
// Konstruktion
condition ? value_if_true : value_if_false

// Beispiel
#include <stdio.h>

int main() {
    int threshold = 81;
    printf("%s", threshold > 80 ? "critical" : "normal");
}
// >> critical

Obschon es den Ternary Operator für Powershell nicht gibt kann sein Verhalten «nachgebaut» werden. Dafür kann die Tatsache, dass ein boolscher Wert durch eine 0 oder eine 1 repräsentiert wird in Zusammenhang mit einem Array verwendet werden. Je nach Script / Programmiersprache ist es unterschiedlich wie ein Wahr oder ein Falsch intern abgespeichert wird. Für Powershell finden wir das mit folgendem Code heraus.

[int]$true
# >> 1
[int]$false
# >> 0

Daraus ist ersichtlich, dass im Array Index 0 der Falsch Wert stehen muss und im Index 1 der Wahr Wert. Das oben verwendete Beispiel sieht dann folgendermassen aus.

# Konstruktion
array(value_if_false,value_if_true)[condition]

# Beispiel
[int]$threshold = 79
Write-Host @('normal','critical')[$threshold -gt 80]
# >> normal

Wofür das Ganze sinnvoll eingesetzt werden kann ist der Kreativität überlassen. In einem noch folgenden Beitrag werde ich aufzeigen wofür ich es bereits verwendet habe.

Powershell Logfile Archiver

Leider gibt es heutzutage immer noch massenhaft Anwendungen, welche frisch fröhlich ihre Logfiles auf die Festplatte schreiben als gäbe es kein Morgen mehr. Dies hat zur Folge, dass der Platz auf der Festplatte immer weiter schrumpft und im ungünstigsten Fall, wenn es sich um die Systemplatte handelt, der ganze Server nicht mehr starten kann.

Um dem entgegen zu wirken habe ich ein kleines Script erstellt, welches alle Dateien welche älter als X Tage sind in komprimierten Archiven nach Monat ablegt. Die Monate wiederum in Ordner als Jahr abgelegt. Weiter löscht das Script alle Archiv Dateien, welcher älter als Y Tage sind. Somit haben wir die Logfiles welche jünger als X Tage sind im Klartext, alle die älter als X Tage sind komprimiert in Monats Archiven und alles was älter als Y Tage ist gar nicht mehr.

<#
.SYNOPSIS
  Moves Logfiles to archive packages for amount of time

.DESCRIPTION
  Moves logfiles older than specified days to monthly archives. Deletes archives older than archive retention days.

.PARAMETER Path
  Path to the folder where the logfiles are

.PARAMETER ArchivesRoot
  Path where the archives shall be stored. Defaults to $Path\Archives

.PARAMETER RetentionDays
  Number of days to skip archiving for. Defaults to 30

.PARAMETER RetentionArchiveDays
  Number of days which trigger an archive to be deleted if older. Defaults to 365

.INPUTS
  Path of the logfile folder

.OUTPUTS
  Reorganized Logfiles folder

.NOTES
  Author: Philipp R
  Version: 1.0
  Last Modify: 01.04.2020

.EXAMPLE
  .\LogArchiver.ps1 -Path C:\my\logs
#>
#Requires -Version 4

Param(
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)][string]$Path,
    [string]$ArchivesRoot = "$Path\Archives",
    [int]$RetentionDays = 30,
    [int]$RetentionArchiveDays = 365
)

Begin{
    if(-not(Test-Path $Path)){
        Write-Host -ForegroundColor Red "Path not found, $Path"
        return
    }
}

Process{
    $CompressionCandidates = Get-ChildItem $Path -File | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$RetentionDays)}
    try{
        $CompressionCandidates | 
        Group-Object -Property {$_.LastWriteTime.Year} | ForEach-Object {
            $Year = $_.Name
            $_.Group | Group-Object -Property {$_.LastWriteTime.Month} | ForEach-Object {
                $TargetArchive = "$ArchivesRoot\$Year\$($_.Name).zip"
                # Create the year folder
                $null = New-Item -ItemType Directory -Path (Split-Path $TargetArchive) -ErrorAction SilentlyContinue
                # if archive exists add it otherwise create new
                $_.Group | Compress-Archive -Update:(Test-Path $TargetArchive) -DestinationPath $TargetArchive
            }
        }
        # Only remove files if we successfully archived them
        $CompressionCandidates | Select-Object -ExpandProperty FullName |  Remove-Item  -Force
    }
    catch {
        Write-Host -ForegroundColor Red "Something strange happened"
        $_.Exception
    }

    # Remove old archives
    $DueDate = (Get-Date).AddDays(-$RetentionArchiveDays)
    Get-ChildItem $ArchivesRoot | Where-Object { [int]($_.Name) -lt [int]($DueDate.Year) } | Select-Object -ExpandProperty FullName | Remove-Item -Recurse -Force
    Get-ChildItem "$ArchivesRoot\$($DueDate.Year)" | Where-Object { [int]($_.Name.Split(".")[0]) -lt $DueDate.Month } | Select-Object -ExpandProperty FullName | Remove-Item -Recurse -Force
}

End{

}

XOR Logik mit 3 Inputs

Kürzlich musste ich ein Powershell Script schreiben, das zu Anfang die Argumente prüfen sollte. Das Script kann mit 3 Parameter aufgerufen werden: -Install, -Uninstall und -Action
Von diesen 3 Parameter darf jeweils nur einer gleichzeitig verwendet werden, also habe ich ein paar logische Operatoren zusammen verknüpft und voilà dies ist das Ergebnis:

if(($Install -and $Uninstall -and $Action) -or -not ($Install -xor $Action -xor $Uninstall))
{
	Write-Host -ForegroundColor Red "Invalid Parameters please read script header for usage"
	exit(1)
}

Die Wahrheitstabelle für ein XOR Gatter mit 3 Inputs:

$a = $true,$false,$false
$a[0] -xor $a[1] -xor $a[2] #true
$a = $true,$true,$false
$a[0] -xor $a[1] -xor $a[2] #false
$a = $true,$true,$true
$a[0] -xor $a[1] -xor $a[2] #true
$a = $true,$false,$true
$a[0] -xor $a[1] -xor $a[2] #false
$a = $false,$true,$true
$a[0] -xor $a[1] -xor $a[2] #false
$a = $false,$false,$true
$a[0] -xor $a[1] -xor $a[2] #true
$a = $false,$false,$false
$a[0] -xor $a[1] -xor $a[2] #false
$a = $false,$true,$false
$a[0] -xor $a[1] -xor $a[2] #true

3InputXOR