param( [Parameter(Mandatory=$true)] [string]$InputFile ) # Read meta JSON try { $json = Get-Content -Raw -Path $InputFile -ErrorAction Stop $meta = $json | ConvertFrom-Json } catch { Write-Output (@{ success = $false; message = "Failed to read/parse meta JSON: $($_.Exception.Message)" } | ConvertTo-Json -Compress) exit 1 } $csvFile = [string]$meta.input_file # PowerShell 5.1 doesn't support the null-coalescing operator '??'. # Use an explicit check here to set the default delimiter. $delimiter = [string]$meta.delimiter if ([string]::IsNullOrWhiteSpace($delimiter)) { $delimiter = ',' } $hasHeader = [bool]($meta.has_header -as [bool]) $dryRun = [bool]($meta.dry_run -as [bool]) $defaultOu = [string]$meta.default_ou if (-not (Test-Path -Path $csvFile)) { Write-Output (@{ success = $false; message = "CSV file not found: $csvFile" } | ConvertTo-Json -Compress) exit 1 } # Ensure ActiveDirectory module is available try { Import-Module ActiveDirectory -ErrorAction Stop } catch { Write-Output (@{ success = $false; message = "ActiveDirectory PowerShell module not available: $($_.Exception.Message)" } | ConvertTo-Json -Compress) exit 1 } # Read CSV try { if ($hasHeader) { $items = Import-Csv -Path $csvFile -Delimiter $delimiter -ErrorAction Stop } else { # Use default headers $headers = 'samaccountname','displayname','mail','password','groups' $items = Import-Csv -Path $csvFile -Delimiter $delimiter -Header $headers -ErrorAction Stop } } catch { Write-Output (@{ success = $false; message = "Failed to parse CSV: $($_.Exception.Message)" } | ConvertTo-Json -Compress) exit 1 } $actor = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name $results = @() $successCount = 0 $failCount = 0 foreach ($row in $items) { $sam = $row.samaccountname $display = $row.displayname $mail = $row.mail $pass = $row.password $groups = $row.groups if ([string]::IsNullOrWhiteSpace($ou) -and -not [string]::IsNullOrWhiteSpace($defaultOu)) { $ou = $defaultOu } # Password hint text (German) $passwordHint = @" Das sollten die Anforderungen an das Passwort sein: - mindestens 7 Zeichen - darf den Benutzer-/Accountnamen nicht enthalten (bzw. keine zu großen Teile davon) - muss Zeichen aus mindestens 3 von 4 Kategorien enthalten: 1) Großbuchstaben (A–Z) 2) Kleinbuchstaben (a–z) 3) Ziffern (0–9) 4) Sonderzeichen (alles, was kein Buchstabe/Zahl ist, z. B. ! ? # _ - . , usw.) "@ function Test-PasswordRequirements { param( [string]$Password, [string]$SamAccountName ) $errors = @() if ([string]::IsNullOrEmpty($Password) -or $Password.Length -lt 7) { $errors += 'Passwort muss mindestens 7 Zeichen lang sein.' } # Categories: uppercase, lowercase, digit, special $categories = 0 if ($Password -match '[A-Z]') { $categories++ } if ($Password -match '[a-z]') { $categories++ } if ($Password -match '\d') { $categories++ } if ($Password -match '[^A-Za-z0-9]') { $categories++ } if ($categories -lt 3) { $errors += 'Passwort muss Zeichen aus mindestens 3 von 4 Kategorien enthalten (Groß, Klein, Ziffern, Sonderzeichen).' } # Check for username inclusion or large parts of it (case-insensitive) if (-not [string]::IsNullOrEmpty($SamAccountName)) { $pwLower = $Password.ToLowerInvariant() $samLower = $SamAccountName.ToLowerInvariant() if ($pwLower -like "*${samLower}*") { $errors += 'Passwort darf den Benutzernamen nicht enthalten.' } else { # Check for substrings of username of length >= 4 $minLen = 4 if ($samLower.Length -ge $minLen) { for ($len = $minLen; $len -le $samLower.Length; $len++) { for ($start = 0; $start -le $samLower.Length - $len; $start++) { $sub = $samLower.Substring($start, $len) if ($pwLower -like "*${sub}*") { $errors += 'Passwort darf keine größeren Teile des Benutzernamens enthalten.' break 2 } } } } } } return $errors } if ([string]::IsNullOrWhiteSpace($sam) -or [string]::IsNullOrWhiteSpace($pass)) { $results += @{ sam = $sam; success = $false; message = 'Missing samaccountname or password' } $failCount++ continue } # Validate password according to requirements, also during dry run so issues are visible early $pwErrors = Test-PasswordRequirements -Password $pass -SamAccountName $sam if ($pwErrors.Count -gt 0) { $results += @{ sam = $sam; success = $false; message = ('Invalid password: ' + ($pwErrors -join ' | ')); hint = $passwordHint } $failCount++ continue } if ($dryRun) { $results += @{ sam = $sam; success = $true; message = 'DRY RUN: would create (password validated)' } $successCount++ continue } try { $props = @{ Name = if ($display -and $display -ne '') { $display } else { $sam }; SamAccountName = $sam; Enabled = $true } if ($mail -and $mail -ne '') { $props['EmailAddress'] = $mail } if ($ou -and $ou -ne '') { $props['Path'] = $ou } $securePass = ConvertTo-SecureString $pass -AsPlainText -Force $props['AccountPassword'] = $securePass New-ADUser @props -ErrorAction Stop if ($groups -and $groups -ne '') { $groupList = $groups -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' } foreach ($g in $groupList) { Add-ADGroupMember -Identity $g -Members $sam -ErrorAction Stop } } $results += @{ sam = $sam; success = $true; message = 'Created' } $successCount++ } catch { $results += @{ sam = $sam; success = $false; message = $_.Exception.Message } $failCount++ } } $output = @{ success = $failCount -eq 0; message = "Created $successCount users, $failCount failures"; details = $results; actor = $actor } Write-Output ($output | ConvertTo-Json -Compress) exit 0